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