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