• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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