• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 package com.android.intentresolver;
17 
18 import android.annotation.IntDef;
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.AppGlobals;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.IPackageManager;
27 import android.os.Trace;
28 import android.os.UserHandle;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.Button;
32 import android.widget.TextView;
33 
34 import androidx.viewpager.widget.PagerAdapter;
35 import androidx.viewpager.widget.ViewPager;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Objects;
42 import java.util.Set;
43 import java.util.function.Supplier;
44 
45 /**
46  * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for
47  * intent resolution (including share sheet).
48  */
49 public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
50 
51     private static final String TAG = "AbstractMultiProfilePagerAdapter";
52     static final int PROFILE_PERSONAL = 0;
53     static final int PROFILE_WORK = 1;
54 
55     @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
56     @interface Profile {}
57 
58     private final Context mContext;
59     private int mCurrentPage;
60     private OnProfileSelectedListener mOnProfileSelectedListener;
61 
62     private Set<Integer> mLoadedPages;
63     private final EmptyStateProvider mEmptyStateProvider;
64     private final UserHandle mWorkProfileUserHandle;
65     private final Supplier<Boolean> mWorkProfileQuietModeChecker;  // True when work is quiet.
66 
AbstractMultiProfilePagerAdapter( Context context, int currentPage, EmptyStateProvider emptyStateProvider, Supplier<Boolean> workProfileQuietModeChecker, UserHandle workProfileUserHandle)67     AbstractMultiProfilePagerAdapter(
68             Context context,
69             int currentPage,
70             EmptyStateProvider emptyStateProvider,
71             Supplier<Boolean> workProfileQuietModeChecker,
72             UserHandle workProfileUserHandle) {
73         mContext = Objects.requireNonNull(context);
74         mCurrentPage = currentPage;
75         mLoadedPages = new HashSet<>();
76         mWorkProfileUserHandle = workProfileUserHandle;
77         mEmptyStateProvider = emptyStateProvider;
78         mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
79     }
80 
setOnProfileSelectedListener(OnProfileSelectedListener listener)81     void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
82         mOnProfileSelectedListener = listener;
83     }
84 
getContext()85     Context getContext() {
86         return mContext;
87     }
88 
89     /**
90      * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets
91      * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
92      * page and rebuilds the list.
93      */
setupViewPager(ViewPager viewPager)94     void setupViewPager(ViewPager viewPager) {
95         viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
96             @Override
97             public void onPageSelected(int position) {
98                 mCurrentPage = position;
99                 if (!mLoadedPages.contains(position)) {
100                     rebuildActiveTab(true);
101                     mLoadedPages.add(position);
102                 }
103                 if (mOnProfileSelectedListener != null) {
104                     mOnProfileSelectedListener.onProfileSelected(position);
105                 }
106             }
107 
108             @Override
109             public void onPageScrollStateChanged(int state) {
110                 if (mOnProfileSelectedListener != null) {
111                     mOnProfileSelectedListener.onProfilePageStateChanged(state);
112                 }
113             }
114         });
115         viewPager.setAdapter(this);
116         viewPager.setCurrentItem(mCurrentPage);
117         mLoadedPages.add(mCurrentPage);
118     }
119 
clearInactiveProfileCache()120     void clearInactiveProfileCache() {
121         if (mLoadedPages.size() == 1) {
122             return;
123         }
124         mLoadedPages.remove(1 - mCurrentPage);
125     }
126 
127     @Override
instantiateItem(ViewGroup container, int position)128     public ViewGroup instantiateItem(ViewGroup container, int position) {
129         final ProfileDescriptor profileDescriptor = getItem(position);
130         container.addView(profileDescriptor.rootView);
131         return profileDescriptor.rootView;
132     }
133 
134     @Override
destroyItem(ViewGroup container, int position, Object view)135     public void destroyItem(ViewGroup container, int position, Object view) {
136         container.removeView((View) view);
137     }
138 
139     @Override
getCount()140     public int getCount() {
141         return getItemCount();
142     }
143 
getCurrentPage()144     protected int getCurrentPage() {
145         return mCurrentPage;
146     }
147 
148     @VisibleForTesting
getCurrentUserHandle()149     public UserHandle getCurrentUserHandle() {
150         return getActiveListAdapter().getUserHandle();
151     }
152 
153     @Override
isViewFromObject(View view, Object object)154     public boolean isViewFromObject(View view, Object object) {
155         return view == object;
156     }
157 
158     @Override
getPageTitle(int position)159     public CharSequence getPageTitle(int position) {
160         return null;
161     }
162 
163     /**
164      * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>.
165      * <ul>
166      * <li>For a device with only one user, <code>pageIndex</code> value of
167      * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
168      * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
169      * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
170      * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
171      * </ul>
172      */
getItem(int pageIndex)173     abstract ProfileDescriptor getItem(int pageIndex);
174 
getEmptyStateView(int pageIndex)175     protected ViewGroup getEmptyStateView(int pageIndex) {
176         return getItem(pageIndex).getEmptyStateView();
177     }
178 
179     /**
180      * Returns the number of {@link ProfileDescriptor} objects.
181      * <p>For a normal consumer device with only one user returns <code>1</code>.
182      * <p>For a device with a work profile returns <code>2</code>.
183      */
getItemCount()184     abstract int getItemCount();
185 
186     /**
187      * Performs view-related initialization procedures for the adapter specified
188      * by <code>pageIndex</code>.
189      */
setupListAdapter(int pageIndex)190     abstract void setupListAdapter(int pageIndex);
191 
192     /**
193      * Returns the adapter of the list view for the relevant page specified by
194      * <code>pageIndex</code>.
195      * <p>This method is meant to be implemented with an implementation-specific return type
196      * depending on the adapter type.
197      */
198     @VisibleForTesting
getAdapterForIndex(int pageIndex)199     public abstract Object getAdapterForIndex(int pageIndex);
200 
201     /**
202      * Returns the {@link ResolverListAdapter} instance of the profile that represents
203      * <code>userHandle</code>. If there is no such adapter for the specified
204      * <code>userHandle</code>, returns {@code null}.
205      * <p>For example, if there is a work profile on the device with user id 10, calling this method
206      * with <code>UserHandle.of(10)</code> returns the work profile {@link ResolverListAdapter}.
207      */
208     @Nullable
getListAdapterForUserHandle(UserHandle userHandle)209     abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle);
210 
211     /**
212      * Returns the {@link ResolverListAdapter} instance of the profile that is currently visible
213      * to the user.
214      * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
215      * the work profile {@link ResolverListAdapter}.
216      * @see #getInactiveListAdapter()
217      */
218     @VisibleForTesting
getActiveListAdapter()219     public abstract ResolverListAdapter getActiveListAdapter();
220 
221     /**
222      * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance
223      * of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
224      * {@code null}.
225      * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
226      * the personal profile {@link ResolverListAdapter}.
227      * @see #getActiveListAdapter()
228      */
229     @VisibleForTesting
getInactiveListAdapter()230     public abstract @Nullable ResolverListAdapter getInactiveListAdapter();
231 
getPersonalListAdapter()232     public abstract ResolverListAdapter getPersonalListAdapter();
233 
getWorkListAdapter()234     public abstract @Nullable ResolverListAdapter getWorkListAdapter();
235 
getCurrentRootAdapter()236     abstract Object getCurrentRootAdapter();
237 
getActiveAdapterView()238     abstract ViewGroup getActiveAdapterView();
239 
getInactiveAdapterView()240     abstract @Nullable ViewGroup getInactiveAdapterView();
241 
242     /**
243      * Rebuilds the tab that is currently visible to the user.
244      * <p>Returns {@code true} if rebuild has completed.
245      */
rebuildActiveTab(boolean doPostProcessing)246     boolean rebuildActiveTab(boolean doPostProcessing) {
247         Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
248         boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
249         Trace.endSection();
250         return result;
251     }
252 
253     /**
254      * Rebuilds the tab that is not currently visible to the user, if such one exists.
255      * <p>Returns {@code true} if rebuild has completed.
256      */
rebuildInactiveTab(boolean doPostProcessing)257     boolean rebuildInactiveTab(boolean doPostProcessing) {
258         Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
259         if (getItemCount() == 1) {
260             Trace.endSection();
261             return false;
262         }
263         boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing);
264         Trace.endSection();
265         return result;
266     }
267 
userHandleToPageIndex(UserHandle userHandle)268     private int userHandleToPageIndex(UserHandle userHandle) {
269         if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
270             return PROFILE_PERSONAL;
271         } else {
272             return PROFILE_WORK;
273         }
274     }
275 
rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing)276     private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
277         if (shouldSkipRebuild(activeListAdapter)) {
278             activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
279             return false;
280         }
281         return activeListAdapter.rebuildList(doPostProcessing);
282     }
283 
shouldSkipRebuild(ResolverListAdapter activeListAdapter)284     private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) {
285         EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
286         return emptyState != null && emptyState.shouldSkipDataRebuild();
287     }
288 
289     /**
290      * The empty state screens are shown according to their priority:
291      * <ol>
292      * <li>(highest priority) cross-profile disabled by policy (handled in
293      * {@link #rebuildTab(ResolverListAdapter, boolean)})</li>
294      * <li>no apps available</li>
295      * <li>(least priority) work is off</li>
296      * </ol>
297      *
298      * The intention is to prevent the user from having to turn
299      * the work profile on if there will not be any apps resolved
300      * anyway.
301      */
showEmptyResolverListEmptyState(ResolverListAdapter listAdapter)302     void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
303         final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
304 
305         if (emptyState == null) {
306             return;
307         }
308 
309         emptyState.onEmptyStateShown();
310 
311         View.OnClickListener clickListener = null;
312 
313         if (emptyState.getButtonClickListener() != null) {
314             clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
315                 ProfileDescriptor descriptor = getItem(
316                         userHandleToPageIndex(listAdapter.getUserHandle()));
317                 AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
318             });
319         }
320 
321         showEmptyState(listAdapter, emptyState, clickListener);
322     }
323 
324     /**
325      * Class to get user id of the current process
326      */
327     public static class MyUserIdProvider {
328         /**
329          * @return user id of the current process
330          */
getMyUserId()331         public int getMyUserId() {
332             return UserHandle.myUserId();
333         }
334     }
335 
336     /**
337      * Utility class to check if there are cross profile intents, it is in a separate class so
338      * it could be mocked in tests
339      */
340     public static class CrossProfileIntentsChecker {
341 
342         private final ContentResolver mContentResolver;
343 
CrossProfileIntentsChecker(@onNull ContentResolver contentResolver)344         public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
345             mContentResolver = contentResolver;
346         }
347 
348         /**
349          * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
350          * from {@code source} (user id) to {@code target} (user id).
351          */
hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source, @UserIdInt int target)352         public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source,
353                 @UserIdInt int target) {
354             IPackageManager packageManager = AppGlobals.getPackageManager();
355 
356             return intents.stream().anyMatch(intent ->
357                     null != IntentForwarderActivity.canForward(intent, source, target,
358                             packageManager, mContentResolver));
359         }
360     }
361 
showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState, View.OnClickListener buttonOnClick)362     protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState,
363             View.OnClickListener buttonOnClick) {
364         ProfileDescriptor descriptor = getItem(
365                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
366         descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
367         ViewGroup emptyStateView = descriptor.getEmptyStateView();
368         resetViewVisibilitiesForEmptyState(emptyStateView);
369         emptyStateView.setVisibility(View.VISIBLE);
370 
371         View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container);
372         setupContainerPadding(container);
373 
374         TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title);
375         String title = emptyState.getTitle();
376         if (title != null) {
377             titleView.setVisibility(View.VISIBLE);
378             titleView.setText(title);
379         } else {
380             titleView.setVisibility(View.GONE);
381         }
382 
383         TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
384         String subtitle = emptyState.getSubtitle();
385         if (subtitle != null) {
386             subtitleView.setVisibility(View.VISIBLE);
387             subtitleView.setText(subtitle);
388         } else {
389             subtitleView.setVisibility(View.GONE);
390         }
391 
392         View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
393         defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
394 
395         Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
396         button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
397         button.setOnClickListener(buttonOnClick);
398 
399         activeListAdapter.markTabLoaded();
400     }
401 
402     /**
403      * Sets up the padding of the view containing the empty state screens.
404      * <p>This method is meant to be overridden so that subclasses can customize the padding.
405      */
setupContainerPadding(View container)406     protected void setupContainerPadding(View container) {}
407 
showSpinner(View emptyStateView)408     private void showSpinner(View emptyStateView) {
409         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE);
410         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
411         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE);
412         emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
413     }
414 
resetViewVisibilitiesForEmptyState(View emptyStateView)415     private void resetViewVisibilitiesForEmptyState(View emptyStateView) {
416         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE);
417         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE);
418         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
419         emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
420         emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
421     }
422 
showListView(ResolverListAdapter activeListAdapter)423     protected void showListView(ResolverListAdapter activeListAdapter) {
424         ProfileDescriptor descriptor = getItem(
425                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
426         descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
427         View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
428         emptyStateView.setVisibility(View.GONE);
429     }
430 
shouldShowEmptyStateScreen(ResolverListAdapter listAdapter)431     boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
432         int count = listAdapter.getUnfilteredCount();
433         return (count == 0 && listAdapter.getPlaceholderCount() == 0)
434                 || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
435                     && mWorkProfileQuietModeChecker.get());
436     }
437 
438     protected static class ProfileDescriptor {
439         final ViewGroup rootView;
440         private final ViewGroup mEmptyStateView;
ProfileDescriptor(ViewGroup rootView)441         ProfileDescriptor(ViewGroup rootView) {
442             this.rootView = rootView;
443             mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
444         }
445 
getEmptyStateView()446         protected ViewGroup getEmptyStateView() {
447             return mEmptyStateView;
448         }
449     }
450 
451     public interface OnProfileSelectedListener {
452         /**
453          * Callback for when the user changes the active tab from personal to work or vice versa.
454          * <p>This callback is only called when the intent resolver or share sheet shows
455          * the work and personal profiles.
456          * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or
457          * {@link #PROFILE_WORK} if the work profile was selected.
458          */
onProfileSelected(int profileIndex)459         void onProfileSelected(int profileIndex);
460 
461 
462         /**
463          * Callback for when the scroll state changes. Useful for discovering when the user begins
464          * dragging, when the pager is automatically settling to the current page, or when it is
465          * fully stopped/idle.
466          * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING}
467          *              or {@link ViewPager#SCROLL_STATE_SETTLING}
468          * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged
469          */
onProfilePageStateChanged(int state)470         void onProfilePageStateChanged(int state);
471     }
472 
473     /**
474      * Returns an empty state to show for the current profile page (tab) if necessary.
475      * This could be used e.g. to show a blocker on a tab if device management policy doesn't
476      * allow to use it or there are no apps available.
477      */
478     public interface EmptyStateProvider {
479         /**
480          * When a non-null empty state is returned the corresponding profile page will show
481          * this empty state
482          * @param resolverListAdapter the current adapter
483          */
484         @Nullable
getEmptyState(ResolverListAdapter resolverListAdapter)485         default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
486             return null;
487         }
488     }
489 
490     /**
491      * Empty state provider that combines multiple providers. Providers earlier in the list have
492      * priority, that is if there is a provider that returns non-null empty state then all further
493      * providers will be ignored.
494      */
495     public static class CompositeEmptyStateProvider implements EmptyStateProvider {
496 
497         private final EmptyStateProvider[] mProviders;
498 
CompositeEmptyStateProvider(EmptyStateProvider... providers)499         public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
500             mProviders = providers;
501         }
502 
503         @Nullable
504         @Override
getEmptyState(ResolverListAdapter resolverListAdapter)505         public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
506             for (EmptyStateProvider provider : mProviders) {
507                 EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
508                 if (emptyState != null) {
509                     return emptyState;
510                 }
511             }
512             return null;
513         }
514     }
515 
516     /**
517      * Describes how the blocked empty state should look like for a profile tab
518      */
519     public interface EmptyState {
520         /**
521          * Title that will be shown on the empty state
522          */
523         @Nullable
getTitle()524         default String getTitle() { return null; }
525 
526         /**
527          * Subtitle that will be shown underneath the title on the empty state
528          */
529         @Nullable
getSubtitle()530         default String getSubtitle()  { return null; }
531 
532         /**
533          * If non-null then a button will be shown and this listener will be called
534          * when the button is clicked
535          */
536         @Nullable
getButtonClickListener()537         default ClickListener getButtonClickListener()  { return null; }
538 
539         /**
540          * If true then default text ('No apps can perform this action') and style for the empty
541          * state will be applied, title and subtitle will be ignored.
542          */
useDefaultEmptyView()543         default boolean useDefaultEmptyView() { return false; }
544 
545         /**
546          * Returns true if for this empty state we should skip rebuilding of the apps list
547          * for this tab.
548          */
shouldSkipDataRebuild()549         default boolean shouldSkipDataRebuild() { return false; }
550 
551         /**
552          * Called when empty state is shown, could be used e.g. to track analytics events
553          */
onEmptyStateShown()554         default void onEmptyStateShown() {}
555 
556         interface ClickListener {
onClick(TabControl currentTab)557             void onClick(TabControl currentTab);
558         }
559 
560         interface TabControl {
showSpinner()561             void showSpinner();
562         }
563     }
564 
565 
566     /**
567      * Listener for when the user switches on the work profile from the work tab.
568      */
569     interface OnSwitchOnWorkSelectedListener {
570         /**
571          * Callback for when the user switches on the work profile from the work tab.
572          */
onSwitchOnWorkSelected()573         void onSwitchOnWorkSelected();
574     }
575 }
576