diff options
author | 2020-02-20 20:36:23 +0000 | |
---|---|---|
committer | 2020-02-25 17:23:52 +0000 | |
commit | a649f548711e4f1cb6ba86285fbd731ee3ba533c (patch) | |
tree | bfc6407d28584b7dfbd732f0dc8f9797b6950c26 | |
parent | 4505a1787d58c3838c3800d1d89a93201a199d13 (diff) |
Ensure user can only share documents from allowed users.
This is done by placing checking on various levels.
- On the UI level, we check in the loader (DirectoryLoader/Recents Loader).
- We also block querying document in DocumentsAccess
- And we will not return the uris if we are not allowed to access the target
user uri.
Need to follow up on some issues:
Breadcrumb bar disappeared in disabled user
(because we cannot load root document and push to the stack)
Cannot search if selected user is disabled (because of empty stack.
We cannot restart loader in loadDocumentsForCurrentStack)
Bug: 149818298
Bug: 148270816
Test: atest DocumentsUIGoogleTests
Test: manual
Change-Id: I000813d3e66a8c376dbfbd7e4085f403c2e5e0f6
35 files changed, 778 insertions, 82 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml index 7c719e644..18dc80b0a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -201,6 +201,9 @@ <!-- Toast shown when user want to share files amount over limit [CHAR LIMIT=60] --> <string name="toast_share_over_limit">Can\u2019t share more than <xliff:g id="count" example="1">%1$d</xliff:g> files</string> + <!-- Toast shown when a document is not allowed to share across users. This is a last-resort toast and not expected to be shown. [CHAR LIMIT=48] --> + <string name="toast_action_not_allowed">Action not allowed</string> + <!-- Title of dialog when prompting user to select an app to share documents with [CHAR LIMIT=32] --> <string name="share_via">Share via</string> diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 59878ecde..70b946f56 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -89,6 +89,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA @VisibleForTesting public static final int CODE_FORWARD = 42; public static final int CODE_AUTHENTICATION = 43; + public static final int CODE_FORWARD_CROSS_PROFILE = 44; @VisibleForTesting static final int LOADER_ID = 42; @@ -259,7 +260,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } @Override - public void openRoot(ResolveInfo app) { + public void openRoot(ResolveInfo app, UserId userId) { throw new UnsupportedOperationException("Can't open an app."); } @@ -758,6 +759,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA 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 diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java index 189608abb..71cccf9ee 100644 --- a/src/com/android/documentsui/ActionHandler.java +++ b/src/com/android/documentsui/ActionHandler.java @@ -93,7 +93,7 @@ public interface ActionHandler { void openRoot(RootInfo root); - void openRoot(ResolveInfo app); + void openRoot(ResolveInfo app, UserId userId); void loadRoot(Uri uri, UserId userId); diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index e9f89eaa7..a6850b3fa 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -154,7 +154,7 @@ public abstract class BaseActivity Metrics.logActivityLaunch(mState, intent); mProviders = DocumentsApplication.getProvidersCache(this); - mDocs = DocumentsAccess.create(this); + mDocs = DocumentsAccess.create(this, mState); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/src/com/android/documentsui/CrossProfileException.java b/src/com/android/documentsui/CrossProfileException.java new file mode 100644 index 000000000..cf6e525a1 --- /dev/null +++ b/src/com/android/documentsui/CrossProfileException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +/** + * Represents an exception related to cross profile. + */ +public abstract class CrossProfileException extends Exception { +} diff --git a/src/com/android/documentsui/CrossProfileNoPermissionException.java b/src/com/android/documentsui/CrossProfileNoPermissionException.java new file mode 100644 index 000000000..484f07e0f --- /dev/null +++ b/src/com/android/documentsui/CrossProfileNoPermissionException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +/** + * Represents an exception when no permission to query the target profile. + */ +class CrossProfileNoPermissionException extends CrossProfileException { +} diff --git a/src/com/android/documentsui/CrossProfileQuietModeException.java b/src/com/android/documentsui/CrossProfileQuietModeException.java new file mode 100644 index 000000000..db2df1f08 --- /dev/null +++ b/src/com/android/documentsui/CrossProfileQuietModeException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +/** + * Represents an exception when the other profile is in quiet mode. + */ +class CrossProfileQuietModeException extends CrossProfileException { +} diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java index cf6953d0b..6de41a909 100644 --- a/src/com/android/documentsui/DirectoryLoader.java +++ b/src/com/android/documentsui/DirectoryLoader.java @@ -122,27 +122,41 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { ContentProviderClient client = null; Cursor cursor; try { - client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); - if (mDoc.isInArchive()) { - ArchivesProvider.acquireArchive(client, mUri); - } - result.client = client; - final Bundle queryArgs = new Bundle(); mModel.addQuerySortArgs(queryArgs); final List<UserId> userIds = new ArrayList<>(); if (mSearchMode) { queryArgs.putAll(mQueryArgs); - if (mState.canShareAcrossProfile && mRoot.supportsCrossProfile()) { - userIds.addAll( - DocumentsApplication.getUserIdManager(getContext()).getUserIds()); + if (mState.supportsCrossProfile() && mRoot.supportsCrossProfile()) { + for (UserId userId : DocumentsApplication.getUserIdManager( + getContext()).getUserIds()) { + if (mState.canInteractWith(userId)) { + userIds.add(userId); + } + } } } if (userIds.isEmpty()) { userIds.add(mDoc.userId); } + if (userIds.size() == 1) { + if (!mState.canInteractWith(mDoc.userId)) { + result.exception = new CrossProfileNoPermissionException(); + return result; + } else if (mDoc.userId.isQuietModeEnabled(getContext())) { + result.exception = new CrossProfileQuietModeException(); + return result; + } + } + + client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); + if (mDoc.isInArchive()) { + ArchivesProvider.acquireArchive(client, mUri); + } + result.client = client; + if (mFeatures.isContentPagingEnabled()) { // TODO: At some point we don't want forced flags to override real paging... // and that point is when we have real paging. @@ -275,7 +289,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { // Ensure the loader is stopped onStopLoading(); - if (mResult.cursor != null && mObserver != null) { + if (mResult != null && mResult.cursor != null && mObserver != null) { mResult.cursor.unregisterContentObserver(mObserver); } diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java index 9108b15eb..c10f9abef 100644 --- a/src/com/android/documentsui/DocumentsAccess.java +++ b/src/com/android/documentsui/DocumentsAccess.java @@ -34,6 +34,7 @@ import androidx.annotation.Nullable; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import java.io.FileNotFoundException; @@ -53,15 +54,16 @@ public interface DocumentsAccess { boolean isDocumentUri(Uri uri); @Nullable - Path findDocumentPath(Uri uri, UserId userId) throws RemoteException, FileNotFoundException; + Path findDocumentPath(Uri uri, UserId userId) + throws RemoteException, FileNotFoundException, CrossProfileNoPermissionException; List<DocumentInfo> getDocuments(UserId userId, String authority, List<String> docIds) - throws RemoteException; + throws RemoteException, CrossProfileNoPermissionException; @Nullable Uri createDocument(DocumentInfo parentDoc, String mimeType, String displayName); - public static DocumentsAccess create(Context context) { - return new RuntimeDocumentAccess(context); + public static DocumentsAccess create(Context context, State state) { + return new RuntimeDocumentAccess(context, state); } public final class RuntimeDocumentAccess implements DocumentsAccess { @@ -69,9 +71,11 @@ public interface DocumentsAccess { private static final String TAG = "DocumentAccess"; private final Context mContext; + private final State mState; - private RuntimeDocumentAccess(Context context) { + private RuntimeDocumentAccess(Context context, State state) { mContext = context; + mState = state; } @Override @@ -84,7 +88,9 @@ public interface DocumentsAccess { @Override public @Nullable DocumentInfo getDocument(Uri uri, UserId userId) { try { - return DocumentInfo.fromUri(userId.getContentResolver(mContext), uri, userId); + if (mState.canInteractWith(userId)) { + return DocumentInfo.fromUri(userId.getContentResolver(mContext), uri, userId); + } } catch (FileNotFoundException e) { Log.w(TAG, "Couldn't create DocumentInfo for uri: " + uri); } @@ -94,8 +100,10 @@ public interface DocumentsAccess { @Override public List<DocumentInfo> getDocuments(UserId userId, String authority, List<String> docIds) - throws RemoteException { - + throws RemoteException, CrossProfileNoPermissionException { + if (!mState.canInteractWith(userId)) { + throw new CrossProfileNoPermissionException(); + } try (ContentProviderClient client = DocumentsApplication.acquireUnstableProviderOrThrow( userId.getContentResolver(mContext), authority)) { @@ -130,7 +138,10 @@ public interface DocumentsAccess { @Override public Path findDocumentPath(Uri docUri, UserId userId) - throws RemoteException, FileNotFoundException { + throws RemoteException, FileNotFoundException, CrossProfileNoPermissionException { + if (!mState.canInteractWith(userId)) { + throw new CrossProfileNoPermissionException(); + } final ContentResolver resolver = userId.getContentResolver(mContext); try (final ContentProviderClient client = DocumentsApplication .acquireUnstableProviderOrThrow(resolver, docUri.getAuthority())) { diff --git a/src/com/android/documentsui/Model.java b/src/com/android/documentsui/Model.java index aef17a380..49e3c9b26 100644 --- a/src/com/android/documentsui/Model.java +++ b/src/com/android/documentsui/Model.java @@ -316,6 +316,12 @@ public class Model { && mException instanceof AuthenticationRequiredException; } + public boolean hasCrossProfileException() { + return mRemoteActionEnabled + && hasException() + && mException instanceof CrossProfileException; + } + public @Nullable Exception getException() { return mException; } diff --git a/src/com/android/documentsui/MultiRootDocumentsLoader.java b/src/com/android/documentsui/MultiRootDocumentsLoader.java index 711a81ca0..e98abbcb1 100644 --- a/src/com/android/documentsui/MultiRootDocumentsLoader.java +++ b/src/com/android/documentsui/MultiRootDocumentsLoader.java @@ -247,7 +247,8 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory HashMap<String, List<RootInfo>> rootsIndex = new HashMap<>(); for (RootInfo root : roots) { // ignore the root with authority is null. e.g. Recent - if (root.authority == null || shouldIgnoreRoot(root)) { + if (root.authority == null || shouldIgnoreRoot(root) + || !mState.canInteractWith(root.userId)) { continue; } diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java index c8472cf3a..2eb259e40 100644 --- a/src/com/android/documentsui/RecentsLoader.java +++ b/src/com/android/documentsui/RecentsLoader.java @@ -53,6 +53,20 @@ public class RecentsLoader extends MultiRootDocumentsLoader { } @Override + public DirectoryResult loadInBackground() { + if (!mState.canInteractWith(mUserId)) { + DirectoryResult result = new DirectoryResult(); + result.exception = new CrossProfileNoPermissionException(); + return result; + } else if (mUserId.isQuietModeEnabled(getContext())) { + DirectoryResult result = new DirectoryResult(); + result.exception = new CrossProfileQuietModeException(); + return result; + } + return super.loadInBackground(); + } + + @Override protected long getRejectBeforeTime() { return System.currentTimeMillis() - REJECT_OLDER_THAN; } diff --git a/src/com/android/documentsui/RefreshTask.java b/src/com/android/documentsui/RefreshTask.java index bcdfac83c..091c64b25 100644 --- a/src/com/android/documentsui/RefreshTask.java +++ b/src/com/android/documentsui/RefreshTask.java @@ -85,6 +85,12 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> { return false; } + if (!mState.canInteractWith(mDoc.userId) || mDoc.userId.isQuietModeEnabled(mContext)) { + // No result was returned by these errors so it does not support refresh. + Log.w(TAG, "Cannot refresh due to cross profile error."); + return false; + } + // API O introduces ContentResolver#refresh, and if available and the ContentProvider // supports it, the ContentProvider will automatically send a content updated notification // and we will update accordingly. Else, we just tell the callback that Refresh is not @@ -94,7 +100,7 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> { return false; } - final ContentResolver resolver = mContext.getContentResolver(); + final ContentResolver resolver = mDoc.userId.getContentResolver(mContext); final String authority = mDoc.authority; boolean refreshSupported = false; ContentProviderClient client = null; diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java index 88be98dae..5106b2273 100644 --- a/src/com/android/documentsui/base/State.java +++ b/src/com/android/documentsui/base/State.java @@ -90,6 +90,13 @@ public class State implements android.os.Parcelable { public boolean canShareAcrossProfile = false; /** + * Returns true if we are allowed to interact with the user. + */ + public boolean canInteractWith(UserId userId) { + return canShareAcrossProfile || UserId.CURRENT_USER.equals(userId); + } + + /** * This is basically a sub-type for the copy operation. It can be either COPY, * COMPRESS, EXTRACT or MOVE. * The only legal values, if set, are: OPERATION_COPY, OPERATION_COMPRESS, diff --git a/src/com/android/documentsui/base/UserId.java b/src/com/android/documentsui/base/UserId.java index b65300c32..d17b3e79c 100644 --- a/src/com/android/documentsui/base/UserId.java +++ b/src/com/android/documentsui/base/UserId.java @@ -138,6 +138,15 @@ public final class UserId { } /** + * Returns true if the this user is in quiet mode. + */ + public boolean isQuietModeEnabled(Context context) { + final UserManager userManager = + (UserManager) context.getSystemService(Context.USER_SERVICE); + return userManager.isQuietModeEnabled(mUserHandle); + } + + /** * Returns a document uri representing this user. */ public Uri buildDocumentUriAsUser(String authority, String documentId) { diff --git a/src/com/android/documentsui/dirlist/AppsRowItemData.java b/src/com/android/documentsui/dirlist/AppsRowItemData.java index 305bde20b..21f6af28c 100644 --- a/src/com/android/documentsui/dirlist/AppsRowItemData.java +++ b/src/com/android/documentsui/dirlist/AppsRowItemData.java @@ -86,7 +86,7 @@ public abstract class AppsRowItemData { @Override protected void onClicked() { - mActionHandler.openRoot(mResolveInfo); + mActionHandler.openRoot(mResolveInfo, mUserId); } } diff --git a/src/com/android/documentsui/dirlist/Message.java b/src/com/android/documentsui/dirlist/Message.java index a4124f1dc..f07e4e503 100644 --- a/src/com/android/documentsui/dirlist/Message.java +++ b/src/com/android/documentsui/dirlist/Message.java @@ -177,8 +177,11 @@ abstract class Message { @Override void update(Update event) { reset(); - if (event.hasException() && !event.hasAuthenticationException()) { - updateToInflatedErrorMesage(); + if (event.hasCrossProfileException()) { + // TODO: update error message. + updateToInflatedErrorMessage(); + } else if (event.hasException() && !event.hasAuthenticationException()) { + updateToInflatedErrorMessage(); } else if (event.hasAuthenticationException()) { updateToCantDisplayContentMessage(); } else if (mEnv.getModel().getModelIds().length == 0) { @@ -186,7 +189,7 @@ abstract class Message { } } - private void updateToInflatedErrorMesage() { + private void updateToInflatedErrorMessage() { update(null, mEnv.getContext().getResources().getText(R.string.query_error), null, mEnv.getContext().getDrawable(R.drawable.hourglass)); } diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java index bcbd8e273..03fb04ecc 100644 --- a/src/com/android/documentsui/picker/ActionHandler.java +++ b/src/com/android/documentsui/picker/ActionHandler.java @@ -46,6 +46,7 @@ import com.android.documentsui.DocumentsAccess; import com.android.documentsui.Injector; import com.android.documentsui.MetricConsts; import com.android.documentsui.Metrics; +import com.android.documentsui.UserIdManager; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; @@ -76,18 +77,20 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private final Features mFeatures; private final ActivityConfig mConfig; private final LastAccessedStorage mLastAccessed; + private final UserIdManager mUserIdManager; private UpdatePickResultTask mUpdatePickResultTask; ActionHandler( - T activity, - State state, - ProvidersAccess providers, - DocumentsAccess docs, - SearchViewManager searchMgr, - Lookup<String, Executor> executors, - Injector injector, - LastAccessedStorage lastAccessed) { + T activity, + State state, + ProvidersAccess providers, + DocumentsAccess docs, + SearchViewManager searchMgr, + Lookup<String, Executor> executors, + Injector injector, + LastAccessedStorage lastAccessed, + UserIdManager userIdManager) { super(activity, state, providers, docs, searchMgr, executors, injector); mConfig = injector.config; @@ -95,6 +98,7 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH mLastAccessed = lastAccessed; mUpdatePickResultTask = new UpdatePickResultTask( activity.getApplicationContext(), mInjector.pickResult); + mUserIdManager = userIdManager; } @Override @@ -251,15 +255,25 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH // let the user pick another app/backend. switch (requestCode) { case CODE_FORWARD: - onExternalAppResult(resultCode, data); + case CODE_FORWARD_CROSS_PROFILE: + onExternalAppResult(requestCode, resultCode, data); break; default: super.onActivityResult(requestCode, resultCode, data); } } - private void onExternalAppResult(int resultCode, Intent data) { + private void onExternalAppResult(int requestCode, int resultCode, Intent data) { if (resultCode != FragmentActivity.RESULT_CANCELED) { + if (requestCode == CODE_FORWARD_CROSS_PROFILE) { + UserId otherUser = UserId.CURRENT_USER.equals(mUserIdManager.getSystemUser()) + ? mUserIdManager.getManagedUser() + : mUserIdManager.getSystemUser(); + if (!mState.canInteractWith(otherUser)) { + mDialogs.showActionNotAllowed(); + return; + } + } // Remember that we last picked via external app mLastAccessed.setLastAccessedToExternalApp(mActivity); @@ -287,9 +301,18 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH } @Override - public void openRoot(ResolveInfo info) { + public void openRoot(ResolveInfo info, UserId userId) { Metrics.logAppVisited(info); mInjector.pickResult.increaseActionCount(); + + // The App root item should not show if we cannot interact with the target user. + // But the user managed to get here, this is the final check of permission. We don't + // perform the check on activity result. + if (!mState.canInteractWith(userId)) { + mInjector.dialogs.showActionNotAllowed(); + return; + } + final Intent intent = new Intent(mActivity.getIntent()); final int flagsRemoved = Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_GRANT_READ_URI_PERMISSION @@ -300,7 +323,9 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH intent.setComponent(new ComponentName( info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); try { - mActivity.startActivityForResult(intent, CODE_FORWARD); + boolean isCurrentUser = UserId.CURRENT_USER.equals(userId); + mActivity.startActivityForResult(intent, + isCurrentUser ? CODE_FORWARD : CODE_FORWARD_CROSS_PROFILE); } catch (SecurityException | ActivityNotFoundException e) { Log.e(TAG, "Caught error: " + e.getLocalizedMessage()); mInjector.dialogs.showNoApplicationFound(); diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java index 0e7e47ba9..cdc1a46c6 100644 --- a/src/com/android/documentsui/picker/PickActivity.java +++ b/src/com/android/documentsui/picker/PickActivity.java @@ -31,6 +31,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.DocumentsContract; +import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -66,6 +67,7 @@ import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.MessageBuilder; import java.util.Collection; +import java.util.Collections; import java.util.List; public class PickActivity extends BaseActivity implements ActionHandler.Addons { @@ -134,7 +136,8 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { mSearchManager, ProviderExecutor::forAuthority, mInjector, - LastAccessedStorage.create()); + LastAccessedStorage.create(), + DocumentsApplication.getUserIdManager(this)); mInjector.searchManager = mSearchManager; @@ -373,6 +376,12 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { mSearchManager.recordHistory(); } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { // Explicit file picked, return + if (!canShare(Collections.singletonList(doc))) { + // A final check to make sure we can share the uri before returning it. + Log.e(TAG, "The document cannot be shared"); + mInjector.dialogs.showActionNotAllowed(); + return; + } mInjector.actions.finishPicking(doc.getDocumentUri()); mSearchManager.recordHistory(); } else if (mState.action == ACTION_CREATE) { @@ -384,6 +393,12 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { @Override public void onDocumentsPicked(List<DocumentInfo> docs) { if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { + if (!canShare(docs)) { + // A final check to make sure we can share these uris before returning them. + Log.e(TAG, "One or more document cannot be shared"); + mInjector.dialogs.showActionNotAllowed(); + return; + } final int size = docs.size(); final Uri[] uris = new Uri[size]; for (int i = 0; i < size; i++) { @@ -394,6 +409,15 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { } } + private boolean canShare(List<DocumentInfo> docs) { + for (DocumentInfo doc : docs) { + if (!mState.canInteractWith(doc.userId)) { + return false; + } + } + return true; + } + @CallSuper @Override public boolean onKeyDown(int keyCode, KeyEvent event) { diff --git a/src/com/android/documentsui/sidebar/AppItem.java b/src/com/android/documentsui/sidebar/AppItem.java index 8be1ae5bc..a17ad4f65 100644 --- a/src/com/android/documentsui/sidebar/AppItem.java +++ b/src/com/android/documentsui/sidebar/AppItem.java @@ -101,7 +101,7 @@ public class AppItem extends Item { @Override void open() { - mActionHandler.openRoot(info); + mActionHandler.openRoot(info, userId); } @Override diff --git a/src/com/android/documentsui/sidebar/RootAndAppItem.java b/src/com/android/documentsui/sidebar/RootAndAppItem.java index e6b51f911..11d51cb38 100644 --- a/src/com/android/documentsui/sidebar/RootAndAppItem.java +++ b/src/com/android/documentsui/sidebar/RootAndAppItem.java @@ -60,7 +60,7 @@ class RootAndAppItem extends RootItem { @Override protected void onActionClick(View view) { - mActionHandler.openRoot(resolveInfo); + mActionHandler.openRoot(resolveInfo, userId); } @Override diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java index d34bceb83..0d83a0f4a 100644 --- a/src/com/android/documentsui/sidebar/RootsFragment.java +++ b/src/com/android/documentsui/sidebar/RootsFragment.java @@ -401,7 +401,6 @@ public class RootsFragment extends Fragment { // for change personal profile root. if (PROFILE_TARGET_ACTIVITY.equals(info.activityInfo.targetActivity)) { if (UserId.CURRENT_USER.equals(userId)) { - getBaseActivity().getDisplayState().canShareAcrossProfile = true; profileItem = new ProfileItem(info, info.loadLabel(pm).toString(), mActionHandler); } @@ -415,6 +414,9 @@ public class RootsFragment extends Fragment { } } + // TODO: refresh UI + getBaseActivity().getDisplayState().canShareAcrossProfile = profileItem != null; + // If there are some providers and apps has the same package name, combine them as one item. for (RootItem rootItem : otherProviders) { final UserPackage userPackage = new UserPackage(rootItem.userId, diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java index 3a74cf9c6..86657fbff 100644 --- a/src/com/android/documentsui/ui/DialogController.java +++ b/src/com/android/documentsui/ui/DialogController.java @@ -46,6 +46,7 @@ public interface DialogController { void showOperationUnsupported(); void showViewInArchivesUnsupported(); void showDocumentsClipped(int size); + void showActionNotAllowed(); /** * Dialogs used when share file count over limit @@ -136,6 +137,13 @@ public interface DialogController { } @Override + public void showActionNotAllowed() { + // Shows as a last resort when a document is not allowed to share across users + Snackbars.makeSnackbar( + mActivity, R.string.toast_action_not_allowed, Snackbar.LENGTH_SHORT).show(); + } + + @Override public void showNoApplicationFound() { Snackbars.makeSnackbar( mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show(); diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java index 53f0792df..40de47931 100644 --- a/tests/common/com/android/documentsui/TestActivity.java +++ b/tests/common/com/android/documentsui/TestActivity.java @@ -18,6 +18,11 @@ package com.android.documentsui; import static junit.framework.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + import android.app.ActivityManager; import android.app.LoaderManager; import android.content.ComponentName; @@ -29,6 +34,7 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.UserHandle; +import android.os.UserManager; import android.test.mock.MockContentResolver; import android.util.Pair; @@ -63,6 +69,7 @@ public abstract class TestActivity extends AbstractBase { public TestLoaderManager loaderManager; public TestSupportLoaderManager supportLoaderManager; public ActivityManager activityManager; + public UserManager userManager; public TestEventListener<Intent> startActivity; public TestEventListener<Pair<Intent, UserHandle>> startActivityAsUser; @@ -100,6 +107,13 @@ public abstract class TestActivity extends AbstractBase { loaderManager = new TestLoaderManager(); supportLoaderManager = new TestSupportLoaderManager(); finishedHandler = new TestEventHandler<>(); + + // Setup some methods which cannot be overridden. + try { + doReturn(this).when(this).createPackageContextAsUser(anyString(), anyInt(), + any()); + } catch (PackageManager.NameNotFoundException e) { + } } @Override @@ -231,6 +245,8 @@ public abstract class TestActivity extends AbstractBase { switch (service) { case Context.ACTIVITY_SERVICE: return activityManager; + case Context.USER_SERVICE: + return userManager; } throw new IllegalArgumentException("Unknown service " + service); diff --git a/tests/common/com/android/documentsui/TestUserIdManager.java b/tests/common/com/android/documentsui/TestUserIdManager.java new file mode 100644 index 000000000..454e13938 --- /dev/null +++ b/tests/common/com/android/documentsui/TestUserIdManager.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import com.android.documentsui.base.UserId; + +import java.util.ArrayList; +import java.util.List; + +public class TestUserIdManager implements UserIdManager { + public List<UserId> userIds = new ArrayList<>(); + public UserId systemUser = null; + public UserId managedUser = null; + + @Override + public List<UserId> getUserIds() { + return userIds; + } + + @Override + public UserId getSystemUser() { + return systemUser; + } + + @Override + public UserId getManagedUser() { + return managedUser; + } +} diff --git a/tests/common/com/android/documentsui/testing/UserManagers.java b/tests/common/com/android/documentsui/testing/UserManagers.java new file mode 100644 index 000000000..5978f8907 --- /dev/null +++ b/tests/common/com/android/documentsui/testing/UserManagers.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.testing; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.os.UserManager; + +import org.mockito.Mockito; + +public class UserManagers { + + private UserManagers() { + } + + public static UserManager create() { + final UserManager um = Mockito.mock(UserManager.class); + when(um.isQuietModeEnabled(any())).thenReturn(false); + return um; + } +} diff --git a/tests/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java index 41acee99e..d1b7d0775 100644 --- a/tests/common/com/android/documentsui/ui/TestDialogController.java +++ b/tests/common/com/android/documentsui/ui/TestDialogController.java @@ -27,6 +27,7 @@ import junit.framework.Assert; public class TestDialogController implements DialogController { private int mFileOpStatus; + private boolean mActionNotAllowed; private boolean mNoApplicationFound; private boolean mDocumentsClipped; private boolean mViewInArchivesUnsupported; @@ -49,6 +50,11 @@ public class TestDialogController implements DialogController { } @Override + public void showActionNotAllowed() { + mActionNotAllowed = true; + } + + @Override public void showNoApplicationFound() { mNoApplicationFound = true; } @@ -87,6 +93,14 @@ public class TestDialogController implements DialogController { Assert.assertEquals(FileOperations.Callback.STATUS_FAILED, mFileOpStatus); } + public void assertActionNotAllowedShown() { + Assert.assertTrue(mActionNotAllowed); + } + + public void assertActionNotAllowedNotShown() { + Assert.assertFalse(mActionNotAllowed); + } + public void assertNoAppFoundShown() { Assert.assertFalse(mNoApplicationFound); } diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java index 3d1e0dbf9..c5c3ac3ca 100644 --- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java @@ -42,6 +42,7 @@ import com.android.documentsui.testing.Roots; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestEventHandler; import com.android.documentsui.testing.TestProvidersAccess; +import com.android.documentsui.testing.UserManagers; import org.junit.Before; import org.junit.Test; @@ -66,6 +67,7 @@ public class AbstractActionHandlerTest { public void setUp() { mEnv = TestEnv.create(); mActivity = TestActivity.create(mEnv); + mActivity.userManager = UserManagers.create(); mHandler = new AbstractActionHandler<TestActivity>( mActivity, mEnv.state, diff --git a/tests/unit/com/android/documentsui/DocumentsAccessTest.java b/tests/unit/com/android/documentsui/DocumentsAccessTest.java new file mode 100644 index 000000000..8a08d431b --- /dev/null +++ b/tests/unit/com/android/documentsui/DocumentsAccessTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static junit.framework.Assert.fail; + +import android.content.pm.PackageManager; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.documentsui.testing.TestEnv; +import com.android.documentsui.testing.TestProvidersAccess; + +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class DocumentsAccessTest { + + private TestActivity mActivity; + private DocumentsAccess mDocumentsAccess; + private TestEnv mEnv; + + @Before + public void setUp() throws PackageManager.NameNotFoundException { + mEnv = TestEnv.create(); + mEnv.reset(); + mActivity = TestActivity.create(mEnv); + mDocumentsAccess = DocumentsAccess.create(mActivity, mEnv.state); + } + + @Test + public void testCreateDocument_noPermission() throws Exception { + try { + mDocumentsAccess.getDocuments(TestProvidersAccess.OtherUser.USER_ID, "authority", + Lists.newArrayList("docId")); + fail("Expects CrossProfileNoPermissionException"); + } catch (CrossProfileNoPermissionException e) { + // expected. + } + } +} diff --git a/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java index 701d22109..aa084e8d1 100644 --- a/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java +++ b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java @@ -16,6 +16,8 @@ package com.android.documentsui; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -135,6 +137,39 @@ public class GlobalSearchLoaderTest { } @Test + public void testSearchResult_includeDirectory_excludedOtherUsers() { + mEnv.state.canShareAcrossProfile = false; + + TestProvidersAccess.DOWNLOADS.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.OtherUser.USER_ID; + TestProvidersAccess.PICKLES.flags |= (DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + + final DocumentInfo currentUserDoc = mEnv.model.createFile( + SEARCH_STRING + "_currentUser.pdf"); + currentUserDoc.lastModified = System.currentTimeMillis(); + mEnv.mockProviders.get(TestProvidersAccess.DOWNLOADS.authority) + .setNextChildDocumentsReturns(currentUserDoc); + + final DocumentInfo otherUserDoc = mEnv.model.createFile(SEARCH_STRING + "_otherUser.png"); + otherUserDoc.lastModified = System.currentTimeMillis(); + mEnv.mockProviders.get(TestProvidersAccess.PICKLES.authority) + .setNextChildDocumentsReturns(otherUserDoc); + + final DirectoryResult result = mLoader.loadInBackground(); + final Cursor c = result.cursor; + + assertThat(c.getCount()).isEqualTo(1); + c.moveToNext(); + final String docName = c.getString(c.getColumnIndex(Document.COLUMN_DISPLAY_NAME)); + assertThat(docName).contains("currentUser"); + + TestProvidersAccess.DOWNLOADS.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH; + } + + @Test public void testSearchResult_includeSearchString() { final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf"); pdfDoc.lastModified = System.currentTimeMillis(); @@ -193,4 +228,102 @@ public class GlobalSearchLoaderTest { TestProvidersAccess.PICKLES.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH | DocumentsContract.Root.FLAG_LOCAL_ONLY); } + + @Test + public void testSearchResult_includeCurrentUserRootOnly() { + mEnv.state.canShareAcrossProfile = false; + mEnv.state.action = State.ACTION_GET_CONTENT; + + final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf"); + pdfDoc.lastModified = System.currentTimeMillis(); + + final DocumentInfo apkDoc = mEnv.model.createFile(SEARCH_STRING + ".apk"); + apkDoc.lastModified = System.currentTimeMillis(); + + final DocumentInfo testApkDoc = mEnv.model.createFile("test.apk"); + testApkDoc.lastModified = System.currentTimeMillis(); + + mEnv.mockProviders.get(TestProvidersAccess.PICKLES.authority) + .setNextChildDocumentsReturns(pdfDoc, apkDoc, testApkDoc); + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.OtherUser.USER_ID; + + TestProvidersAccess.PICKLES.flags |= (DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + mEnv.state.sortModel.sortByUser( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING); + + final DirectoryResult result = mLoader.loadInBackground(); + final Cursor c = result.cursor; + + assertEquals(1, c.getCount()); + + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + } + + + @Test + public void testSearchResult_includeBothUsersRoots() { + mEnv.state.canShareAcrossProfile = true; + mEnv.state.action = State.ACTION_GET_CONTENT; + + final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf"); + pdfDoc.lastModified = System.currentTimeMillis(); + + final DocumentInfo apkDoc = mEnv.model.createFile(SEARCH_STRING + ".apk"); + apkDoc.lastModified = System.currentTimeMillis(); + + final DocumentInfo testApkDoc = mEnv.model.createFile("test.apk"); + testApkDoc.lastModified = System.currentTimeMillis(); + + mEnv.mockProviders.get(TestProvidersAccess.PICKLES.authority) + .setNextChildDocumentsReturns(pdfDoc, apkDoc, testApkDoc); + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.OtherUser.USER_ID; + + TestProvidersAccess.PICKLES.flags |= (DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + mEnv.state.sortModel.sortByUser( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING); + + final DirectoryResult result = mLoader.loadInBackground(); + final Cursor c = result.cursor; + + assertEquals(3, c.getCount()); + + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + } + + + @Test + public void testSearchResult_emptyCurrentUsersRoot() { + mEnv.state.canShareAcrossProfile = false; + mEnv.state.action = State.ACTION_GET_CONTENT; + + final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf"); + pdfDoc.lastModified = System.currentTimeMillis(); + + mEnv.mockProviders.get(TestProvidersAccess.PICKLES.authority) + .setNextChildDocumentsReturns(pdfDoc); + + TestProvidersAccess.DOWNLOADS.userId = TestProvidersAccess.OtherUser.USER_ID; + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.OtherUser.USER_ID; + TestProvidersAccess.PICKLES.flags |= (DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + mEnv.state.sortModel.sortByUser( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING); + + final DirectoryResult result = mLoader.loadInBackground(); + assertThat(result.cursor.getCount()).isEqualTo(0); + // We don't expect exception even if all roots are from other users. + assertThat(result.exception).isNull(); + + + TestProvidersAccess.DOWNLOADS.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.userId = TestProvidersAccess.USER_ID; + TestProvidersAccess.PICKLES.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH + | DocumentsContract.Root.FLAG_LOCAL_ONLY); + } } diff --git a/tests/unit/com/android/documentsui/PickActivityTest.java b/tests/unit/com/android/documentsui/PickActivityTest.java new file mode 100644 index 000000000..d635540cc --- /dev/null +++ b/tests/unit/com/android/documentsui/PickActivityTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static com.android.documentsui.base.Providers.AUTHORITY_STORAGE; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; +import android.provider.DocumentsContract; + +import androidx.test.filters.SmallTest; +import androidx.test.rule.ActivityTestRule; + +import com.android.documentsui.base.DocumentInfo; +import com.android.documentsui.picker.PickActivity; +import com.android.documentsui.testing.TestProvidersAccess; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +@SmallTest +public class PickActivityTest { + + private Intent intentGetContent; + + @Rule + public final ActivityTestRule<PickActivity> mRule = + new ActivityTestRule<>(PickActivity.class, false, false); + + @Before + public void setUp() throws Exception { + intentGetContent = new Intent(Intent.ACTION_GET_CONTENT); + intentGetContent.addCategory(Intent.CATEGORY_OPENABLE); + intentGetContent.setType("*/*"); + Uri hintUri = DocumentsContract.buildRootUri(AUTHORITY_STORAGE, "primary"); + intentGetContent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, hintUri); + } + + @Test + public void testOnDocumentPicked() { + DocumentInfo doc = new DocumentInfo(); + doc.userId = TestProvidersAccess.USER_ID; + doc.authority = "authority"; + doc.documentId = "documentId"; + + PickActivity pickActivity = mRule.launchActivity(intentGetContent); + pickActivity.mState.canShareAcrossProfile = true; + pickActivity.onDocumentPicked(doc); + SystemClock.sleep(3000); + + Instrumentation.ActivityResult result = mRule.getActivityResult(); + assertThat(pickActivity.isFinishing()).isTrue(); + assertThat(result.getResultCode()).isEqualTo(Activity.RESULT_OK); + assertThat(result.getResultData().getData()).isEqualTo(doc.getDocumentUri()); + } + + @Test + public void testOnDocumentPicked_otherUser() { + DocumentInfo doc = new DocumentInfo(); + doc.userId = TestProvidersAccess.OtherUser.USER_ID; + doc.authority = "authority"; + doc.documentId = "documentId"; + + PickActivity pickActivity = mRule.launchActivity(intentGetContent); + pickActivity.mState.canShareAcrossProfile = true; + pickActivity.onDocumentPicked(doc); + SystemClock.sleep(3000); + + Instrumentation.ActivityResult result = mRule.getActivityResult(); + assertThat(result.getResultCode()).isEqualTo(Activity.RESULT_OK); + assertThat(result.getResultData().getData()).isEqualTo(doc.getDocumentUri()); + } + + @Test + public void testOnDocumentPicked_otherUserDoesNotReturn() { + DocumentInfo doc = new DocumentInfo(); + doc.userId = TestProvidersAccess.OtherUser.USER_ID; + doc.authority = "authority"; + doc.documentId = "documentId"; + + PickActivity pickActivity = mRule.launchActivity(intentGetContent); + pickActivity.mState.canShareAcrossProfile = false; + pickActivity.onDocumentPicked(doc); + SystemClock.sleep(3000); + + assertThat(pickActivity.isFinishing()).isFalse(); + } +} diff --git a/tests/unit/com/android/documentsui/ProfileTabsTest.java b/tests/unit/com/android/documentsui/ProfileTabsTest.java index dc8f0f9cb..5330e9c95 100644 --- a/tests/unit/com/android/documentsui/ProfileTabsTest.java +++ b/tests/unit/com/android/documentsui/ProfileTabsTest.java @@ -37,9 +37,7 @@ import com.google.android.material.tabs.TabLayout; import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; public class ProfileTabsTest { @@ -233,26 +231,5 @@ public class ProfileTabsTest { } } - - private static class TestUserIdManager implements UserIdManager { - List<UserId> userIds = new ArrayList<>(); - UserId systemUser = null; - UserId managedUser = null; - - @Override - public List<UserId> getUserIds() { - return userIds; - } - - @Override - public UserId getSystemUser() { - return systemUser; - } - - @Override - public UserId getManagedUser() { - return managedUser; - } - } } diff --git a/tests/unit/com/android/documentsui/RecentsLoaderTests.java b/tests/unit/com/android/documentsui/RecentsLoaderTests.java index 85ab7429f..a1b96ee1f 100644 --- a/tests/unit/com/android/documentsui/RecentsLoaderTests.java +++ b/tests/unit/com/android/documentsui/RecentsLoaderTests.java @@ -16,10 +16,15 @@ package com.android.documentsui; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import android.database.Cursor; import android.provider.DocumentsContract.Document; @@ -35,6 +40,7 @@ import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestFileTypeLookup; import com.android.documentsui.testing.TestImmediateExecutor; import com.android.documentsui.testing.TestProvidersAccess; +import com.android.documentsui.testing.UserManagers; import org.junit.Before; import org.junit.Test; @@ -57,9 +63,11 @@ public class RecentsLoaderTests { mEnv = TestEnv.create(); mActivity = TestActivity.create(mEnv); mActivity.activityManager = ActivityManagers.create(false); + mActivity.userManager = UserManagers.create(); mEnv.state.action = State.ACTION_BROWSE; mEnv.state.acceptMimes = new String[] { "*/*" }; + mEnv.state.canShareAcrossProfile = true; mLoader = new RecentsLoader(mActivity, mEnv.providers, mEnv.state, TestImmediateExecutor.createLookup(), new TestFileTypeLookup(), @@ -141,4 +149,25 @@ public class RecentsLoaderTests { latch.await(1, TimeUnit.SECONDS); assertTrue(mContentChanged); } + + @Test + public void testLoaderOnUserWithoutPermission() { + mEnv.state.canShareAcrossProfile = false; + mLoader = new RecentsLoader(mActivity, mEnv.providers, mEnv.state, + TestImmediateExecutor.createLookup(), new TestFileTypeLookup(), + TestProvidersAccess.OtherUser.USER_ID); + final DirectoryResult result = mLoader.loadInBackground(); + + assertThat(result.cursor).isNull(); + assertThat(result.exception).isInstanceOf(CrossProfileNoPermissionException.class); + } + + @Test + public void testLoaderOnUser_quietMode() { + when(mActivity.userManager.isQuietModeEnabled(any())).thenReturn(true); + final DirectoryResult result = mLoader.loadInBackground(); + + assertThat(result.cursor).isNull(); + assertThat(result.exception).isInstanceOf(CrossProfileQuietModeException.class); + } } diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java index 45a0740f2..d7bdbd1ec 100644 --- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java @@ -68,6 +68,7 @@ import com.android.documentsui.testing.TestDragAndDropManager; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestFeatures; import com.android.documentsui.testing.TestProvidersAccess; +import com.android.documentsui.testing.UserManagers; import com.android.documentsui.ui.TestDialogController; import org.junit.Before; @@ -97,6 +98,7 @@ public class ActionHandlerTest { mFeatures = new TestFeatures(); mEnv = TestEnv.create(mFeatures); mActivity = TestActivity.create(mEnv); + mActivity.userManager = UserManagers.create(); mActionModeAddons = new TestActionModeAddons(); mDialogs = new TestDialogController(); mClipper = new TestDocumentClipper(); diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java index 57d4df7f5..3098bbb7d 100644 --- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java @@ -16,6 +16,8 @@ package com.android.documentsui.picker; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; @@ -37,6 +39,8 @@ import com.android.documentsui.AbstractActionHandler; import com.android.documentsui.DocumentsAccess; import com.android.documentsui.Injector; import com.android.documentsui.R; +import com.android.documentsui.TestUserIdManager; +import com.android.documentsui.UserIdManager; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Lookup; import com.android.documentsui.base.RootInfo; @@ -69,6 +73,7 @@ public class ActionHandlerTest { private TestableActionHandler<TestActivity> mHandler; private TestLastAccessedStorage mLastAccessed; private PickCountRecordStorage mPickCountRecord; + private TestUserIdManager mTestUserIdManager; @Before public void setUp() { @@ -77,6 +82,7 @@ public class ActionHandlerTest { mEnv.providers.configurePm(mActivity.packageMgr); mEnv.injector.pickResult = new PickResult(); mLastAccessed = new TestLastAccessedStorage(); + mTestUserIdManager = new TestUserIdManager(); mPickCountRecord = mock(PickCountRecordStorage.class); mHandler = new TestableActionHandler<>( @@ -88,7 +94,8 @@ public class ActionHandlerTest { mEnv::lookupExecutor, mEnv.injector, mLastAccessed, - mPickCountRecord + mPickCountRecord, + mTestUserIdManager ); mEnv.selectionMgr.select("1"); @@ -102,18 +109,20 @@ public class ActionHandlerTest { private UpdatePickResultTask mTask; TestableActionHandler( - T activity, - State state, - ProvidersAccess providers, - DocumentsAccess docs, - SearchViewManager searchMgr, - Lookup<String, Executor> executors, - Injector injector, - LastAccessedStorage lastAccessed, - PickCountRecordStorage pickCountRecordStorage) { - super(activity, state, providers, docs, searchMgr, executors, injector, lastAccessed); + T activity, + State state, + ProvidersAccess providers, + DocumentsAccess docs, + SearchViewManager searchMgr, + Lookup<String, Executor> executors, + Injector injector, + LastAccessedStorage lastAccessed, + PickCountRecordStorage pickCountRecordStorage, + UserIdManager userIdManager) { + super(activity, state, providers, docs, searchMgr, executors, injector, lastAccessed, + userIdManager); mTask = new UpdatePickResultTask( - mActivity, mInjector.pickResult, pickCountRecordStorage); + mActivity, mInjector.pickResult, pickCountRecordStorage); } @Override @@ -519,6 +528,37 @@ public class ActionHandlerTest { } @Test + public void testOnAppPickedResult_OnOK_crossProfile() throws Exception { + mEnv.state.canShareAcrossProfile = true; + mTestUserIdManager.managedUser = TestProvidersAccess.OtherUser.USER_ID; + mTestUserIdManager.systemUser = TestProvidersAccess.USER_ID; + + Intent intent = new Intent(); + mHandler.onActivityResult(AbstractActionHandler.CODE_FORWARD_CROSS_PROFILE, + Activity.RESULT_OK, intent); + mActivity.finishedHandler.assertCalled(); + mActivity.setResult.assertCalled(); + + assertEquals(Activity.RESULT_OK, (long) mActivity.setResult.getLastValue().first); + assertEquals(intent, mActivity.setResult.getLastValue().second); + mEnv.dialogs.assertActionNotAllowedNotShown(); + } + + @Test + public void testOnAppPickedResult_OnOK_crossProfile_withoutPermission() throws Exception { + mEnv.state.canShareAcrossProfile = false; + mTestUserIdManager.managedUser = TestProvidersAccess.OtherUser.USER_ID; + mTestUserIdManager.systemUser = TestProvidersAccess.USER_ID; + + Intent intent = new Intent(); + mHandler.onActivityResult(AbstractActionHandler.CODE_FORWARD_CROSS_PROFILE, + Activity.RESULT_OK, intent); + mActivity.finishedHandler.assertNotCalled(); + mActivity.setResult.assertNotCalled(); + mEnv.dialogs.assertActionNotAllowedShown(); + } + + @Test public void testOnAppPickedResult_OnNotOK() throws Exception { Intent intent = new Intent(); mHandler.onActivityResult(0, Activity.RESULT_OK, intent); @@ -532,8 +572,18 @@ public class ActionHandlerTest { } @Test + public void testOnAppPickedResult_OnNotOK_crossProfile() throws Exception { + Intent intent = new Intent(); + mHandler.onActivityResult(AbstractActionHandler.CODE_FORWARD_CROSS_PROFILE, + Activity.RESULT_CANCELED, + intent); + mActivity.finishedHandler.assertNotCalled(); + mActivity.setResult.assertNotCalled(); + } + + @Test public void testOpenAppRoot() throws Exception { - mHandler.openRoot(TestResolveInfo.create()); + mHandler.openRoot(TestResolveInfo.create(), TestProvidersAccess.USER_ID); assertEquals((long) mActivity.startActivityForResult.getLastValue().second, AbstractActionHandler.CODE_FORWARD); assertNotNull(mActivity.startActivityForResult.getLastValue().first); @@ -546,7 +596,7 @@ public class ActionHandlerTest { | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - mHandler.openRoot(TestResolveInfo.create()); + mHandler.openRoot(TestResolveInfo.create(), TestProvidersAccess.USER_ID); assertEquals((long) mActivity.startActivityForResult.getLastValue().second, AbstractActionHandler.CODE_FORWARD); assertNotNull(mActivity.startActivityForResult.getLastValue().first); @@ -563,10 +613,31 @@ public class ActionHandlerTest { public void testOpenAppRootWithQueryContent_matchedContent() throws Exception { final String queryContent = "query"; mActivity.intent.putExtra(Intent.EXTRA_CONTENT_QUERY, queryContent); - mHandler.openRoot(TestResolveInfo.create()); + mHandler.openRoot(TestResolveInfo.create(), TestProvidersAccess.USER_ID); + assertEquals(queryContent, + mActivity.startActivityForResult.getLastValue().first.getStringExtra( + Intent.EXTRA_CONTENT_QUERY)); + } + + @Test + public void testOpenAppRoot_doesNotHappen_differentUser() throws Exception { + final String queryContent = "query"; + mActivity.intent.putExtra(Intent.EXTRA_CONTENT_QUERY, queryContent); + mHandler.openRoot(TestResolveInfo.create(), TestProvidersAccess.OtherUser.USER_ID); + assertThat(mActivity.startActivityForResult.getLastValue()).isNull(); + mEnv.dialogs.assertActionNotAllowedShown(); + } + + @Test + public void testOpenAppRoot_happenWithPermission_differentUser() throws Exception { + final String queryContent = "query"; + mEnv.state.canShareAcrossProfile = true; + mActivity.intent.putExtra(Intent.EXTRA_CONTENT_QUERY, queryContent); + mHandler.openRoot(TestResolveInfo.create(), TestProvidersAccess.OtherUser.USER_ID); assertEquals(queryContent, mActivity.startActivityForResult.getLastValue().first.getStringExtra( Intent.EXTRA_CONTENT_QUERY)); + mEnv.dialogs.assertActionNotAllowedNotShown(); } @Test |