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