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