diff options
author | 2020-03-02 14:14:06 +0000 | |
---|---|---|
committer | 2020-03-09 11:18:46 +0000 | |
commit | 339c7bcdf3353f948144d675a49aa35fad647417 (patch) | |
tree | 4e478a21d83d9905033e2602aa3208f19093f028 | |
parent | 4d5551ae0172dcd7a6d3c1dbde7eb9fb521ff5d2 (diff) |
Enable DirectoryFragment.onRefresh works when there is no root doc.
This enables us to reload the directory correctly. On next CL,
we will refresh directory when we listened to some event. (e.g. work
profile is unlocked)
Also now we can perform search in directory loader on a disabled user.
* In DirectoryFragment, refresh stack without root doc
DirectoryFragment will now try to reload rootDoc and push it to stack
onRefresh. This is useful when the root doc was temporarily unable to
load (e.g. work profile was turned off)
* Search across profile on an empty stack
Directory takes care of empty stack now. We will restart loader in
loadDocumentsForCurrentStack. If there is only one queriable user,
the same exception message will be updated.
* ProfileTabs
The tab layout will be updated if the current root does not match.
This could happen when opening a search folder from the other user.
* QuickViewIntentBuilder
Now use a correct packageManager to test intent
Bug: 148270816
Bug: 150600030
Bug: 150799134
Test: atest DocumentsUIGoogleTests
Test: manual
Change-Id: Ib6684e4dd2a257f92ee3784297b5568cbd3d21b2
13 files changed, 429 insertions, 48 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index a3cd89b49..815b2f60b 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -471,7 +471,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } Intent intent = new QuickViewIntentBuilder( - mActivity.getPackageManager(), + mActivity, mActivity.getResources(), doc, mModel, @@ -575,6 +575,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mState.stack.push(doc); } else { if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) { + // It is now possible when opening cross-profile folder. Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected " + mState.stack.getRoot()); } @@ -757,17 +758,11 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA @Override public void loadDocumentsForCurrentStack() { - DocumentStack stack = mState.stack; - if (!stack.isRecents() && stack.isEmpty()) { - // TODO: we may also need to reload cross-profile supported root with empty stack - DirectoryResult result = new DirectoryResult(); - - // TODO (b/35996595): Consider plumbing through the actual exception, though it might - // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()). - result.exception = new IllegalStateException("Failed to load root document."); - mInjector.getModel().update(result); - return; - } + // mState.stack may be empty when we cannot load the root document. + // However, we still want to restart loader because we may need to perform search in a + // cross-profile scenario. + // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op. + // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null. mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); } @@ -890,14 +885,24 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA loader.setObserver(observer); return loader; } else { + // There maybe no root docInfo + DocumentInfo rootDoc = mState.stack.peek(); + + String authority = rootDoc == null + ? mState.stack.getRoot().authority + : rootDoc.authority; + String documentId = rootDoc == null + ? mState.stack.getRoot().documentId + : rootDoc.documentId; + Uri contentsUri = mSearchMgr.isSearching() ? DocumentsContract.buildSearchDocumentsUri( - mState.stack.getRoot().authority, - mState.stack.getRoot().rootId, - mSearchMgr.getCurrentSearch()) + mState.stack.getRoot().authority, + mState.stack.getRoot().rootId, + mSearchMgr.getCurrentSearch()) : DocumentsContract.buildChildDocumentsUri( - mState.stack.peek().authority, - mState.stack.peek().documentId); + authority, + documentId); final Bundle queryArgs = mSearchMgr.isSearching() ? mSearchMgr.buildQueryArgs() diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index 65aa7e7a8..3b09659f9 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -267,13 +267,38 @@ public abstract class BaseActivity }); mNavigator.setProfileTabsListener(userId -> { - // Reload the roots with the selected user is changed. + // There are several possible cases that may trigger this callback. + // 1. A user click on tab layout. + // 2. A user click on tab layout, when filter is checked. (searching = true) + // 3. A user click on a open a dir of a different user in search (stack size > 1) + // 4. After tab layout is initialized. + + if (!mState.stack.isInitialized()) { + return; + } + + // Reload the roots when the selected user is changed. + // After reloading, we have visually same roots in the drawer. But they are + // different by holding different userId. Next time when user select a root, it can + // bring the user to correct root doc. final RootsFragment roots = RootsFragment.get(getSupportFragmentManager()); if (roots != null) { roots.onSelectedUserChanged(); } - mInjector.actions.loadCrossProfileRoot(getCurrentRoot(), userId); + if (mState.stack.size() <= 1) { + // We do not load cross-profile root if the stack contains two documents. The + // stack may contain >1 docs when the user select a folder of the other user in + // search. In that case, we don't want to reload the root. The whole stack + // and the root will be updated in openFolderInSearchResult. + + // When a user filters files by search chips on the root doc, we will be in + // searching mode and with stack size 1 (0 if rootDoc cannot be loaded). + // The activity will clear search on root picked. If we don't clear the search, + // user may see the search result screen show up briefly and then get cleared. + mSearchManager.cancelSearch(); + mInjector.actions.loadCrossProfileRoot(getCurrentRoot(), userId); + } }); mSortController = SortController.create(this, mState.derivedMode, mState.sortModel); diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java index 6de41a909..9a9cba48e 100644 --- a/src/com/android/documentsui/DirectoryLoader.java +++ b/src/com/android/documentsui/DirectoryLoader.java @@ -70,6 +70,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { private final Bundle mQueryArgs; private final boolean mPhotoPicking; + @Nullable private DocumentInfo mDoc; private CancellationSignal mSignal; private DirectoryResult mResult; @@ -113,7 +114,6 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { mSignal = new CancellationSignal(); } - final ContentResolver resolver = mDoc.userId.getContentResolver(getContext()); final String authority = mUri.getAuthority(); final DirectoryResult result = new DirectoryResult(); @@ -138,24 +138,31 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { } } if (userIds.isEmpty()) { - userIds.add(mDoc.userId); + userIds.add(mRoot.userId); } if (userIds.size() == 1) { - if (!mState.canInteractWith(mDoc.userId)) { + if (!mState.canInteractWith(mRoot.userId)) { result.exception = new CrossProfileNoPermissionException(); return result; - } else if (mDoc.userId.isQuietModeEnabled(getContext())) { + } else if (mRoot.userId.isQuietModeEnabled(getContext())) { result.exception = new CrossProfileQuietModeException(); return result; + } else if (mDoc == null) { + // TODO (b/35996595): Consider plumbing through the actual exception, though it + // might not be very useful (always pointing to + // DatabaseUtils#readExceptionFromParcel()). + result.exception = new IllegalStateException("Failed to load root document."); + return result; } } - client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); - if (mDoc.isInArchive()) { + if (mDoc != null && mDoc.isInArchive()) { + final ContentResolver resolver = mRoot.userId.getContentResolver(getContext()); + client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); ArchivesProvider.acquireArchive(client, mUri); + result.client = client; } - result.client = client; if (mFeatures.isContentPagingEnabled()) { // TODO: At some point we don't want forced flags to override real paging... diff --git a/src/com/android/documentsui/IconUtils.java b/src/com/android/documentsui/IconUtils.java index 5a829ff90..1763fd2a3 100644 --- a/src/com/android/documentsui/IconUtils.java +++ b/src/com/android/documentsui/IconUtils.java @@ -38,7 +38,7 @@ public class IconUtils { } else { packageIcon = userId.getDrawable(context, icon); } - if (maybeShowBadge) { + if (packageIcon != null && maybeShowBadge) { return userId.getUserBadgedIcon(context, packageIcon); } else { return packageIcon; diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java index ca436a3cd..29595a6d1 100644 --- a/src/com/android/documentsui/NavigationViewManager.java +++ b/src/com/android/documentsui/NavigationViewManager.java @@ -18,7 +18,6 @@ package com.android.documentsui; import static com.android.documentsui.base.SharedMinimal.VERBOSE; -import android.app.Activity; import android.content.res.Resources; import android.graphics.Outline; import android.graphics.drawable.Drawable; @@ -60,7 +59,7 @@ public class NavigationViewManager { private final boolean mShowSearchBar; public NavigationViewManager( - Activity activity, + BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, @@ -74,7 +73,7 @@ public class NavigationViewManager { mEnv = env; mBreadcrumb = breadcrumb; mBreadcrumb.setup(env, state, this::onNavigationItemSelected); - mProfileTabs = new ProfileTabs(tabLayout, mState, userIdManager, mEnv); + mProfileTabs = new ProfileTabs(tabLayout, mState, userIdManager, mEnv, activity); mToolbar.setNavigationOnClickListener( new View.OnClickListener() { diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java index 6c2094f2e..44e66e22a 100644 --- a/src/com/android/documentsui/ProfileTabs.java +++ b/src/com/android/documentsui/ProfileTabs.java @@ -23,10 +23,12 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; +import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.google.android.material.tabs.TabLayout; +import com.google.common.base.Objects; import java.util.Collections; import java.util.List; @@ -40,23 +42,28 @@ public class ProfileTabs implements ProfileTabsAddons { private final TabLayout mTabs; private final State mState; private final NavigationViewManager.Environment mEnv; + private final AbstractActionHandler.CommonAddons mCommonAddons; private final UserIdManager mUserIdManager; private List<UserId> mUserIds; @Nullable private Listener mListener; + private TabLayout.OnTabSelectedListener mOnTabSelectedListener; public ProfileTabs(TabLayout tabLayout, State state, UserIdManager userIdManager, - NavigationViewManager.Environment env) { + NavigationViewManager.Environment env, + AbstractActionHandler.CommonAddons commonAddons) { mTabs = checkNotNull(tabLayout); mState = checkNotNull(state); mEnv = checkNotNull(env); + mCommonAddons = checkNotNull(commonAddons); mUserIdManager = checkNotNull(userIdManager); mTabs.removeAllTabs(); mUserIds = Collections.singletonList(UserId.CURRENT_USER); - mTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + mOnTabSelectedListener = new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { if (mListener != null) { + // find a way to identify user iteraction mListener.onUserSelected((UserId) tab.getTag()); } } @@ -68,7 +75,8 @@ public class ProfileTabs implements ProfileTabsAddons { @Override public void onTabReselected(TabLayout.Tab tab) { } - }); + }; + mTabs.addOnTabSelectedListener(mOnTabSelectedListener); } /** @@ -76,6 +84,15 @@ public class ProfileTabs implements ProfileTabsAddons { */ public void updateView() { updateTabsIfNeeded(); + RootInfo currentRoot = mCommonAddons.getCurrentRoot(); + if (mTabs.getSelectedTabPosition() == -1 + || !Objects.equal(currentRoot.userId, getSelectedUser())) { + // Update the layout according to the current root if necessary. + // Make sure we do not invoke callback. Otherwise, it is likely to cause infinite loop. + mTabs.removeOnTabSelectedListener(mOnTabSelectedListener); + mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId))); + mTabs.addOnTabSelectedListener(mOnTabSelectedListener); + } mTabs.setVisibility(shouldShow() ? View.VISIBLE : View.GONE); } @@ -98,7 +115,6 @@ public class ProfileTabs implements ProfileTabsAddons { mTabs.addTab(createTab(R.string.work_tab, mUserIdManager.getManagedUser()), /* setSelected= */false); } - mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(UserId.CURRENT_USER))); } } diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java index 14740e1eb..be9af0ff6 100644 --- a/src/com/android/documentsui/base/DocumentStack.java +++ b/src/com/android/documentsui/base/DocumentStack.java @@ -163,6 +163,7 @@ public class DocumentStack implements Durable, Parcelable { // Add this for keep stack size is 1 on recent root. if (root.isRecents()) { DocumentInfo rootRecent = new DocumentInfo(); + rootRecent.userId = root.userId; rootRecent.deriveFields(); push(rootRecent); } diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index 9d574d4a8..65b89609a 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -23,13 +23,16 @@ import static com.android.documentsui.base.State.MODE_GRID; import static com.android.documentsui.base.State.MODE_LIST; import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; +import android.os.UserHandle; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.util.Log; @@ -52,6 +55,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.MutableSelection; import androidx.recyclerview.selection.Selection; @@ -82,6 +86,7 @@ import com.android.documentsui.Model; import com.android.documentsui.ProfileTabsController; import com.android.documentsui.R; import com.android.documentsui.ThumbnailCache; +import com.android.documentsui.TimeoutTask; import com.android.documentsui.base.DocumentFilters; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; @@ -91,6 +96,7 @@ import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.base.State; import com.android.documentsui.base.State.ViewMode; +import com.android.documentsui.base.UserId; import com.android.documentsui.clipping.ClipStore; import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.clipping.UrisSupplier; @@ -103,6 +109,8 @@ import com.android.documentsui.services.FileOperations; import com.android.documentsui.sorting.SortDimension; import com.android.documentsui.sorting.SortModel; +import com.google.common.base.Objects; + import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -211,6 +219,26 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On return true; }; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (isManagedProfileAction(action)) { + UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER); + if (Objects.equal(mActivity.getSelectedUser(), UserId.of(userHandle))) { + // We only need to refresh the layout when the selected user is equal to the + // received profile user. + onRefresh(); + } + } + } + }; + + private static boolean isManagedProfileAction(String action) { + return Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action) + || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action); + } + @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -272,6 +300,9 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On @Override public void onDestroyView() { mInjector.actions.unregisterDisplayStateChangedListener(mOnDisplayStateChanged); + if (mState.supportsCrossProfile()) { + LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mReceiver); + } // Cancel any outstanding thumbnail requests final int count = mRecView.getChildCount(); @@ -427,6 +458,16 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On // Kick off loader at least once mActions.loadDocumentsForCurrentStack(); + + if (mState.supportsCrossProfile()) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + // DocumentsApplication will resend the broadcast locally after roots are updated. + // Register to a local broadcast manager to avoid this fragment from updating before + // roots are updated. + LocalBroadcastManager.getInstance(mActivity).registerReceiver(mReceiver, filter); + } } @Override @@ -1165,6 +1206,12 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On } final DocumentInfo doc = mActivity.getCurrentDirectory(); + if (doc == null) { + // If there is no root doc, try to reload the root doc from root info. + Log.w(TAG, "No root document. Try to get root document."); + getRootDocumentAndMaybeRefreshDocument(); + return; + } mActions.refreshDocument(doc, (boolean refreshSupported) -> { if (refreshSupported) { mRefreshLayout.setRefreshing(false); @@ -1175,6 +1222,26 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On }); } + private void getRootDocumentAndMaybeRefreshDocument() { + // If we can reload the root doc successfully, we will push it to the stack and load the + // stack. + final RootInfo emptyDocRoot = mActivity.getCurrentRoot(); + mInjector.actions.getRootDocument( + emptyDocRoot, + TimeoutTask.DEFAULT_TIMEOUT, + rootDoc -> { + mRefreshLayout.setRefreshing(false); + if (rootDoc != null && mActivity.getCurrentDirectory() == null) { + // Make sure the stack does not change during task was running. + Log.d(TAG, "Root doc is retrieved. Pushing to the stack"); + mState.stack.push(rootDoc); + mActivity.updateNavigator(); + mActions.loadDocumentsForCurrentStack(); + } + } + ); + } + private final class ModelUpdateListener implements EventListener<Model.Update> { @Override diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java index f399a2864..e50653518 100644 --- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java +++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java @@ -22,6 +22,7 @@ import static com.android.documentsui.base.SharedMinimal.DEBUG; import android.content.ClipData; import android.content.ClipDescription; +import android.content.Context; import android.content.Intent; import android.content.QuickViewConstants; import android.content.pm.PackageManager; @@ -80,18 +81,18 @@ public final class QuickViewIntentBuilder { private final boolean mFromPicker; public QuickViewIntentBuilder( - PackageManager packageMgr, + Context context, Resources resources, DocumentInfo doc, Model model, boolean fromPicker) { - assert(packageMgr != null); + assert(context != null); assert(resources != null); assert(doc != null); assert(model != null); - mPackageMgr = packageMgr; + mPackageMgr = doc.userId.getPackageManager(context); mResources = resources; mDocument = doc; mModel = model; diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java index 8f55dde6a..e5c7eb8d4 100644 --- a/tests/common/com/android/documentsui/testing/TestEnv.java +++ b/tests/common/com/android/documentsui/testing/TestEnv.java @@ -62,6 +62,7 @@ public class TestEnv { public static DocumentInfo FILE_READ_ONLY; public static class OtherUser { + public static DocumentInfo FOLDER_0; public static DocumentInfo FILE_PNG; } @@ -72,6 +73,7 @@ public class TestEnv { public final TestFocusHandler focusHandler = new TestFocusHandler(); public final TestDialogController dialogs = new TestDialogController(); public final TestModel model; + public final TestModel modelOtherUser; public final TestModel archiveModel; public final DocsSelectionHelper selectionMgr; public final TestSearchViewManager searchViewManager; @@ -83,13 +85,14 @@ public class TestEnv { public final MockContentResolver contentResolver; public final Map<String, TestDocumentsProvider> mockProviders; - private TestEnv(Context context, Features features, String authority, UserId userId) { + private TestEnv(Context context, Features features, String authority) { this.features = features; - this.userId = userId; + this.userId = TestProvidersAccess.USER_ID; userHandle = UserHandle.of(userId.getIdentifier()); state.sortModel = SortModel.createModel(); mExecutor = new TestScheduledExecutorService(); model = new TestModel(userId, authority, features); + modelOtherUser = new TestModel(TestProvidersAccess.OtherUser.USER_ID, authority, features); archiveModel = new TestModel(userId, ArchivesProvider.AUTHORITY, features); selectionMgr = SelectionHelpers.createTestInstance(); searchViewManager = new TestSearchViewManager(); @@ -142,7 +145,7 @@ public class TestEnv { } private static TestEnv create(Context context, Features features, String authority) { - TestEnv env = new TestEnv(context, features, authority, TestProvidersAccess.USER_ID); + TestEnv env = new TestEnv(context, features, authority); env.reset(); return env; } @@ -178,8 +181,8 @@ public class TestEnv { | Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME); - OtherUser.FILE_PNG = model.createFile("work.png"); - OtherUser.FILE_PNG.userId = TestProvidersAccess.OtherUser.USER_ID; + OtherUser.FOLDER_0 = modelOtherUser.createFolder("folder 0"); + OtherUser.FILE_PNG = modelOtherUser.createFile("work.png"); archiveModel.update(); model.update(); diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java index c5c3ac3ca..3eb8ab2fd 100644 --- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java @@ -16,10 +16,13 @@ package com.android.documentsui; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; import android.content.Intent; import android.net.Uri; @@ -32,8 +35,10 @@ import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import com.android.documentsui.base.DocumentStack; +import com.android.documentsui.base.EventListener; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; +import com.android.documentsui.base.State; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.sorting.SortDimension; import com.android.documentsui.sorting.SortModel; @@ -274,6 +279,145 @@ public class AbstractActionHandlerTest { } @Test + public void testCrossProfileDocuments_success() throws Exception { + mEnv.state.action = State.ACTION_GET_CONTENT; + mEnv.state.canShareAcrossProfile = true; + mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); + mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); + + mEnv.state.sortModel.sortByUser( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING); + + // Currently mock provider does not have cross profile concept, this will always return + // the supplied docs without checking for the user. But this should not be a problem for + // this test case. + mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority) + .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG); + + mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch = new CountDownLatch(1); + mEnv.model.addUpdateListener(event -> latch.countDown()); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + + latch.await(1, TimeUnit.SECONDS); + assertEquals(1, mEnv.model.getItemCount()); + String[] modelIds = mEnv.model.getModelIds(); + assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0])); + } + + @Test + public void testLoadCrossProfileDoc_failsWithQuietModeException() throws Exception { + mEnv.state.action = State.ACTION_GET_CONTENT; + mEnv.state.canShareAcrossProfile = true; + mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); + mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); + // Turn off the other user. + when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE)) + .thenReturn(true); + + TestEventHandler<Model.Update> listener = new TestEventHandler<>(); + mEnv.model.addUpdateListener(listener::accept); + + mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch = new CountDownLatch(1); + mEnv.model.addUpdateListener(event -> latch.countDown()); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + + latch.await(1, TimeUnit.SECONDS); + assertThat(listener.getLastValue().getException()) + .isInstanceOf(CrossProfileQuietModeException.class); + } + + @Test + public void testLoadCrossProfileDoc_failsWithNoPermissionException() throws Exception { + mEnv.state.action = State.ACTION_GET_CONTENT; + mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); + mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); + // Disallow sharing across profile + mEnv.state.canShareAcrossProfile = false; + + TestEventHandler<Model.Update> listener = new TestEventHandler<>(); + mEnv.model.addUpdateListener(listener::accept); + + mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch = new CountDownLatch(1); + mEnv.model.addUpdateListener(event -> latch.countDown()); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + + latch.await(1, TimeUnit.SECONDS); + assertThat(listener.getLastValue().getException()) + .isInstanceOf(CrossProfileNoPermissionException.class); + } + + @Test + public void testLoadCrossProfileDoc_bothError_showNoPermissionException() throws Exception { + mEnv.state.action = State.ACTION_GET_CONTENT; + mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); + mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); + // Disallow sharing + mEnv.state.canShareAcrossProfile = false; + // Turn off the other user. + when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE)) + .thenReturn(true); + + TestEventHandler<Model.Update> listener = new TestEventHandler<>(); + mEnv.model.addUpdateListener(listener::accept); + + mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch = new CountDownLatch(1); + mEnv.model.addUpdateListener(event -> latch.countDown()); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + + latch.await(1, TimeUnit.SECONDS); + assertThat(listener.getLastValue().getException()) + .isInstanceOf(CrossProfileNoPermissionException.class); + } + + @Test + public void testCrossProfileDocuments_reloadSuccessAfterCrossProfileError() throws Exception { + mEnv.state.action = State.ACTION_GET_CONTENT; + mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); + mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); + + mEnv.state.sortModel.sortByUser( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING); + + // Currently mock provider does not have cross profile concept, this will always return + // the supplied docs without checking for the user. But this should not be a problem for + // this test case. + mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority) + .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG); + + // Disallow sharing across profile + mEnv.state.canShareAcrossProfile = false; + + TestEventHandler<Model.Update> listener = new TestEventHandler<>(); + mEnv.model.addUpdateListener(listener::accept); + + mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch1 = new CountDownLatch(1); + EventListener<Model.Update> updateEventListener1 = update -> latch1.countDown(); + mEnv.model.addUpdateListener(updateEventListener1); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + latch1.await(1, TimeUnit.SECONDS); + assertThat(listener.getLastValue().getException()) + .isInstanceOf(CrossProfileNoPermissionException.class); + + // Allow sharing across profile. + mEnv.state.canShareAcrossProfile = true; + + CountDownLatch latch2 = new CountDownLatch(1); + mEnv.model.addUpdateListener(update -> latch2.countDown()); + mHandler.loadDocumentsForCurrentStack(); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + + latch2.await(1, TimeUnit.SECONDS); + assertEquals(1, mEnv.model.getItemCount()); + String[] modelIds = mEnv.model.getModelIds(); + assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0])); + } + + @Test public void testLoadChildrenDocuments_failsWithNonRecentsAndEmptyStack() throws Exception { mEnv.state.stack.changeRoot(TestProvidersAccess.HOME); @@ -284,7 +428,11 @@ public class AbstractActionHandlerTest { mEnv.model.addUpdateListener(listener::accept); mHandler.loadDocumentsForCurrentStack(); + CountDownLatch latch = new CountDownLatch(1); + mEnv.model.addUpdateListener(event -> latch.countDown()); + mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID); + latch.await(1, TimeUnit.SECONDS); assertTrue(listener.getLastValue().hasException()); } diff --git a/tests/unit/com/android/documentsui/ProfileTabsTest.java b/tests/unit/com/android/documentsui/ProfileTabsTest.java index 5330e9c95..156c7cab1 100644 --- a/tests/unit/com/android/documentsui/ProfileTabsTest.java +++ b/tests/unit/com/android/documentsui/ProfileTabsTest.java @@ -19,12 +19,14 @@ package com.android.documentsui; import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.net.Uri; import android.os.UserHandle; import android.view.LayoutInflater; import android.view.View; import androidx.test.InstrumentationRegistry; +import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; @@ -38,6 +40,7 @@ import org.junit.Before; import org.junit.Test; import java.util.Collections; +import java.util.List; public class ProfileTabsTest { @@ -51,6 +54,8 @@ public class ProfileTabsTest { private TestEnvironment mTestEnv; private State mState; private TestUserIdManager mTestUserIdManager; + private TestCommonAddons mTestCommonAddons; + private boolean mIsListenerInvoked; @Before public void setUp() { @@ -70,6 +75,8 @@ public class ProfileTabsTest { mTestEnv.isSearching = false; mTestUserIdManager = new TestUserIdManager(); + mTestCommonAddons = new TestCommonAddons(); + mTestCommonAddons.mCurrentRoot = TestProvidersAccess.DOWNLOADS; } @Test @@ -167,6 +174,23 @@ public class ProfileTabsTest { } @Test + public void testUpdateView_afterCurrentRootChanged_shouldChangeSelectedUser() { + initializeWithUsers(systemUser, managedUser); + mProfileTabs.updateView(); + + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + + RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot); + newRoot.userId = managedUser; + mTestCommonAddons.mCurrentRoot = newRoot; + mProfileTabs.updateView(); + + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser); + // updating view should not trigger listener callback. + assertThat(mIsListenerInvoked).isFalse(); + } + + @Test public void testgetSelectedUser_twoUsers() { initializeWithUsers(systemUser, managedUser); @@ -175,6 +199,19 @@ public class ProfileTabsTest { mTabLayout.selectTab(mTabLayout.getTabAt(1)); assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser); + assertThat(mIsListenerInvoked).isTrue(); + } + + @Test + public void testReselectedUser_doesNotInvokeListener() { + initializeWithUsers(systemUser, managedUser); + + assertThat(mTabLayout.getSelectedTabPosition()).isAtLeast(0); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + + mTabLayout.selectTab(mTabLayout.getTabAt(0)); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mIsListenerInvoked).isFalse(); } @Test @@ -194,8 +231,10 @@ public class ProfileTabsTest { } } - mProfileTabs = new ProfileTabs(mTabLayout, mState, mTestUserIdManager, mTestEnv); + mProfileTabs = new ProfileTabs(mTabLayout, mState, mTestUserIdManager, mTestEnv, + mTestCommonAddons); mProfileTabs.updateView(); + mProfileTabs.setListener(userId -> mIsListenerInvoked = true); } /** @@ -231,5 +270,70 @@ public class ProfileTabsTest { } } + + private static class TestCommonAddons implements AbstractActionHandler.CommonAddons { + + private RootInfo mCurrentRoot; + + @Override + public void restoreRootAndDirectory() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void refreshCurrentRootAndDirectory(int anim) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void onRootPicked(RootInfo root) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void onDocumentsPicked(List<DocumentInfo> docs) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void onDocumentPicked(DocumentInfo doc) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public RootInfo getCurrentRoot() { + return mCurrentRoot; + } + + @Override + public DocumentInfo getCurrentDirectory() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public UserId getSelectedUser() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public boolean isInRecents() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void setRootsDrawerOpen(boolean open) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void updateNavigator() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void notifyDirectoryNavigated(Uri docUri) { + throw new UnsupportedOperationException("not implemented"); + } + } } diff --git a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java index 4679ee8a1..7f3c4c43c 100644 --- a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java +++ b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java @@ -3,10 +3,15 @@ package com.android.documentsui.files; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; import android.content.Intent; import android.content.QuickViewConstants; import android.content.pm.PackageManager; +import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -14,8 +19,6 @@ import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestPackageManager; import com.android.documentsui.testing.TestResources; -import androidx.test.InstrumentationRegistry; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +32,7 @@ import java.util.Set; public class QuickViewIntentBuilderTest { private static String mTargetPackageName; + private Context mContext = mock(Context.class); private PackageManager mPm; private TestEnv mEnv; private TestResources mRes; @@ -38,6 +42,7 @@ public class QuickViewIntentBuilderTest { mTargetPackageName = InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(); mPm = TestPackageManager.create(); + when(mContext.getPackageManager()).thenReturn(mPm); mEnv = TestEnv.create(); mRes = TestResources.create(); @@ -48,7 +53,7 @@ public class QuickViewIntentBuilderTest { public void testSetsNoFeatures_InArchiveDocument() { QuickViewIntentBuilder builder = new QuickViewIntentBuilder( - mPm, mRes, TestEnv.FILE_IN_ARCHIVE, mEnv.archiveModel, false); + mContext, mRes, TestEnv.FILE_IN_ARCHIVE, mEnv.archiveModel, false); Intent intent = builder.build(); @@ -59,7 +64,7 @@ public class QuickViewIntentBuilderTest { @Test public void testSetsFullFeatures_RegularDocument() { QuickViewIntentBuilder builder = - new QuickViewIntentBuilder(mPm, mRes, TestEnv.FILE_JPG, mEnv.model, false); + new QuickViewIntentBuilder(mContext, mRes, TestEnv.FILE_JPG, mEnv.model, false); Intent intent = builder.build(); @@ -79,7 +84,7 @@ public class QuickViewIntentBuilderTest { public void testPickerFeatures_RegularDocument() { QuickViewIntentBuilder builder = - new QuickViewIntentBuilder(mPm, mRes, TestEnv.FILE_JPG, mEnv.model, true); + new QuickViewIntentBuilder(mContext, mRes, TestEnv.FILE_JPG, mEnv.model, true); Intent intent = builder.build(); |