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 /**
56  * Description of a single dashboard tile that the user can select.
57  */
58 public abstract class Tile implements Parcelable {
59 
60     private static final String TAG = "Tile";
61 
62     /**
63      * Optional list of user handles which the intent should be launched on.
64      */
65     public ArrayList<UserHandle> userHandle = new ArrayList<>();
66 
67     public HashMap<UserHandle, PendingIntent> pendingIntentMap = new HashMap<>();
68 
69     @VisibleForTesting
70     long mLastUpdateTime;
71     private final String mComponentPackage;
72     private final String mComponentName;
73     private final Intent mIntent;
74 
75     protected ComponentInfo mComponentInfo;
76     private CharSequence mSummaryOverride;
77     private Bundle mMetaData;
78     private String mCategory;
79 
Tile(ComponentInfo info, String category, Bundle metaData)80     public Tile(ComponentInfo info, String category, Bundle metaData) {
81         mComponentInfo = info;
82         mComponentPackage = mComponentInfo.packageName;
83         mComponentName = mComponentInfo.name;
84         mCategory = category;
85         mMetaData = metaData;
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     }
106 
107     @Override
describeContents()108     public int describeContents() {
109         return 0;
110     }
111 
112     @Override
writeToParcel(Parcel dest, int flags)113     public void writeToParcel(Parcel dest, int flags) {
114         dest.writeBoolean(this instanceof ProviderTile);
115         dest.writeString(mComponentPackage);
116         dest.writeString(mComponentName);
117         final int size = userHandle.size();
118         dest.writeInt(size);
119         for (int i = 0; i < size; i++) {
120             userHandle.get(i).writeToParcel(dest, flags);
121         }
122         dest.writeString(mCategory);
123         dest.writeBundle(mMetaData);
124     }
125 
126     /**
127      * Unique ID of the tile
128      */
getId()129     public abstract int getId();
130 
131     /**
132      * Human-readable description of the tile
133      */
getDescription()134     public abstract String getDescription();
135 
getComponentInfo(Context context)136     protected abstract ComponentInfo getComponentInfo(Context context);
137 
getComponentLabel(Context context)138     protected abstract CharSequence getComponentLabel(Context context);
139 
getComponentIcon(ComponentInfo info)140     protected abstract int getComponentIcon(ComponentInfo info);
141 
getPackageName()142     public String getPackageName() {
143         return mComponentPackage;
144     }
145 
getComponentName()146     public String getComponentName() {
147         return mComponentName;
148     }
149 
150     /**
151      * Intent to launch when the preference is selected.
152      */
getIntent()153     public Intent getIntent() {
154         return mIntent;
155     }
156 
157     /**
158      * Category in which the tile should be placed.
159      */
getCategory()160     public String getCategory() {
161         return mCategory;
162     }
163 
setCategory(String newCategoryKey)164     public void setCategory(String newCategoryKey) {
165         mCategory = newCategoryKey;
166     }
167 
168     /**
169      * Priority of this tile, used for display ordering.
170      */
getOrder()171     public int getOrder() {
172         if (hasOrder()) {
173             return mMetaData.getInt(META_DATA_KEY_ORDER);
174         } else {
175             return 0;
176         }
177     }
178 
179     /**
180      * Check whether tile has order.
181      */
hasOrder()182     public boolean hasOrder() {
183         return mMetaData != null
184                 && mMetaData.containsKey(META_DATA_KEY_ORDER)
185                 && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
186     }
187 
188     /**
189      * Check whether tile has a switch.
190      */
hasSwitch()191     public boolean hasSwitch() {
192         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SWITCH_URI);
193     }
194 
195     /**
196      * Check whether tile has a pending intent.
197      */
hasPendingIntent()198     public boolean hasPendingIntent() {
199         return !pendingIntentMap.isEmpty();
200     }
201 
202     /**
203      * Title of the tile that is shown to the user.
204      */
getTitle(Context context)205     public CharSequence getTitle(Context context) {
206         CharSequence title = null;
207         ensureMetadataNotStale(context);
208         final PackageManager packageManager = context.getPackageManager();
209         if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
210             if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
211                 // If has as uri to provide dynamic title, skip loading here. UI will later load
212                 // at tile binding time.
213                 return null;
214             }
215             if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
216                 try {
217                     final Resources res =
218                             packageManager.getResourcesForApplication(mComponentPackage);
219                     title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE));
220                 } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
221                     Log.w(TAG, "Couldn't find info", e);
222                 }
223             } else {
224                 title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
225             }
226         }
227         // Set the preference title by the component if no meta-data is found
228         if (title == null) {
229             title = getComponentLabel(context);
230         }
231         return title;
232     }
233 
234     /**
235      * Overrides the summary. This can happen when injected tile wants to provide dynamic summary.
236      */
overrideSummary(CharSequence summaryOverride)237     public void overrideSummary(CharSequence summaryOverride) {
238         mSummaryOverride = summaryOverride;
239     }
240 
241     /**
242      * Optional summary describing what this tile controls.
243      */
getSummary(Context context)244     public CharSequence getSummary(Context context) {
245         if (mSummaryOverride != null) {
246             return mSummaryOverride;
247         }
248         ensureMetadataNotStale(context);
249         CharSequence summary = null;
250         final PackageManager packageManager = context.getPackageManager();
251         if (mMetaData != null) {
252             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
253                 // If has as uri to provide dynamic summary, skip loading here. UI will later load
254                 // at tile binding time.
255                 return null;
256             }
257             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
258                 if (mMetaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
259                     try {
260                         final Resources res =
261                                 packageManager.getResourcesForApplication(mComponentPackage);
262                         summary = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_SUMMARY));
263                     } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
264                         Log.d(TAG, "Couldn't find info", e);
265                     }
266                 } else {
267                     summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY);
268                 }
269             }
270         }
271         return summary;
272     }
273 
setMetaData(Bundle metaData)274     public void setMetaData(Bundle metaData) {
275         mMetaData = metaData;
276     }
277 
278     /**
279      * The metaData from the activity that defines this tile.
280      */
getMetaData()281     public Bundle getMetaData() {
282         return mMetaData;
283     }
284 
285     /**
286      * Optional key to use for this tile.
287      */
getKey(Context context)288     public String getKey(Context context) {
289         ensureMetadataNotStale(context);
290         if (!hasKey()) {
291             return null;
292         }
293         if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
294             return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
295         } else {
296             return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT);
297         }
298     }
299 
300     /**
301      * Check whether title has key.
302      */
hasKey()303     public boolean hasKey() {
304         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_KEYHINT);
305     }
306 
307     /**
308      * Optional icon to show for this tile.
309      *
310      * @attr ref android.R.styleable#PreferenceHeader_icon
311      */
getIcon(Context context)312     public Icon getIcon(Context context) {
313         if (context == null || mMetaData == null) {
314             return null;
315         }
316         ensureMetadataNotStale(context);
317         final ComponentInfo componentInfo = getComponentInfo(context);
318         if (componentInfo == null) {
319             Log.w(TAG, "Cannot find ComponentInfo for " + getDescription());
320             return null;
321         }
322 
323         int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON);
324         // Set the icon. Skip the transparent color for backward compatibility since Android S.
325         if (iconResId != 0 && iconResId != android.R.color.transparent) {
326             final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);
327             if (isIconTintable(context)) {
328                 final TypedArray a = context.obtainStyledAttributes(new int[]{
329                         android.R.attr.colorControlNormal});
330                 final int tintColor = a.getColor(0, 0);
331                 a.recycle();
332                 icon.setTint(tintColor);
333             }
334             return icon;
335         } else {
336             return null;
337         }
338     }
339 
340     /**
341      * Whether the icon can be tinted. This is true when icon needs to be monochrome (single-color)
342      */
isIconTintable(Context context)343     public boolean isIconTintable(Context context) {
344         ensureMetadataNotStale(context);
345         if (mMetaData != null
346                 && mMetaData.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) {
347             return mMetaData.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE);
348         }
349         return false;
350     }
351 
352     /**
353      * Whether the {@link Activity} should be launched in a separate task.
354      */
isNewTask()355     public boolean isNewTask() {
356         if (mMetaData != null
357                 && mMetaData.containsKey(META_DATA_NEW_TASK)) {
358             return mMetaData.getBoolean(META_DATA_NEW_TASK);
359         }
360         return false;
361     }
362 
363     /**
364      * Ensures metadata is not stale for this tile.
365      */
ensureMetadataNotStale(Context context)366     private void ensureMetadataNotStale(Context context) {
367         final PackageManager pm = context.getApplicationContext().getPackageManager();
368 
369         try {
370             final long lastUpdateTime = pm.getPackageInfo(mComponentPackage,
371                     PackageManager.GET_META_DATA).lastUpdateTime;
372             if (lastUpdateTime == mLastUpdateTime) {
373                 // All good. Do nothing
374                 return;
375             }
376             // App has been updated since we load metadata last time. Reload metadata.
377             mComponentInfo = null;
378             getComponentInfo(context);
379             mLastUpdateTime = lastUpdateTime;
380         } catch (PackageManager.NameNotFoundException e) {
381             Log.d(TAG, "Can't find package, probably uninstalled.");
382         }
383     }
384 
385     public static final Creator<Tile> CREATOR = new Creator<Tile>() {
386         public Tile createFromParcel(Parcel source) {
387             final boolean isProviderTile = source.readBoolean();
388             // reset the Parcel pointer before delegating to the real constructor.
389             source.setDataPosition(0);
390             return isProviderTile ? new ProviderTile(source) : new ActivityTile(source);
391         }
392 
393         public Tile[] newArray(int size) {
394             return new Tile[size];
395         }
396     };
397 
398     /**
399      * Check whether tile only has primary profile.
400      */
isPrimaryProfileOnly()401     public boolean isPrimaryProfileOnly() {
402         return isPrimaryProfileOnly(mMetaData);
403     }
404 
isPrimaryProfileOnly(Bundle metaData)405     static boolean isPrimaryProfileOnly(Bundle metaData) {
406         String profile = metaData != null
407                 ? metaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL;
408         profile = (profile != null ? profile : PROFILE_ALL);
409         return TextUtils.equals(profile, PROFILE_PRIMARY);
410     }
411 
412     /**
413      * Returns whether the tile belongs to another group / category.
414      */
hasGroupKey()415     public boolean hasGroupKey() {
416         return mMetaData != null
417                 && !TextUtils.isEmpty(mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY));
418     }
419 
420     /**
421      * Returns the group / category key this tile belongs to.
422      */
getGroupKey()423     public String getGroupKey() {
424         return (mMetaData == null) ? null : mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY);
425     }
426 
427     /**
428      * Returns if this is searchable.
429      */
isSearchable()430     public boolean isSearchable() {
431         return mMetaData == null || mMetaData.getBoolean(META_DATA_PREFERENCE_SEARCHABLE, true);
432     }
433 
434     /**
435      * The type of the tile.
436      */
437     public enum Type {
438         /**
439          * A preference that can be tapped on to open a new page.
440          */
441         ACTION,
442 
443         /**
444          * A preference that can be tapped on to open an external app.
445          */
446         EXTERNAL_ACTION,
447 
448         /**
449          * A preference that shows an on / off switch that can be toggled by the user.
450          */
451         SWITCH,
452 
453         /**
454          * A preference with both an on / off switch, and a tappable area that can perform an
455          * action.
456          */
457         SWITCH_WITH_ACTION,
458 
459         /**
460          * A preference category with a title that can be used to group multiple preferences
461          * together.
462          */
463         GROUP;
464     }
465 
466     /**
467      * Returns the type of the tile.
468      *
469      * @see Type
470      */
getType()471     public Type getType() {
472         boolean hasExternalAction = hasPendingIntent();
473         boolean hasAction = hasExternalAction || this instanceof ActivityTile;
474         boolean hasSwitch = hasSwitch();
475 
476         if (hasSwitch && hasAction) {
477             return Type.SWITCH_WITH_ACTION;
478         } else if (hasSwitch) {
479             return Type.SWITCH;
480         } else if (hasExternalAction) {
481             return Type.EXTERNAL_ACTION;
482         } else if (hasAction) {
483             return Type.ACTION;
484         } else {
485             return Type.GROUP;
486         }
487     }
488 
489     public static final Comparator<Tile> TILE_COMPARATOR =
490             (lhs, rhs) -> rhs.getOrder() - lhs.getOrder();
491 }
492