1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.drawer;
18 
19 import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;
20 import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;
21 import static com.android.settingslib.drawer.TileUtils.META_DATA_NEW_TASK;
22 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
23 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
24 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
25 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
26 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
27 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
28 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
29 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
30 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
31 import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;
32 import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;
33 
34 import android.app.PendingIntent;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.pm.ComponentInfo;
38 import android.content.pm.PackageManager;
39 import android.content.res.Resources;
40 import android.content.res.TypedArray;
41 import android.graphics.drawable.Icon;
42 import android.os.Bundle;
43 import android.os.Parcel;
44 import android.os.Parcelable;
45 import android.os.UserHandle;
46 import android.text.TextUtils;
47 import android.util.Log;
48 
49 import androidx.annotation.VisibleForTesting;
50 
51 import java.util.ArrayList;
52 import java.util.Comparator;
53 import java.util.HashMap;
54 
55 /** Description of a single dashboard tile that the user can select. */
56 public abstract class Tile implements Parcelable {
57 
58     private static final String TAG = "Tile";
59 
60     /** Optional list of user handles which the intent should be launched on. */
61     public ArrayList<UserHandle> userHandle = new ArrayList<>();
62 
63     public HashMap<UserHandle, PendingIntent> pendingIntentMap = new HashMap<>();
64 
65     @VisibleForTesting
66     long mLastUpdateTime;
67     private final String mComponentPackage;
68     private final String mComponentName;
69     private final Intent mIntent;
70 
71     protected ComponentInfo mComponentInfo;
72     private CharSequence mSummaryOverride;
73     private Bundle mMetaData;
74     private String mCategory;
75     private String mGroupKey;
76 
Tile(ComponentInfo info, String category, Bundle metaData)77     public Tile(ComponentInfo info, String category, Bundle metaData) {
78         mComponentInfo = info;
79         mComponentPackage = mComponentInfo.packageName;
80         mComponentName = mComponentInfo.name;
81         mCategory = category;
82         mMetaData = metaData;
83         if (mMetaData != null) {
84             mGroupKey = metaData.getString(META_DATA_PREFERENCE_GROUP_KEY);
85         }
86         mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
87         if (isNewTask()) {
88             mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
89         }
90     }
91 
Tile(Parcel in)92     Tile(Parcel in) {
93         mComponentPackage = in.readString();
94         mComponentName = in.readString();
95         mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
96         final int number = in.readInt();
97         for (int i = 0; i < number; i++) {
98             userHandle.add(UserHandle.CREATOR.createFromParcel(in));
99         }
100         mCategory = in.readString();
101         mMetaData = in.readBundle();
102         if (isNewTask()) {
103             mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
104         }
105         mGroupKey = in.readString();
106     }
107 
108     @Override
describeContents()109     public int describeContents() {
110         return 0;
111     }
112 
113     @Override
writeToParcel(Parcel dest, int flags)114     public void writeToParcel(Parcel dest, int flags) {
115         dest.writeBoolean(this instanceof ProviderTile);
116         dest.writeString(mComponentPackage);
117         dest.writeString(mComponentName);
118         final int size = userHandle.size();
119         dest.writeInt(size);
120         for (int i = 0; i < size; i++) {
121             userHandle.get(i).writeToParcel(dest, flags);
122         }
123         dest.writeString(mCategory);
124         dest.writeBundle(mMetaData);
125         dest.writeString(mGroupKey);
126     }
127 
128     /** Unique ID of the tile */
getId()129     public abstract int getId();
130 
131     /** Human-readable description of the tile */
getDescription()132     public abstract String getDescription();
133 
getComponentInfo(Context context)134     protected abstract ComponentInfo getComponentInfo(Context context);
135 
getComponentLabel(Context context)136     protected abstract CharSequence getComponentLabel(Context context);
137 
getComponentIcon(ComponentInfo info)138     protected abstract int getComponentIcon(ComponentInfo info);
139 
getPackageName()140     public String getPackageName() {
141         return mComponentPackage;
142     }
143 
getComponentName()144     public String getComponentName() {
145         return mComponentName;
146     }
147 
148     /** Intent to launch when the preference is selected. */
getIntent()149     public Intent getIntent() {
150         return mIntent;
151     }
152 
153     /** Category in which the tile should be placed. */
getCategory()154     public String getCategory() {
155         return mCategory;
156     }
157 
setCategory(String newCategoryKey)158     public void setCategory(String newCategoryKey) {
159         mCategory = newCategoryKey;
160     }
161 
162     /** Priority of this tile, used for display ordering. */
getOrder()163     public int getOrder() {
164         if (hasOrder()) {
165             return mMetaData.getInt(META_DATA_KEY_ORDER);
166         } else {
167             return 0;
168         }
169     }
170 
171     /** Check whether tile has order. */
hasOrder()172     public boolean hasOrder() {
173         return mMetaData != null
174                 && mMetaData.containsKey(META_DATA_KEY_ORDER)
175                 && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
176     }
177 
178     /** Check whether tile has a switch. */
hasSwitch()179     public boolean hasSwitch() {
180         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SWITCH_URI);
181     }
182 
183     /** Check whether tile has a pending intent. */
hasPendingIntent()184     public boolean hasPendingIntent() {
185         return !pendingIntentMap.isEmpty();
186     }
187 
188     /** Title of the tile that is shown to the user. */
getTitle(Context context)189     public CharSequence getTitle(Context context) {
190         CharSequence title = null;
191         ensureMetadataNotStale(context);
192         final PackageManager packageManager = context.getPackageManager();
193         if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
194             if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
195                 // If has as uri to provide dynamic title, skip loading here. UI will later load
196                 // at tile binding time.
197                 return null;
198             }
199             if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
200                 try {
201                     final Resources res =
202                             packageManager.getResourcesForApplication(mComponentPackage);
203                     title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE));
204                 } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
205                     Log.w(TAG, "Couldn't find info", e);
206                 }
207             } else {
208                 title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
209             }
210         }
211         // Set the preference title by the component if no meta-data is found
212         if (title == null) {
213             title = getComponentLabel(context);
214         }
215         return title;
216     }
217 
218     /**
219      * Overrides the summary. This can happen when injected tile wants to provide dynamic summary.
220      */
overrideSummary(CharSequence summaryOverride)221     public void overrideSummary(CharSequence summaryOverride) {
222         mSummaryOverride = summaryOverride;
223     }
224 
225     /** Optional summary describing what this tile controls. */
getSummary(Context context)226     public CharSequence getSummary(Context context) {
227         if (mSummaryOverride != null) {
228             return mSummaryOverride;
229         }
230         ensureMetadataNotStale(context);
231         CharSequence summary = null;
232         final PackageManager packageManager = context.getPackageManager();
233         if (mMetaData != null) {
234             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
235                 // If has as uri to provide dynamic summary, skip loading here. UI will later load
236                 // at tile binding time.
237                 return null;
238             }
239             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
240                 if (mMetaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
241                     try {
242                         final Resources res =
243                                 packageManager.getResourcesForApplication(mComponentPackage);
244                         summary = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_SUMMARY));
245                     } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
246                         Log.d(TAG, "Couldn't find info", e);
247                     }
248                 } else {
249                     summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY);
250                 }
251             }
252         }
253         return summary;
254     }
255 
setMetaData(Bundle metaData)256     public void setMetaData(Bundle metaData) {
257         mMetaData = metaData;
258     }
259 
260     /** The metaData from the activity that defines this tile. */
getMetaData()261     public Bundle getMetaData() {
262         return mMetaData;
263     }
264 
265     /** Optional key to use for this tile. */
getKey(Context context)266     public String getKey(Context context) {
267         ensureMetadataNotStale(context);
268         if (!hasKey()) {
269             return null;
270         }
271         if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
272             return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
273         } else {
274             return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT);
275         }
276     }
277 
278     /** Check whether title has key. */
hasKey()279     public boolean hasKey() {
280         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_KEYHINT);
281     }
282 
283     /**
284      * Optional icon to show for this tile.
285      *
286      * @attr ref android.R.styleable#PreferenceHeader_icon
287      */
getIcon(Context context)288     public Icon getIcon(Context context) {
289         if (context == null || mMetaData == null) {
290             return null;
291         }
292         ensureMetadataNotStale(context);
293         final ComponentInfo componentInfo = getComponentInfo(context);
294         if (componentInfo == null) {
295             Log.w(TAG, "Cannot find ComponentInfo for " + getDescription());
296             return null;
297         }
298 
299         int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON);
300         // Set the icon. Skip the transparent color for backward compatibility since Android S.
301         if (iconResId != 0 && iconResId != android.R.color.transparent) {
302             final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);
303             if (isIconTintable(context)) {
304                 final TypedArray a =
305                         context.obtainStyledAttributes(
306                                 new int[] {android.R.attr.colorControlNormal});
307                 final int tintColor = a.getColor(0, 0);
308                 a.recycle();
309                 icon.setTint(tintColor);
310             }
311             return icon;
312         } else {
313             return null;
314         }
315     }
316 
317     /**
318      * Whether the icon can be tinted. This is true when icon needs to be monochrome (single-color)
319      */
isIconTintable(Context context)320     public boolean isIconTintable(Context context) {
321         ensureMetadataNotStale(context);
322         if (mMetaData != null
323                 && mMetaData.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) {
324             return mMetaData.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE);
325         }
326         return false;
327     }
328 
329     /** Whether the {@link Activity} should be launched in a separate task. */
isNewTask()330     public boolean isNewTask() {
331         if (mMetaData != null && mMetaData.containsKey(META_DATA_NEW_TASK)) {
332             return mMetaData.getBoolean(META_DATA_NEW_TASK);
333         }
334         return false;
335     }
336 
337     /** Ensures metadata is not stale for this tile. */
ensureMetadataNotStale(Context context)338     private void ensureMetadataNotStale(Context context) {
339         final PackageManager pm = context.getApplicationContext().getPackageManager();
340 
341         try {
342             final long lastUpdateTime =
343                     pm.getPackageInfo(mComponentPackage, PackageManager.GET_META_DATA)
344                             .lastUpdateTime;
345             if (lastUpdateTime == mLastUpdateTime) {
346                 // All good. Do nothing
347                 return;
348             }
349             // App has been updated since we load metadata last time. Reload metadata.
350             mComponentInfo = null;
351             getComponentInfo(context);
352             mLastUpdateTime = lastUpdateTime;
353         } catch (PackageManager.NameNotFoundException e) {
354             Log.d(TAG, "Can't find package, probably uninstalled.");
355         }
356     }
357 
358     public static final Creator<Tile> CREATOR =
359             new Creator<Tile>() {
360                 public Tile createFromParcel(Parcel source) {
361                     final boolean isProviderTile = source.readBoolean();
362                     // reset the Parcel pointer before delegating to the real constructor.
363                     source.setDataPosition(0);
364                     return isProviderTile ? new ProviderTile(source) : new ActivityTile(source);
365                 }
366 
367                 public Tile[] newArray(int size) {
368                     return new Tile[size];
369                 }
370             };
371 
372     /** Check whether tile only has primary profile. */
isPrimaryProfileOnly()373     public boolean isPrimaryProfileOnly() {
374         return isPrimaryProfileOnly(mMetaData);
375     }
376 
isPrimaryProfileOnly(Bundle metaData)377     static boolean isPrimaryProfileOnly(Bundle metaData) {
378         String profile = metaData != null ? metaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL;
379         profile = (profile != null ? profile : PROFILE_ALL);
380         return TextUtils.equals(profile, PROFILE_PRIMARY);
381     }
382 
383     /** Returns whether the tile belongs to another group / category. */
hasGroupKey()384     public boolean hasGroupKey() {
385         return !TextUtils.isEmpty(mGroupKey);
386     }
387 
388     /** Set the group / PreferenceCategory key this tile belongs to. */
setGroupKey(String groupKey)389     public void setGroupKey(String groupKey) {
390         mGroupKey = groupKey;
391     }
392 
393     /** Returns the group / category key this tile belongs to. */
getGroupKey()394     public String getGroupKey() {
395         return mGroupKey;
396     }
397 
398     /** Returns if this is searchable. */
isSearchable()399     public boolean isSearchable() {
400         return mMetaData == null || mMetaData.getBoolean(META_DATA_PREFERENCE_SEARCHABLE, true);
401     }
402 
403     /** The type of the tile. */
404     public enum Type {
405         /** A preference that can be tapped on to open a new page. */
406         ACTION,
407 
408         /** A preference that can be tapped on to open an external app. */
409         EXTERNAL_ACTION,
410 
411         /** A preference that shows an on / off switch that can be toggled by the user. */
412         SWITCH,
413 
414         /**
415          * A preference with both an on / off switch, and a tappable area that can perform an
416          * action.
417          */
418         SWITCH_WITH_ACTION,
419 
420         /**
421          * A preference category with a title that can be used to group multiple preferences
422          * together.
423          */
424         GROUP
425     }
426 
427     /**
428      * Returns the type of the tile.
429      *
430      * @see Type
431      */
getType()432     public Type getType() {
433         boolean hasExternalAction = hasPendingIntent();
434         boolean hasAction = hasExternalAction || this instanceof ActivityTile;
435         boolean hasSwitch = hasSwitch();
436 
437         if (hasSwitch && hasAction) {
438             return Type.SWITCH_WITH_ACTION;
439         } else if (hasSwitch) {
440             return Type.SWITCH;
441         } else if (hasExternalAction) {
442             return Type.EXTERNAL_ACTION;
443         } else if (hasAction) {
444             return Type.ACTION;
445         } else {
446             return Type.GROUP;
447         }
448     }
449 
450     public static final Comparator<Tile> TILE_COMPARATOR =
451             (lhs, rhs) -> rhs.getOrder() - lhs.getOrder();
452 }
453