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