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.dashboard.profileselector.ProfileSelectDialog; 62 import com.android.settings.overlay.FeatureFactory; 63 import com.android.settings.widget.MasterSwitchPreference; 64 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 65 import com.android.settingslib.drawer.ActivityTile; 66 import com.android.settingslib.drawer.DashboardCategory; 67 import com.android.settingslib.drawer.Tile; 68 import com.android.settingslib.drawer.TileUtils; 69 import com.android.settingslib.utils.ThreadUtils; 70 import com.android.settingslib.widget.AdaptiveIcon; 71 72 import java.util.ArrayList; 73 import java.util.List; 74 import java.util.Map; 75 76 /** 77 * Impl for {@code DashboardFeatureProvider}. 78 */ 79 public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { 80 81 private static final String TAG = "DashboardFeatureImpl"; 82 private static final String DASHBOARD_TILE_PREF_KEY_PREFIX = "dashboard_tile_pref_"; 83 private static final String META_DATA_KEY_INTENT_ACTION = "com.android.settings.intent.action"; 84 85 protected final Context mContext; 86 87 private final MetricsFeatureProvider mMetricsFeatureProvider; 88 private final CategoryManager mCategoryManager; 89 private final PackageManager mPackageManager; 90 DashboardFeatureProviderImpl(Context context)91 public DashboardFeatureProviderImpl(Context context) { 92 mContext = context.getApplicationContext(); 93 mCategoryManager = CategoryManager.get(context); 94 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 95 mPackageManager = context.getPackageManager(); 96 } 97 98 @Override getTilesForCategory(String key)99 public DashboardCategory getTilesForCategory(String key) { 100 return mCategoryManager.getTilesByCategory(mContext, key); 101 } 102 103 @Override getAllCategories()104 public List<DashboardCategory> getAllCategories() { 105 return mCategoryManager.getCategories(mContext); 106 } 107 108 @Override getDashboardKeyForTile(Tile tile)109 public String getDashboardKeyForTile(Tile tile) { 110 if (tile == null) { 111 return null; 112 } 113 if (tile.hasKey()) { 114 return tile.getKey(mContext); 115 } 116 final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX); 117 final ComponentName component = tile.getIntent().getComponent(); 118 sb.append(component.getClassName()); 119 return sb.toString(); 120 } 121 122 @Override bindPreferenceToTileAndGetObservers(FragmentActivity activity, boolean forceRoundedIcon, int sourceMetricsCategory, Preference pref, Tile tile, String key, int baseOrder)123 public List<DynamicDataObserver> bindPreferenceToTileAndGetObservers(FragmentActivity activity, 124 boolean forceRoundedIcon, int sourceMetricsCategory, Preference pref, Tile tile, 125 String key, int baseOrder) { 126 if (pref == null) { 127 return null; 128 } 129 if (!TextUtils.isEmpty(key)) { 130 pref.setKey(key); 131 } else { 132 pref.setKey(getDashboardKeyForTile(tile)); 133 } 134 final List<DynamicDataObserver> outObservers = new ArrayList<>(); 135 DynamicDataObserver observer = bindTitleAndGetObserver(pref, tile); 136 if (observer != null) { 137 outObservers.add(observer); 138 } 139 observer = bindSummaryAndGetObserver(pref, tile); 140 if (observer != null) { 141 outObservers.add(observer); 142 } 143 observer = bindSwitchAndGetObserver(pref, tile); 144 if (observer != null) { 145 outObservers.add(observer); 146 } 147 bindIcon(pref, tile, forceRoundedIcon); 148 149 if (tile instanceof ActivityTile) { 150 final Bundle metadata = tile.getMetaData(); 151 String clsName = null; 152 String action = null; 153 if (metadata != null) { 154 clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); 155 action = metadata.getString(META_DATA_KEY_INTENT_ACTION); 156 } 157 if (!TextUtils.isEmpty(clsName)) { 158 pref.setFragment(clsName); 159 } else { 160 final Intent intent = new Intent(tile.getIntent()); 161 intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 162 sourceMetricsCategory); 163 if (action != null) { 164 intent.setAction(action); 165 } 166 pref.setOnPreferenceClickListener(preference -> { 167 launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory); 168 return true; 169 }); 170 } 171 } 172 173 if (tile.hasOrder()) { 174 final String skipOffsetPackageName = activity.getPackageName(); 175 final int order = tile.getOrder(); 176 boolean shouldSkipBaseOrderOffset = TextUtils.equals( 177 skipOffsetPackageName, tile.getIntent().getComponent().getPackageName()); 178 if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) { 179 pref.setOrder(order); 180 } else { 181 pref.setOrder(order + baseOrder); 182 } 183 } 184 return outObservers.isEmpty() ? null : outObservers; 185 } 186 187 @Override openTileIntent(FragmentActivity activity, Tile tile)188 public void openTileIntent(FragmentActivity activity, Tile tile) { 189 if (tile == null) { 190 Intent intent = new Intent(Settings.ACTION_SETTINGS).addFlags( 191 Intent.FLAG_ACTIVITY_CLEAR_TASK); 192 mContext.startActivity(intent); 193 return; 194 } 195 final Intent intent = new Intent(tile.getIntent()) 196 .putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 197 SettingsEnums.DASHBOARD_SUMMARY) 198 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 199 launchIntentOrSelectProfile(activity, tile, intent, SettingsEnums.DASHBOARD_SUMMARY); 200 } 201 createDynamicDataObserver(String method, Uri uri, Preference pref)202 private DynamicDataObserver createDynamicDataObserver(String method, Uri uri, Preference pref) { 203 return new DynamicDataObserver() { 204 @Override 205 public Uri getUri() { 206 return uri; 207 } 208 209 @Override 210 public void onDataChanged() { 211 switch (method) { 212 case METHOD_GET_DYNAMIC_TITLE: 213 refreshTitle(uri, pref); 214 break; 215 case METHOD_GET_DYNAMIC_SUMMARY: 216 refreshSummary(uri, pref); 217 break; 218 case METHOD_IS_CHECKED: 219 refreshSwitch(uri, pref); 220 break; 221 } 222 } 223 }; 224 } 225 226 private DynamicDataObserver bindTitleAndGetObserver(Preference preference, Tile tile) { 227 final CharSequence title = tile.getTitle(mContext.getApplicationContext()); 228 if (title != null) { 229 preference.setTitle(title); 230 return null; 231 } 232 if (tile.getMetaData() != null && tile.getMetaData().containsKey( 233 META_DATA_PREFERENCE_TITLE_URI)) { 234 // Set a placeholder title before starting to fetch real title, this is necessary 235 // to avoid preference height change. 236 preference.setTitle(R.string.summary_placeholder); 237 238 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI, 239 METHOD_GET_DYNAMIC_TITLE); 240 refreshTitle(uri, preference); 241 return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference); 242 } 243 return null; 244 } 245 246 private void refreshTitle(Uri uri, Preference preference) { 247 ThreadUtils.postOnBackgroundThread(() -> { 248 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 249 final String titleFromUri = TileUtils.getTextFromUri( 250 mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); 251 if (!TextUtils.equals(titleFromUri, preference.getTitle())) { 252 ThreadUtils.postOnMainThread(() -> preference.setTitle(titleFromUri)); 253 } 254 }); 255 } 256 257 private DynamicDataObserver bindSummaryAndGetObserver(Preference preference, Tile tile) { 258 final CharSequence summary = tile.getSummary(mContext); 259 if (summary != null) { 260 preference.setSummary(summary); 261 } else if (tile.getMetaData() != null 262 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { 263 // Set a placeholder summary before starting to fetch real summary, this is necessary 264 // to avoid preference height change. 265 preference.setSummary(R.string.summary_placeholder); 266 267 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI, 268 METHOD_GET_DYNAMIC_SUMMARY); 269 refreshSummary(uri, preference); 270 return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference); 271 } else { 272 preference.setSummary(R.string.summary_placeholder); 273 } 274 return null; 275 } 276 277 private void refreshSummary(Uri uri, Preference preference) { 278 ThreadUtils.postOnBackgroundThread(() -> { 279 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 280 final String summaryFromUri = TileUtils.getTextFromUri( 281 mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); 282 if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { 283 ThreadUtils.postOnMainThread(() -> preference.setSummary(summaryFromUri)); 284 } 285 }); 286 } 287 288 private DynamicDataObserver bindSwitchAndGetObserver(Preference preference, Tile tile) { 289 if (!tile.hasSwitch()) { 290 return null; 291 } 292 293 final Uri onCheckedChangedUri = TileUtils.getCompleteUri(tile, 294 META_DATA_PREFERENCE_SWITCH_URI, METHOD_ON_CHECKED_CHANGED); 295 preference.setOnPreferenceChangeListener((pref, newValue) -> { 296 onCheckedChanged(onCheckedChangedUri, pref, (boolean) newValue); 297 return true; 298 }); 299 300 final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI, 301 METHOD_IS_CHECKED); 302 setSwitchEnabled(preference, false); 303 refreshSwitch(isCheckedUri, preference); 304 return createDynamicDataObserver(METHOD_IS_CHECKED, isCheckedUri, preference); 305 } 306 307 private void onCheckedChanged(Uri uri, Preference pref, boolean checked) { 308 setSwitchEnabled(pref, false); 309 ThreadUtils.postOnBackgroundThread(() -> { 310 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 311 final Bundle result = TileUtils.putBooleanToUriAndGetResult(mContext, uri, providerMap, 312 EXTRA_SWITCH_CHECKED_STATE, checked); 313 314 ThreadUtils.postOnMainThread(() -> { 315 setSwitchEnabled(pref, true); 316 final boolean error = result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR); 317 if (!error) { 318 return; 319 } 320 321 setSwitchChecked(pref, !checked); 322 final String errorMsg = result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE); 323 if (!TextUtils.isEmpty(errorMsg)) { 324 Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show(); 325 } 326 }); 327 }); 328 } 329 330 private void refreshSwitch(Uri uri, Preference preference) { 331 ThreadUtils.postOnBackgroundThread(() -> { 332 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 333 final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap, 334 EXTRA_SWITCH_CHECKED_STATE); 335 ThreadUtils.postOnMainThread(() -> { 336 setSwitchChecked(preference, checked); 337 setSwitchEnabled(preference, true); 338 }); 339 }); 340 } 341 342 private void setSwitchChecked(Preference pref, boolean checked) { 343 if (pref instanceof MasterSwitchPreference) { 344 ((MasterSwitchPreference) pref).setChecked(checked); 345 } else if (pref instanceof SwitchPreference) { 346 ((SwitchPreference) pref).setChecked(checked); 347 } 348 } 349 350 private void setSwitchEnabled(Preference pref, boolean enabled) { 351 if (pref instanceof MasterSwitchPreference) { 352 ((MasterSwitchPreference) pref).setSwitchEnabled(enabled); 353 } else { 354 pref.setEnabled(enabled); 355 } 356 } 357 358 @VisibleForTesting 359 void bindIcon(Preference preference, Tile tile, boolean forceRoundedIcon) { 360 // Use preference context instead here when get icon from Tile, as we are using the context 361 // to get the style to tint the icon. Using mContext here won't get the correct style. 362 final Icon tileIcon = tile.getIcon(preference.getContext()); 363 if (tileIcon != null) { 364 Drawable iconDrawable = tileIcon.loadDrawable(preference.getContext()); 365 if (forceRoundedIcon 366 && !TextUtils.equals(mContext.getPackageName(), tile.getPackageName())) { 367 iconDrawable = new AdaptiveIcon(mContext, iconDrawable); 368 ((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile); 369 } 370 preference.setIcon(iconDrawable); 371 } else if (tile.getMetaData() != null 372 && tile.getMetaData().containsKey(META_DATA_PREFERENCE_ICON_URI)) { 373 ThreadUtils.postOnBackgroundThread(() -> { 374 final Intent intent = tile.getIntent(); 375 String packageName = null; 376 if (!TextUtils.isEmpty(intent.getPackage())) { 377 packageName = intent.getPackage(); 378 } else if (intent.getComponent() != null) { 379 packageName = intent.getComponent().getPackageName(); 380 } 381 final Map<String, IContentProvider> providerMap = new ArrayMap<>(); 382 final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_ICON_URI, 383 METHOD_GET_PROVIDER_ICON); 384 final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri( 385 mContext, packageName, uri, providerMap); 386 if (iconInfo == null) { 387 Log.w(TAG, "Failed to get icon from uri " + uri); 388 return; 389 } 390 final Icon icon = Icon.createWithResource(iconInfo.first, iconInfo.second); 391 ThreadUtils.postOnMainThread(() -> 392 preference.setIcon(icon.loadDrawable(preference.getContext())) 393 ); 394 }); 395 } 396 } 397 398 private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent, 399 int sourceMetricCategory) { 400 if (!isIntentResolvable(intent)) { 401 Log.w(TAG, "Cannot resolve intent, skipping. " + intent); 402 return; 403 } 404 ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); 405 406 if (tile.userHandle == null || tile.isPrimaryProfileOnly()) { 407 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 408 activity.startActivityForResult(intent, 0); 409 } else if (tile.userHandle.size() == 1) { 410 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 411 activity.startActivityForResultAsUser(intent, 0, tile.userHandle.get(0)); 412 } else { 413 mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory); 414 final UserHandle userHandle = intent.getParcelableExtra(EXTRA_USER); 415 if (userHandle != null && tile.userHandle.contains(userHandle)) { 416 activity.startActivityForResultAsUser(intent, 0, userHandle); 417 } else { 418 ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile, 419 sourceMetricCategory); 420 } 421 } 422 } 423 424 private boolean isIntentResolvable(Intent intent) { 425 return mPackageManager.resolveActivity(intent, 0) != null; 426 } 427 } 428