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