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