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