• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_PREFERENCE_ICON;
22 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
23 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
24 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
25 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
26 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
27 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
28 import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;
29 import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;
30 
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.ComponentInfo;
34 import android.content.pm.PackageManager;
35 import android.content.res.Resources;
36 import android.content.res.TypedArray;
37 import android.graphics.drawable.Icon;
38 import android.os.Bundle;
39 import android.os.Parcel;
40 import android.os.Parcelable;
41 import android.os.UserHandle;
42 import android.text.TextUtils;
43 import android.util.Log;
44 
45 import androidx.annotation.VisibleForTesting;
46 
47 import java.util.ArrayList;
48 import java.util.Comparator;
49 
50 /**
51  * Description of a single dashboard tile that the user can select.
52  */
53 public abstract class Tile implements Parcelable {
54 
55     private static final String TAG = "Tile";
56 
57     /**
58      * Optional list of user handles which the intent should be launched on.
59      */
60     public ArrayList<UserHandle> userHandle = new ArrayList<>();
61 
62     @VisibleForTesting
63     long mLastUpdateTime;
64     private final String mComponentPackage;
65     private final String mComponentName;
66     private final Intent mIntent;
67 
68     protected ComponentInfo mComponentInfo;
69     private CharSequence mSummaryOverride;
70     private Bundle mMetaData;
71     private String mCategory;
72 
Tile(ComponentInfo info, String category)73     public Tile(ComponentInfo info, String category) {
74         mComponentInfo = info;
75         mComponentPackage = mComponentInfo.packageName;
76         mComponentName = mComponentInfo.name;
77         mCategory = category;
78         mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
79     }
80 
Tile(Parcel in)81     Tile(Parcel in) {
82         final boolean isProviderTile = in.readBoolean();
83         mComponentPackage = in.readString();
84         mComponentName = in.readString();
85         mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
86         final int number = in.readInt();
87         for (int i = 0; i < number; i++) {
88             userHandle.add(UserHandle.CREATOR.createFromParcel(in));
89         }
90         mCategory = in.readString();
91         mMetaData = in.readBundle();
92     }
93 
94     @Override
describeContents()95     public int describeContents() {
96         return 0;
97     }
98 
99     @Override
writeToParcel(Parcel dest, int flags)100     public void writeToParcel(Parcel dest, int flags) {
101         dest.writeBoolean(this instanceof ProviderTile);
102         dest.writeString(mComponentPackage);
103         dest.writeString(mComponentName);
104         final int size = userHandle.size();
105         dest.writeInt(size);
106         for (int i = 0; i < size; i++) {
107             userHandle.get(i).writeToParcel(dest, flags);
108         }
109         dest.writeString(mCategory);
110         dest.writeBundle(mMetaData);
111     }
112 
113     /**
114      * Unique ID of the tile
115      */
getId()116     public abstract int getId();
117 
118     /**
119      * Human-readable description of the tile
120      */
getDescription()121     public abstract String getDescription();
122 
getComponentInfo(Context context)123     protected abstract ComponentInfo getComponentInfo(Context context);
124 
getComponentLabel(Context context)125     protected abstract CharSequence getComponentLabel(Context context);
126 
getComponentIcon(ComponentInfo info)127     protected abstract int getComponentIcon(ComponentInfo info);
128 
getPackageName()129     public String getPackageName() {
130         return mComponentPackage;
131     }
132 
getComponentName()133     public String getComponentName() {
134         return mComponentName;
135     }
136 
137     /**
138      * Intent to launch when the preference is selected.
139      */
getIntent()140     public Intent getIntent() {
141         return mIntent;
142     }
143 
144     /**
145      * Category in which the tile should be placed.
146      */
getCategory()147     public String getCategory() {
148         return mCategory;
149     }
150 
setCategory(String newCategoryKey)151     public void setCategory(String newCategoryKey) {
152         mCategory = newCategoryKey;
153     }
154 
155     /**
156      * Priority of this tile, used for display ordering.
157      */
getOrder()158     public int getOrder() {
159         if (hasOrder()) {
160             return mMetaData.getInt(META_DATA_KEY_ORDER);
161         } else {
162             return 0;
163         }
164     }
165 
166     /**
167      * Check whether tile has order.
168      */
hasOrder()169     public boolean hasOrder() {
170         return mMetaData.containsKey(META_DATA_KEY_ORDER)
171                 && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
172     }
173 
174     /**
175      * Check whether tile has a switch.
176      */
hasSwitch()177     public boolean hasSwitch() {
178         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SWITCH_URI);
179     }
180 
181     /**
182      * Title of the tile that is shown to the user.
183      */
getTitle(Context context)184     public CharSequence getTitle(Context context) {
185         CharSequence title = null;
186         ensureMetadataNotStale(context);
187         final PackageManager packageManager = context.getPackageManager();
188         if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
189             if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
190                 // If has as uri to provide dynamic title, skip loading here. UI will later load
191                 // at tile binding time.
192                 return null;
193             }
194             if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
195                 try {
196                     final Resources res =
197                             packageManager.getResourcesForApplication(mComponentPackage);
198                     title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE));
199                 } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
200                     Log.w(TAG, "Couldn't find info", e);
201                 }
202             } else {
203                 title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
204             }
205         }
206         // Set the preference title by the component if no meta-data is found
207         if (title == null) {
208             title = getComponentLabel(context);
209         }
210         return title;
211     }
212 
213     /**
214      * Overrides the summary. This can happen when injected tile wants to provide dynamic summary.
215      */
overrideSummary(CharSequence summaryOverride)216     public void overrideSummary(CharSequence summaryOverride) {
217         mSummaryOverride = summaryOverride;
218     }
219 
220     /**
221      * Optional summary describing what this tile controls.
222      */
getSummary(Context context)223     public CharSequence getSummary(Context context) {
224         if (mSummaryOverride != null) {
225             return mSummaryOverride;
226         }
227         ensureMetadataNotStale(context);
228         CharSequence summary = null;
229         final PackageManager packageManager = context.getPackageManager();
230         if (mMetaData != null) {
231             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
232                 // If has as uri to provide dynamic summary, skip loading here. UI will later load
233                 // at tile binding time.
234                 return null;
235             }
236             if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
237                 if (mMetaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
238                     try {
239                         final Resources res =
240                                 packageManager.getResourcesForApplication(mComponentPackage);
241                         summary = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_SUMMARY));
242                     } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
243                         Log.d(TAG, "Couldn't find info", e);
244                     }
245                 } else {
246                     summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY);
247                 }
248             }
249         }
250         return summary;
251     }
252 
setMetaData(Bundle metaData)253     public void setMetaData(Bundle metaData) {
254         mMetaData = metaData;
255     }
256 
257     /**
258      * The metaData from the activity that defines this tile.
259      */
getMetaData()260     public Bundle getMetaData() {
261         return mMetaData;
262     }
263 
264     /**
265      * Optional key to use for this tile.
266      */
getKey(Context context)267     public String getKey(Context context) {
268         if (!hasKey()) {
269             return null;
270         }
271         ensureMetadataNotStale(context);
272         if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
273             return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
274         } else {
275             return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT);
276         }
277     }
278 
279     /**
280      * Check whether title has key.
281      */
hasKey()282     public boolean hasKey() {
283         return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_KEYHINT);
284     }
285 
286     /**
287      * Optional icon to show for this tile.
288      *
289      * @attr ref android.R.styleable#PreferenceHeader_icon
290      */
getIcon(Context context)291     public Icon getIcon(Context context) {
292         if (context == null || mMetaData == null) {
293             return null;
294         }
295         ensureMetadataNotStale(context);
296         final ComponentInfo componentInfo = getComponentInfo(context);
297         if (componentInfo == null) {
298             Log.w(TAG, "Cannot find ComponentInfo for " + getDescription());
299             return null;
300         }
301 
302         int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON);
303         // Set the icon. Skip the transparent color for backward compatibility since Android S.
304         if (iconResId != 0 && iconResId != android.R.color.transparent) {
305             final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);
306             if (isIconTintable(context)) {
307                 final TypedArray a = context.obtainStyledAttributes(new int[]{
308                         android.R.attr.colorControlNormal});
309                 final int tintColor = a.getColor(0, 0);
310                 a.recycle();
311                 icon.setTint(tintColor);
312             }
313             return icon;
314         } else {
315             return null;
316         }
317     }
318 
319     /**
320      * Whether the icon can be tinted. This is true when icon needs to be monochrome (single-color)
321      */
isIconTintable(Context context)322     public boolean isIconTintable(Context context) {
323         ensureMetadataNotStale(context);
324         if (mMetaData != null
325                 && mMetaData.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) {
326             return mMetaData.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE);
327         }
328         return false;
329     }
330 
331     /**
332      * Ensures metadata is not stale for this tile.
333      */
ensureMetadataNotStale(Context context)334     private void ensureMetadataNotStale(Context context) {
335         final PackageManager pm = context.getApplicationContext().getPackageManager();
336 
337         try {
338             final long lastUpdateTime = pm.getPackageInfo(mComponentPackage,
339                     PackageManager.GET_META_DATA).lastUpdateTime;
340             if (lastUpdateTime == mLastUpdateTime) {
341                 // All good. Do nothing
342                 return;
343             }
344             // App has been updated since we load metadata last time. Reload metadata.
345             mComponentInfo = null;
346             getComponentInfo(context);
347             mLastUpdateTime = lastUpdateTime;
348         } catch (PackageManager.NameNotFoundException e) {
349             Log.d(TAG, "Can't find package, probably uninstalled.");
350         }
351     }
352 
353     public static final Creator<Tile> CREATOR = new Creator<Tile>() {
354         public Tile createFromParcel(Parcel source) {
355             final boolean isProviderTile = source.readBoolean();
356             // reset the Parcel pointer before delegating to the real constructor.
357             source.setDataPosition(0);
358             return isProviderTile ? new ProviderTile(source) : new ActivityTile(source);
359         }
360 
361         public Tile[] newArray(int size) {
362             return new Tile[size];
363         }
364     };
365 
366     /**
367      * Check whether tile only has primary profile.
368      */
isPrimaryProfileOnly()369     public boolean isPrimaryProfileOnly() {
370         return isPrimaryProfileOnly(mMetaData);
371     }
372 
isPrimaryProfileOnly(Bundle metaData)373     static boolean isPrimaryProfileOnly(Bundle metaData) {
374         String profile = metaData != null
375                 ? metaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL;
376         profile = (profile != null ? profile : PROFILE_ALL);
377         return TextUtils.equals(profile, PROFILE_PRIMARY);
378     }
379 
380     public static final Comparator<Tile> TILE_COMPARATOR =
381             (lhs, rhs) -> rhs.getOrder() - lhs.getOrder();
382 }
383