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