diff options
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 |