1 /* 2 * Copyright (C) 2015 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.settings.dashboard; 18 19 import static android.content.Intent.EXTRA_USER; 20 21 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_CHECKED_STATE; 22 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR; 23 import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE; 24 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; 25 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; 26 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON; 27 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; 28 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_ON_CHECKED_CHANGED; 29 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; 30 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; 31 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; 32 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; 33 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; 34 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; 35 36 import android.app.settings.SettingsEnums; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.IContentProvider; 40 import android.content.Intent; 41 import android.content.pm.PackageManager; 42 import android.graphics.drawable.Drawable; 43 import android.graphics.drawable.Icon; 44 import android.net.Uri; 45 import android.os.Bundle; 46 import android.os.UserHandle; 47 import android.provider.Settings; 48 import android.text.TextUtils; 49 import android.util.ArrayMap; 50 import android.util.Log; 51 import android.util.Pair; 52 import android.widget.Toast; 53 54 import androidx.annotation.VisibleForTesting; 55 import androidx.fragment.app.FragmentActivity; 56 import androidx.preference.Preference; 57 import androidx.preference.SwitchPreference; 58 59 import com.android.settings.R; 60 import com.android.settings.SettingsActivity; 61 import com.android.settings.Utils; 62 import com.android.settings.activityembedding.ActivityEmbeddingRulesController; 63 import com.android.settings.activityembedding.ActivityEmbeddingUtils; 64 import com.android.settings.dashboard.profileselector.ProfileSelectDialog; 65 import com.android.settings.homepage.TopLevelHighlightMixin; 66 import com.android.settings.homepage.TopLevelSettings; 67 import com.android.settings.overlay.FeatureFactory; 68 import com.android.settings.widget.PrimarySwitchPreference; 69 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 70 import com.android.settingslib.drawer.ActivityTile; 71 import com.android.settingslib.drawer.CategoryKey; 72 import com.android.settingslib.drawer.DashboardCategory; 73 import com.android.settingslib.drawer.Tile; 74 import com.android.settingslib.drawer.TileUtils; 75 import com.android.settingslib.utils.ThreadUtils; 76 import com.android.settingslib.widget.AdaptiveIcon; 77 78 import java.util.ArrayList; 79 import java.util.List; 80 import java.util.Map; 81 82 /** 83 * Impl for {@code DashboardFeatureProvider}. 84 */ 85 public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { 86 87 private static final String TAG = "DashboardFeatureImpl"; 88 private static final String DASHBOARD_TILE_PREF_KEY_PREFIX = "dashboard_tile_pref_"; 89 private static final String META_DATA_KEY_INTENT_ACTION = "com.android.settings.intent.action"; 90 91 protected final Context mContext; 92 93 private final MetricsFeatureProvider mMetricsFeatureProvider; 94 private final CategoryManager mCategoryManager; 95 private final PackageManager mPackageManager; 96 DashboardFeatureProviderImpl(Context context)97 public DashboardFeatureProviderImpl(Context context) { 98 mContext = context.getApplicationContext(); 99 mCategoryManager = CategoryManager.get(context); 100 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 101 mPackageManager = context.getPackageManager(); 102 } 103 104 @Override getTilesForCategory(String key)105 public DashboardCategory getTilesForCategory(String key) { 106 return mCategoryManager.getTilesByCategory(mContext, key); 107 } 108 109 @Override getAllCategories()110 public List<DashboardCategory> getAllCategories() { 111 return mCategoryManager.getCategories(mContext); 112 } 113 114 @Override getDashboardKeyForTile(Tile tile)115 public String getDashboardKeyForTile(Tile tile) { 116 if (tile == null) { 117 return null; 118 } 119 if (tile.hasKey()) { 120 return tile.getKey(mContext); 121 } 122 final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX); 123 final ComponentName component = tile.getIntent().getComponent(); 124 sb.append(component.getClassName()); 125 return sb.toString(); 126 } 127 128 @Override bindPreferenceToTileAndGetObservers(FragmentActivity activity, DashboardFragment fragment, boolean forceRoundedIcon, Preference pref, Tile tile, String key, int baseOrder)129 public List<DynamicDataObserver> bindPreferenceToTileAndGetObservers(FragmentActivity activity, 130 DashboardFragment fragment, boolean forceRoundedIcon, Preference pref, Tile tile, 131 String key, int baseOrder) { 132 if (pref == null) { 133 return null; 134 } 135 if (!TextUtils.isEmpty(key)) { 136 pref.setKey(key); 137 } else { 138 pref.setKey(getDashboardKeyForTile(tile)); 139 } 140 final List<DynamicDataObserver> outObservers = new ArrayList<>(); 141 DynamicDataObserver observer = bindTitleAndGetObserver(pref, tile); 142 if (observer != null) { 143 outObservers.add(observer); 144 } 145 observer = bindSummaryAndGetObserver(pref, tile); 146 if (observer != null) { 147 outObservers.add(observer); 148 } 149 observer = bindSwitchAndGetObserver(pref, tile); 150 if (observer != null) { 151 outObservers.add(observer); 152 } 153 bindIcon(pref, tile, forceRoundedIcon); 154 155 if (tile instanceof ActivityTile) { 156 final int sourceMetricsCategory = fragment.getMetricsCategory(); 157 final Bundle metadata = tile.getMetaData(); 158 String clsName = null; 159 String action = null; 160 if (metadata != null) { 161 clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); 162 action = metadata.getString(META_DATA_KEY_INTENT_ACTION); 163 } 164 if (!TextUtils.isEmpty(clsName)) { 165 pref.setFragment(clsName); 166 } else { 167 final Intent intent = new Intent(tile.getIntent()); 168 intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 169 sourceMetricsCategory); 170 if (action != null) { 171 intent.setAction(action); 172 } 173 // Register the rule for injected apps. 174 ActivityEmbeddingRulesController.registerTwoPanePairRuleForSettingsHome( 175 mContext, 176 new ComponentName(tile.getPackageName(), tile.getComponentName()), 177 action, 178 true /* clearTop */); 179 pref.setOnPreferenceClickListener(preference -> { 180 TopLevelHighlightMixin highlightMixin = null; 181 if (fragment instanceof TopLevelSettings 182 && ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext)) { 183 // Highlight the preference whenever it's clicked 184 final TopLevelSettings topLevelSettings = (TopLevelSettings) fragment; 185 topLevelSettings.setHighlightPreferenceKey(key); 186 highlightMixin = topLevelSettings.getHighlightMixin(); 187 } 188 launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory, 189 highlightMixin); 190 return true; 191 }); 192 } 193 } 194 195 if (tile.hasOrder()) { 196 final String skipOffsetPackageName = activity.getPackageName(); 197 final int order = tile.getOrder(); 198 boolean shouldSkipBaseOrderOffset = TextUtils.equals( 199 skipOffsetPackageName, tile.getIntent().getComponent().getPackageName()); 200 if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) { 201 pref.setOrder(order); 202 } else { 203 pref.setOrder(order + baseOrder); 204 } 205 } 206 return outObservers.isEmpty() ? null : outObservers; 207 } 208 209 @Override openTileIntent(FragmentActivity activity, Tile tile)210 public void openTileIntent(FragmentActivity activity, Tile tile) { 211 if (tile == null) { 212 Intent intent = new Intent(Settings.ACTION_SETTINGS).addFlags( 213 Intent.FLAG_ACTIVITY_CLEAR_TASK); 214 mContext.startActivity(intent); 215 return; 216 } 217 final Intent intent = new Intent(tile.getIntent()) 218 .putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 219 SettingsEnums.DASHBOARD_SUMMARY) 220 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 221 launchIntentOrSelectProfile(activity, tile, intent, SettingsEnums.DASHBOARD_SUMMARY, 222 /* highlightMixin= */ null); 223 } 224 createDynamicDataObserver(String method, Uri uri, Preference pref)225 private DynamicDataObserver createDynamicDataObserver(String method, Uri uri, Preference pref) { 226 return new DynamicDataObserver() { 227 @Override 228 public Uri getUri() { 229 return uri; 230 } 231 232 @Override 233 public void onDataChanged() { 234 switch (method) { 235 case METHOD_GET_DYNAMIC_TITLE: 236 refreshTitle(uri, pref); 237 break; 238 case METHOD_GET_DYNAMIC_SUMMARY: 239 refreshSummary(uri, pref); 240 break; 241 case METHOD_IS_CHECKED: 242 refreshSwitch(uri, pref); 243 break; 244 } 245 } 246 }; 247 } 248 249 private DynamicDataObserver bindTitleAndGetObserver(Preference preference, Tile tile) { 250 final CharSequence title = tile.getTitle(mContext.getApplicationContext()); 251 if (title != null) { 252 preference.setTitle(title); 253 return null; 254 } 255 if (tile.getMetaData() != null && tile.getMetaData().containsKey( 256 META_DATA_PREFERENCE_TITLE_URI)) { 257 // Set a placeholder title before starting to fetch real title, this is necessary 258 // to avoid preference height change. 259 preference.setTitle(R.string.summary_placeholder); 260 261 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI, 262 METHOD_GET_DYNAMIC_TITLE); 263 refreshTitle(uri, preference); 264 return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference); 265 } 266 return null; 267 } 268 269 private void refreshTitle(Uri uri, Preference preference) { 270 ThreadUtils.postOnBackgroundThread(() -> { 271 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 272 final String titleFromUri = TileUtils.getTextFromUri( 273 mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); 274 if (!TextUtils.equals(titleFromUri, preference.getTitle())) { 275 ThreadUtils.postOnMainThread(() -> preference.setTitle(titleFromUri)); 276 } 277 }); 278 } 279 280 private DynamicDataObserver bindSummaryAndGetObserver(Preference preference, Tile tile) { 281 final CharSequence summary = tile.getSummary(mContext); 282 if (summary != null) { 283 preference.setSummary(summary); 284 } else if (tile.getMetaData() != null 285 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { 286 // Set a placeholder summary before starting to fetch real summary, this is necessary 287 // to avoid preference height change. 288 preference.setSummary(R.string.summary_placeholder); 289 290 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI, 291 METHOD_GET_DYNAMIC_SUMMARY); 292 refreshSummary(uri, preference); 293 return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference); 294 } 295 return null; 296 } 297 298 private void refreshSummary(Uri uri, Preference preference) { 299 ThreadUtils.postOnBackgroundThread(() -> { 300 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 301 final String summaryFromUri = TileUtils.getTextFromUri( 302 mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); 303 if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { 304 ThreadUtils.postOnMainThread(() -> preference.setSummary(summaryFromUri)); 305 } 306 }); 307 } 308 309 private DynamicDataObserver bindSwitchAndGetObserver(Preference preference, Tile tile) { 310 if (!tile.hasSwitch()) { 311 return null; 312 } 313 314 final Uri onCheckedChangedUri = TileUtils.getCompleteUri(tile, 315 META_DATA_PREFERENCE_SWITCH_URI, METHOD_ON_CHECKED_CHANGED); 316 preference.setOnPreferenceChangeListener((pref, newValue) -> { 317 onCheckedChanged(onCheckedChangedUri, pref, (boolean) newValue); 318 return true; 319 }); 320 321 final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI, 322 METHOD_IS_CHECKED); 323 setSwitchEnabled(preference, false); 324 refreshSwitch(isCheckedUri, preference); 325 return createDynamicDataObserver(METHOD_IS_CHECKED, isCheckedUri, preference); 326 } 327 328 private void onCheckedChanged(Uri uri, Preference pref, boolean checked) { 329 setSwitchEnabled(pref, false); 330 ThreadUtils.postOnBackgroundThread(() -> { 331 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 332 final Bundle result = TileUtils.putBooleanToUriAndGetResult(mContext, uri, providerMap, 333 EXTRA_SWITCH_CHECKED_STATE, checked); 334 335 ThreadUtils.postOnMainThread(() -> { 336 setSwitchEnabled(pref, true); 337 final boolean error = result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR); 338 if (!error) { 339 return; 340 } 341 342 setSwitchChecked(pref, !checked); 343 final String errorMsg = result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE); 344 if (!TextUtils.isEmpty(errorMsg)) { 345 Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show(); 346 } 347 }); 348 }); 349 } 350 351 private void refreshSwitch(Uri uri, Preference preference) { 352 ThreadUtils.postOnBackgroundThread(() -> { 353 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 354 final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap, 355 EXTRA_SWITCH_CHECKED_STATE); 356 ThreadUtils.postOnMainThread(() -> { 357 setSwitchChecked(preference, checked); 358 setSwitchEnabled(preference, true); 359 }); 360 }); 361 } 362 363 private void setSwitchChecked(Preference pref, boolean checked) { 364 if (pref instanceof PrimarySwitchPreference) { 365 ((PrimarySwitchPreference) pref).setChecked(checked); 366 } else if (pref instanceof SwitchPreference) { 367 ((SwitchPreference) pref).setChecked(checked); 368 } 369 } 370 371 private void setSwitchEnabled(Preference pref, boolean enabled) { 372 if (pref instanceof PrimarySwitchPreference) { 373 ((PrimarySwitchPreference) pref).setSwitchEnabled(enabled); 374 } else { 375 pref.setEnabled(enabled); 376 } 377 } 378 379 @VisibleForTesting 380 void bindIcon(Preference preference, Tile tile, boolean forceRoundedIcon) { 381 // Icon provided by the content provider overrides any static icon. 382 if (tile.getMetaData() != null 383 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_ICON_URI)) { 384 // Set a transparent color before starting to fetch the real icon, this is necessary 385 // to avoid preference padding change. 386 setPreferenceIcon(preference, tile, forceRoundedIcon, mContext.getPackageName(), 387 Icon.createWithResource(mContext, android.R.color.transparent)); 388 389 ThreadUtils.postOnBackgroundThread(() -> { 390 final Intent intent = tile.getIntent(); 391 String packageName = null; 392 if (!TextUtils.isEmpty(intent.getPackage())) { 393 packageName = intent.getPackage(); 394 } else if (intent.getComponent() != null) { 395 packageName = intent.getComponent().getPackageName(); 396 } 397 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 398 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_ICON_URI, 399 METHOD_GET_PROVIDER_ICON); 400 final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri( 401 mContext, packageName, uri, providerMap); 402 if (iconInfo == null) { 403 Log.w(TAG, "Failed to get icon from uri " + uri); 404 return; 405 } 406 final Icon icon = Icon.createWithResource(iconInfo.first, iconInfo.second); 407 ThreadUtils.postOnMainThread(() -> { 408 setPreferenceIcon(preference, tile, forceRoundedIcon, iconInfo.first, icon); 409 }); 410 }); 411 return; 412 } 413 414 // Use preference context instead here when get icon from Tile, as we are using the context 415 // to get the style to tint the icon. Using mContext here won't get the correct style. 416 final Icon tileIcon = tile.getIcon(preference.getContext()); 417 if (tileIcon == null) { 418 return; 419 } 420 setPreferenceIcon(preference, tile, forceRoundedIcon, tile.getPackageName(), tileIcon); 421 } 422 423 private void setPreferenceIcon(Preference preference, Tile tile, boolean forceRoundedIcon, 424 String iconPackage, Icon icon) { 425 Drawable iconDrawable = icon.loadDrawable(preference.getContext()); 426 if (TextUtils.equals(tile.getCategory(), CategoryKey.CATEGORY_HOMEPAGE)) { 427 iconDrawable.setTint(Utils.getHomepageIconColor(preference.getContext())); 428 } else if (forceRoundedIcon && !TextUtils.equals(mContext.getPackageName(), iconPackage)) { 429 iconDrawable = new AdaptiveIcon(mContext, iconDrawable, 430 R.dimen.dashboard_tile_foreground_image_inset); 431 ((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile); 432 } 433 preference.setIcon(iconDrawable); 434 } 435 436 private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent, 437 int sourceMetricCategory, TopLevelHighlightMixin highlightMixin) { 438 if (!isIntentResolvable(intent)) { 439 Log.w(TAG, "Cannot resolve intent, skipping. " + intent); 440 return; 441 } 442 ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); 443 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 444 445 //TODO(b/201970810): Add test cases. 446 if (tile.isNewTask(mContext)) { 447 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 448 } 449 450 if (tile.userHandle == null || tile.isPrimaryProfileOnly()) { 451 activity.startActivity(intent); 452 } else if (tile.userHandle.size() == 1) { 453 activity.startActivityAsUser(intent, tile.userHandle.get(0)); 454 } else { 455 final UserHandle userHandle = intent.getParcelableExtra(EXTRA_USER); 456 if (userHandle != null && tile.userHandle.contains(userHandle)) { 457 activity.startActivityAsUser(intent, userHandle); 458 return; 459 } 460 461 final List<UserHandle> resolvableUsers = getResolvableUsers(intent, tile); 462 if (resolvableUsers.size() == 1) { 463 activity.startActivityAsUser(intent, resolvableUsers.get(0)); 464 return; 465 } 466 467 ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile, 468 sourceMetricCategory, /* onShowListener= */ highlightMixin, 469 /* onDismissListener= */ highlightMixin, 470 /* onCancelListener= */ highlightMixin); 471 } 472 } 473 474 private boolean isIntentResolvable(Intent intent) { 475 return mPackageManager.resolveActivity(intent, 0) != null; 476 } 477 478 private List<UserHandle> getResolvableUsers(Intent intent, Tile tile) { 479 final ArrayList<UserHandle> eligibleUsers = new ArrayList<>(); 480 for (UserHandle user : tile.userHandle) { 481 if (mPackageManager.resolveActivityAsUser(intent, 0, user.getIdentifier()) != null) { 482 eligibleUsers.add(user); 483 } 484 } 485 return eligibleUsers; 486 } 487 } 488