| /* |
| * Copyright (C) 2016 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.DocumentInfo.getCursorInt; |
| import static com.android.documentsui.base.DocumentInfo.getCursorString; |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; |
| |
| import android.app.PendingIntent; |
| import android.content.ActivityNotFoundException; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.provider.DocumentsContract; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.DragEvent; |
| |
| import androidx.annotation.VisibleForTesting; |
| import androidx.fragment.app.FragmentActivity; |
| import androidx.loader.app.LoaderManager.LoaderCallbacks; |
| import androidx.loader.content.Loader; |
| import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; |
| import androidx.recyclerview.selection.MutableSelection; |
| import androidx.recyclerview.selection.SelectionTracker; |
| |
| import com.android.documentsui.AbstractActionHandler.CommonAddons; |
| import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; |
| import com.android.documentsui.base.BooleanConsumer; |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.DocumentStack; |
| import com.android.documentsui.base.Lookup; |
| import com.android.documentsui.base.MimeTypes; |
| import com.android.documentsui.base.Providers; |
| import com.android.documentsui.base.RootInfo; |
| import com.android.documentsui.base.Shared; |
| import com.android.documentsui.base.State; |
| import com.android.documentsui.base.UserId; |
| import com.android.documentsui.dirlist.AnimationView; |
| import com.android.documentsui.dirlist.AnimationView.AnimationType; |
| import com.android.documentsui.dirlist.FocusHandler; |
| import com.android.documentsui.files.LauncherActivity; |
| import com.android.documentsui.files.QuickViewIntentBuilder; |
| import com.android.documentsui.queries.SearchViewManager; |
| import com.android.documentsui.roots.GetRootDocumentTask; |
| import com.android.documentsui.roots.LoadFirstRootTask; |
| import com.android.documentsui.roots.LoadRootTask; |
| import com.android.documentsui.roots.ProvidersAccess; |
| import com.android.documentsui.sidebar.EjectRootTask; |
| import com.android.documentsui.sorting.SortListFragment; |
| import com.android.documentsui.ui.DialogController; |
| import com.android.documentsui.ui.Snackbars; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Semaphore; |
| import java.util.function.Consumer; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Provides support for specializing the actions (openDocument etc.) to the host activity. |
| */ |
| public abstract class AbstractActionHandler<T extends FragmentActivity & CommonAddons> |
| implements ActionHandler { |
| |
| @VisibleForTesting |
| public static final int CODE_AUTHENTICATION = 43; |
| |
| @VisibleForTesting |
| static final int LOADER_ID = 42; |
| |
| private static final String TAG = "AbstractActionHandler"; |
| private static final int REFRESH_SPINNER_TIMEOUT = 500; |
| private final Semaphore mLoaderSemaphore = new Semaphore(1); |
| |
| protected final T mActivity; |
| protected final State mState; |
| protected final ProvidersAccess mProviders; |
| protected final DocumentsAccess mDocs; |
| protected final FocusHandler mFocusHandler; |
| protected final SelectionTracker<String> mSelectionMgr; |
| protected final SearchViewManager mSearchMgr; |
| protected final Lookup<String, Executor> mExecutors; |
| protected final DialogController mDialogs; |
| protected final Model mModel; |
| protected final Injector<?> mInjector; |
| |
| private final LoaderBindings mBindings; |
| |
| private Runnable mDisplayStateChangedListener; |
| |
| private ContentLock mContentLock; |
| |
| @Override |
| public void registerDisplayStateChangedListener(Runnable l) { |
| mDisplayStateChangedListener = l; |
| } |
| @Override |
| public void unregisterDisplayStateChangedListener(Runnable l) { |
| if (mDisplayStateChangedListener == l) { |
| mDisplayStateChangedListener = null; |
| } |
| } |
| |
| public AbstractActionHandler( |
| T activity, |
| State state, |
| ProvidersAccess providers, |
| DocumentsAccess docs, |
| SearchViewManager searchMgr, |
| Lookup<String, Executor> executors, |
| Injector<?> injector) { |
| |
| assert(activity != null); |
| assert(state != null); |
| assert(providers != null); |
| assert(searchMgr != null); |
| assert(docs != null); |
| assert(injector != null); |
| |
| mActivity = activity; |
| mState = state; |
| mProviders = providers; |
| mDocs = docs; |
| mFocusHandler = injector.focusManager; |
| mSelectionMgr = injector.selectionMgr; |
| mSearchMgr = searchMgr; |
| mExecutors = executors; |
| mDialogs = injector.dialogs; |
| mModel = injector.getModel(); |
| mInjector = injector; |
| |
| mBindings = new LoaderBindings(); |
| } |
| |
| @Override |
| public void ejectRoot(RootInfo root, BooleanConsumer listener) { |
| new EjectRootTask( |
| mActivity.getContentResolver(), |
| root.authority, |
| root.rootId, |
| listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority)); |
| } |
| |
| @Override |
| public void startAuthentication(PendingIntent intent) { |
| try { |
| mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION, |
| null, 0, 0, 0); |
| } catch (IntentSender.SendIntentException cancelled) { |
| Log.d(TAG, "Authentication Pending Intent either canceled or ignored."); |
| } |
| } |
| |
| @Override |
| public void requestQuietModeDisabled(RootInfo info, UserId userId) { |
| new RequestQuietModeDisabledTask(mActivity, userId).execute(); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| switch (requestCode) { |
| case CODE_AUTHENTICATION: |
| onAuthenticationResult(resultCode); |
| break; |
| } |
| } |
| |
| private void onAuthenticationResult(int resultCode) { |
| if (resultCode == FragmentActivity.RESULT_OK) { |
| Log.v(TAG, "Authentication was successful. Refreshing directory now."); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } |
| } |
| |
| @Override |
| public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) { |
| GetRootDocumentTask task = new GetRootDocumentTask( |
| root, |
| mActivity, |
| timeout, |
| mDocs, |
| callback); |
| |
| task.executeOnExecutor(mExecutors.lookup(root.authority)); |
| } |
| |
| @Override |
| public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) { |
| RefreshTask task = new RefreshTask( |
| mInjector.features, |
| mState, |
| doc, |
| REFRESH_SPINNER_TIMEOUT, |
| mActivity.getApplicationContext(), |
| mActivity::isDestroyed, |
| callback); |
| task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority)); |
| } |
| |
| @Override |
| public void openSelectedInNewWindow() { |
| throw new UnsupportedOperationException("Can't open in new window."); |
| } |
| |
| @Override |
| public void openInNewWindow(DocumentStack path) { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_NEW_WINDOW); |
| |
| Intent intent = LauncherActivity.createLaunchIntent(mActivity); |
| intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path); |
| |
| // Multi-window necessitates we pick how we are launched. |
| // By default we'd be launched in-place above the existing app. |
| // By setting launch-to-side ActivityManager will open us to side. |
| if (mActivity.isInMultiWindowMode()) { |
| intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); |
| } |
| |
| mActivity.startActivity(intent); |
| } |
| |
| @Override |
| public boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) { |
| throw new UnsupportedOperationException("Can't open document."); |
| } |
| |
| @Override |
| public void showInspector(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Can't open properties."); |
| } |
| |
| @Override |
| public void springOpenDirectory(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Can't spring open directories."); |
| } |
| |
| @Override |
| public void openSettings(RootInfo root) { |
| throw new UnsupportedOperationException("Can't open settings."); |
| } |
| |
| @Override |
| public void openRoot(ResolveInfo app, UserId userId) { |
| throw new UnsupportedOperationException("Can't open an app."); |
| } |
| |
| @Override |
| public void showAppDetails(ResolveInfo info, UserId userId) { |
| throw new UnsupportedOperationException("Can't show app details."); |
| } |
| |
| @Override |
| public boolean dropOn(DragEvent event, RootInfo root) { |
| throw new UnsupportedOperationException("Can't open an app."); |
| } |
| |
| @Override |
| public void pasteIntoFolder(RootInfo root) { |
| throw new UnsupportedOperationException("Can't paste into folder."); |
| } |
| |
| @Override |
| public void viewInOwner() { |
| throw new UnsupportedOperationException("Can't view in application."); |
| } |
| |
| @Override |
| public void selectAllFiles() { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_SELECT_ALL); |
| Model model = mInjector.getModel(); |
| |
| // Exclude disabled files |
| List<String> enabled = new ArrayList<>(); |
| for (String id : model.getModelIds()) { |
| Cursor cursor = model.getItem(id); |
| if (cursor == null) { |
| Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id); |
| continue; |
| } |
| String docMimeType = getCursorString( |
| cursor, DocumentsContract.Document.COLUMN_MIME_TYPE); |
| int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS); |
| if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) { |
| enabled.add(id); |
| } |
| } |
| |
| // Only select things currently visible in the adapter. |
| boolean changed = mSelectionMgr.setItemsSelected(enabled, true); |
| if (changed) { |
| mDisplayStateChangedListener.run(); |
| } |
| } |
| |
| @Override |
| public void deselectAllFiles() { |
| mSelectionMgr.clearSelection(); |
| } |
| |
| @Override |
| public void showCreateDirectoryDialog() { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_CREATE_DIR); |
| |
| CreateDirectoryFragment.show(mActivity.getSupportFragmentManager()); |
| } |
| |
| @Override |
| public void showSortDialog() { |
| SortListFragment.show(mActivity.getSupportFragmentManager(), mState.sortModel); |
| } |
| |
| @Override |
| @Nullable |
| public DocumentInfo renameDocument(String name, DocumentInfo document) { |
| throw new UnsupportedOperationException("Can't rename documents."); |
| } |
| |
| @Override |
| public void showChooserForDoc(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Show chooser for doc not supported!"); |
| } |
| |
| @Override |
| public void openRootDocument(@Nullable DocumentInfo rootDoc) { |
| if (rootDoc == null) { |
| // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root |
| // document. Either case we should call refreshCurrentRootAndDirectory() to let |
| // DirectoryFragment update UI. |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } else { |
| openContainerDocument(rootDoc); |
| } |
| } |
| |
| @Override |
| public void openContainerDocument(DocumentInfo doc) { |
| assert(doc.isContainer()); |
| |
| if (mSearchMgr.isSearching()) { |
| loadDocument( |
| doc.derivedUri, |
| doc.userId, |
| (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); |
| } else { |
| openChildContainer(doc); |
| } |
| } |
| |
| // TODO: Make this private and make tests call interface method instead. |
| /** |
| * Behavior when a document is opened. |
| */ |
| @VisibleForTesting |
| public void onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback, |
| boolean fromPicker) { |
| // In picker mode, don't access archive container to avoid pick file in archive files. |
| if (doc.isContainer() && !fromPicker) { |
| openContainerDocument(doc); |
| return; |
| } |
| |
| if (manageDocument(doc)) { |
| return; |
| } |
| |
| // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow |
| // PackageManager to install it. This allows users to install APKs from any root. |
| // The Downloads special case is handled above in #manageDocument. |
| if (MimeTypes.isApkType(doc.mimeType)) { |
| viewDocument(doc); |
| return; |
| } |
| |
| switch (type) { |
| case VIEW_TYPE_REGULAR: |
| if (viewDocument(doc)) { |
| return; |
| } |
| break; |
| |
| case VIEW_TYPE_PREVIEW: |
| if (previewDocument(doc, fromPicker)) { |
| return; |
| } |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Illegal view type."); |
| } |
| |
| switch (fallback) { |
| case VIEW_TYPE_REGULAR: |
| if (viewDocument(doc)) { |
| return; |
| } |
| break; |
| |
| case VIEW_TYPE_PREVIEW: |
| if (previewDocument(doc, fromPicker)) { |
| return; |
| } |
| break; |
| |
| case VIEW_TYPE_NONE: |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Illegal fallback view type."); |
| } |
| |
| // Failed to view including fallback, and it's in an archive. |
| if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) { |
| mDialogs.showViewInArchivesUnsupported(); |
| } |
| } |
| |
| private boolean viewDocument(DocumentInfo doc) { |
| if (doc.isPartial()) { |
| Log.w(TAG, "Can't view partial file."); |
| return false; |
| } |
| |
| if (doc.isInArchive()) { |
| Log.w(TAG, "Can't view files in archives."); |
| return false; |
| } |
| |
| if (doc.isDirectory()) { |
| Log.w(TAG, "Can't view directories."); |
| return true; |
| } |
| |
| Intent intent = buildViewIntent(doc); |
| if (DEBUG && intent.getClipData() != null) { |
| Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData()); |
| } |
| |
| try { |
| doc.userId.startActivityAsUser(mActivity, intent); |
| return true; |
| } catch (ActivityNotFoundException e) { |
| mDialogs.showNoApplicationFound(); |
| } |
| return false; |
| } |
| |
| private boolean previewDocument(DocumentInfo doc, boolean fromPicker) { |
| if (doc.isPartial()) { |
| Log.w(TAG, "Can't view partial file."); |
| return false; |
| } |
| |
| Intent intent = new QuickViewIntentBuilder( |
| mActivity, |
| mActivity.getResources(), |
| doc, |
| mModel, |
| fromPicker).build(); |
| |
| if (intent != null) { |
| // TODO: un-work around issue b/24963914. Should be fixed soon. |
| try { |
| doc.userId.startActivityAsUser(mActivity, intent); |
| return true; |
| } catch (SecurityException e) { |
| // Carry on to regular view mode. |
| Log.e(TAG, "Caught security error: " + e.getLocalizedMessage()); |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| protected boolean manageDocument(DocumentInfo doc) { |
| if (isManagedDownload(doc)) { |
| // First try managing the document; we expect manager to filter |
| // based on authority, so we don't grant. |
| Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); |
| manage.setData(doc.getDocumentUri()); |
| try { |
| doc.userId.startActivityAsUser(mActivity, manage); |
| return true; |
| } catch (ActivityNotFoundException ex) { |
| // Fall back to regular handling. |
| } |
| } |
| |
| return false; |
| } |
| |
| private boolean isManagedDownload(DocumentInfo doc) { |
| // Anything on downloads goes through the back through downloads manager |
| // (that's the MANAGE_DOCUMENT bit). |
| // This is done for two reasons: |
| // 1) The file in question might be a failed/queued or otherwise have some |
| // specialized download handling. |
| // 2) For APKs, the download manager will add on some important security stuff |
| // like origin URL. |
| // 3) For partial files, the download manager will offer to restart/retry downloads. |
| |
| // All other files not on downloads, event APKs, would get no benefit from this |
| // treatment, thusly the "isDownloads" check. |
| |
| // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for |
| // files in archives or in child folders. Also, if the activity is already browsing |
| // a ZIP from downloads, then skip MANAGE_DOCUMENTS. |
| if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction()) |
| && mState.stack.size() > 1) { |
| // viewing the contents of an archive. |
| return false; |
| } |
| |
| // management is only supported in Downloads root or downloaded files show in Recent root. |
| if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) { |
| // only on APKs or partial files. |
| return MimeTypes.isApkType(doc.mimeType) || doc.isPartial(); |
| } |
| |
| return false; |
| } |
| |
| protected Intent buildViewIntent(DocumentInfo doc) { |
| Intent intent = new Intent(Intent.ACTION_VIEW); |
| intent.setDataAndType(doc.getDocumentUri(), doc.mimeType); |
| |
| // Downloads has traditionally added the WRITE permission |
| // in the TrampolineActivity. Since this behavior is long |
| // established, we set the same permission for non-managed files |
| // This ensures consistent behavior between the Downloads root |
| // and other roots. |
| int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP; |
| if (doc.isWriteSupported()) { |
| flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; |
| } |
| intent.setFlags(flags); |
| |
| return intent; |
| } |
| |
| @Override |
| public boolean previewItem(ItemDetails<String> doc) { |
| throw new UnsupportedOperationException("Can't handle preview."); |
| } |
| |
| private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) { |
| if (stack == null) { |
| mState.stack.popToRootDocument(); |
| |
| // Update navigator to give horizontal breadcrumb a chance to update documents. It |
| // doesn't update its content if the size of document stack doesn't change. |
| // TODO: update breadcrumb to take range update. |
| mActivity.updateNavigator(); |
| |
| mState.stack.push(doc); |
| } else { |
| if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) { |
| // It is now possible when opening cross-profile folder. |
| Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected " |
| + mState.stack.getRoot()); |
| } |
| |
| final DocumentInfo top = stack.peek(); |
| if (top.isArchive()) { |
| // Swap the zip file in original provider and the one provided by ArchiveProvider. |
| stack.pop(); |
| stack.push(mDocs.getArchiveDocument(top.derivedUri, top.userId)); |
| } |
| |
| mState.stack.reset(); |
| // Update navigator to give horizontal breadcrumb a chance to update documents. It |
| // doesn't update its content if the size of document stack doesn't change. |
| // TODO: update breadcrumb to take range update. |
| mActivity.updateNavigator(); |
| |
| mState.stack.reset(stack); |
| } |
| |
| // Show an opening animation only if pressing "back" would get us back to the |
| // previous directory. Especially after opening a root document, pressing |
| // back, wouldn't go to the previous root, but close the activity. |
| final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) |
| ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; |
| mActivity.refreshCurrentRootAndDirectory(anim); |
| } |
| |
| private void openChildContainer(DocumentInfo doc) { |
| DocumentInfo currentDoc = null; |
| |
| if (doc.isDirectory()) { |
| // Regular directory. |
| currentDoc = doc; |
| } else if (doc.isArchive()) { |
| // Archive. |
| currentDoc = mDocs.getArchiveDocument(doc.derivedUri, doc.userId); |
| } |
| |
| assert(currentDoc != null); |
| if (currentDoc.equals(mState.stack.peek())) { |
| Log.w(TAG, "This DocumentInfo is already in current DocumentsStack"); |
| return; |
| } |
| |
| mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); |
| |
| mState.stack.push(currentDoc); |
| // Show an opening animation only if pressing "back" would get us back to the |
| // previous directory. Especially after opening a root document, pressing |
| // back, wouldn't go to the previous root, but close the activity. |
| final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) |
| ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; |
| mActivity.refreshCurrentRootAndDirectory(anim); |
| } |
| |
| @Override |
| public void setDebugMode(boolean enabled) { |
| if (!mInjector.features.isDebugSupportEnabled()) { |
| return; |
| } |
| |
| mState.debugMode = enabled; |
| mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled); |
| mInjector.features.forceFeature(R.bool.feature_inspector, enabled); |
| mActivity.invalidateOptionsMenu(); |
| |
| if (enabled) { |
| showDebugMessage(); |
| } else { |
| mActivity.getWindow().setStatusBarColor( |
| mActivity.getResources().getColor(R.color.app_background_color)); |
| } |
| } |
| |
| @Override |
| public void showDebugMessage() { |
| assert (mInjector.features.isDebugSupportEnabled()); |
| |
| int[] colors = mInjector.debugHelper.getNextColors(); |
| Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage(); |
| |
| Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second); |
| |
| mActivity.getWindow().setStatusBarColor(colors[1]); |
| } |
| |
| @Override |
| public void switchLauncherIcon() { |
| PackageManager pm = mActivity.getPackageManager(); |
| if (pm != null) { |
| final boolean enalbled = Shared.isLauncherEnabled(mActivity); |
| ComponentName component = new ComponentName( |
| mActivity.getPackageName(), Shared.LAUNCHER_TARGET_CLASS); |
| pm.setComponentEnabledSetting(component, enalbled |
| ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED |
| : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, |
| PackageManager.DONT_KILL_APP); |
| } |
| } |
| |
| @Override |
| public void cutToClipboard() { |
| throw new UnsupportedOperationException("Cut not supported!"); |
| } |
| |
| @Override |
| public void copyToClipboard() { |
| throw new UnsupportedOperationException("Copy not supported!"); |
| } |
| |
| @Override |
| public void showDeleteDialog() { |
| throw new UnsupportedOperationException("Delete not supported!"); |
| } |
| |
| @Override |
| public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) { |
| throw new UnsupportedOperationException("Delete not supported!"); |
| } |
| |
| @Override |
| public void shareSelectedDocuments() { |
| throw new UnsupportedOperationException("Share not supported!"); |
| } |
| |
| protected final void loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback) { |
| new LoadDocStackTask( |
| mActivity, |
| mProviders, |
| mDocs, |
| userId, |
| callback |
| ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); |
| } |
| |
| @Override |
| public final void loadRoot(Uri uri, UserId userId) { |
| new LoadRootTask<>(mActivity, mProviders, uri, userId, this::onRootLoaded) |
| .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); |
| } |
| |
| @Override |
| public final void loadCrossProfileRoot(RootInfo info, UserId selectedUser) { |
| if (info.isRecents()) { |
| openRoot(mProviders.getRecentsRoot(selectedUser)); |
| return; |
| } |
| new LoadRootTask<>(mActivity, mProviders, info.getUri(), selectedUser, |
| new LoadCrossProfileRootCallback(info, selectedUser)) |
| .executeOnExecutor(mExecutors.lookup(info.getUri().getAuthority())); |
| } |
| |
| private class LoadCrossProfileRootCallback implements LoadRootTask.LoadRootCallback { |
| private final RootInfo mOriginalRoot; |
| private final UserId mSelectedUserId; |
| |
| LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser) { |
| mOriginalRoot = rootInfo; |
| mSelectedUserId = selectedUser; |
| } |
| |
| @Override |
| public void onRootLoaded(@Nullable RootInfo root) { |
| if (root == null) { |
| // There is no such root in the other profile. Maybe the provider is missing on |
| // the other profile. Create a placeholder root and open it to show error message. |
| root = RootInfo.copyRootInfo(mOriginalRoot); |
| root.userId = mSelectedUserId; |
| } |
| openRoot(root); |
| } |
| } |
| |
| @Override |
| public final void loadFirstRoot(Uri uri) { |
| new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded) |
| .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); |
| } |
| |
| @Override |
| public void loadDocumentsForCurrentStack() { |
| // mState.stack may be empty when we cannot load the root document. |
| // However, we still want to restart loader because we may need to perform search in a |
| // cross-profile scenario. |
| // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op. |
| // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null. |
| |
| // Only allow restartLoader when the previous loader is finished or reset. Allowing |
| // multiple consecutive calls to restartLoader() / onCreateLoader() will probably create |
| // multiple active loaders, because restartLoader() does not interrupt previous loaders' |
| // loading, therefore may block the UI thread and cause ANR. |
| if (mLoaderSemaphore.tryAcquire()) { |
| mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); |
| } |
| } |
| |
| protected final boolean launchToDocument(Uri uri) { |
| if (DEBUG) { |
| Log.d(TAG, "launchToDocument() uri=" + uri); |
| } |
| |
| // We don't support launching to a document in an archive. |
| if (Providers.isArchiveUri(uri)) { |
| return false; |
| } |
| |
| loadDocument(uri, UserId.DEFAULT_USER, this::onStackToLaunchToLoaded); |
| return true; |
| } |
| |
| /** |
| * Invoked <b>only</b> once, when the initial stack (that is the stack we are going to |
| * "launch to") is loaded. |
| * |
| * @see #launchToDocument(Uri) |
| */ |
| private void onStackToLaunchToLoaded(@Nullable DocumentStack stack) { |
| if (DEBUG) { |
| Log.d(TAG, "onLaunchStackLoaded() stack=" + stack); |
| } |
| |
| if (stack == null) { |
| Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); |
| launchToDefaultLocation(); |
| |
| Metrics.logLaunchAtLocation(mState, null); |
| return; |
| } |
| |
| // Make sure the document at the top of the stack is a directory (if it isn't - just pop |
| // one off). |
| if (!stack.peek().isDirectory()) { |
| stack.pop(); |
| } |
| |
| mState.stack.reset(stack); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| |
| Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); |
| } |
| |
| private void onRootLoaded(@Nullable RootInfo root) { |
| boolean invalidRootForAction = |
| (root != null |
| && !root.supportsChildren() |
| && mState.action == State.ACTION_OPEN_TREE); |
| |
| if (invalidRootForAction) { |
| loadDeviceRoot(); |
| } else if (root != null) { |
| mActivity.onRootPicked(root); |
| } else { |
| launchToDefaultLocation(); |
| } |
| } |
| |
| protected abstract void launchToDefaultLocation(); |
| |
| protected void restoreRootAndDirectory() { |
| if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) { |
| mActivity.onRootPicked(mState.stack.getRoot()); |
| } else { |
| mActivity.restoreRootAndDirectory(); |
| } |
| } |
| |
| protected final void loadDeviceRoot() { |
| loadRoot(DocumentsContract.buildRootUri(Providers.AUTHORITY_STORAGE, |
| Providers.ROOT_ID_DEVICE), UserId.DEFAULT_USER); |
| } |
| |
| protected final void loadHomeDir() { |
| loadRoot(Shared.getDefaultRootUri(mActivity), UserId.DEFAULT_USER); |
| } |
| |
| protected final void loadRecent() { |
| mState.stack.changeRoot(mProviders.getRecentsRoot(UserId.DEFAULT_USER)); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } |
| |
| protected MutableSelection<String> getStableSelection() { |
| MutableSelection<String> selection = new MutableSelection<>(); |
| mSelectionMgr.copySelection(selection); |
| return selection; |
| } |
| |
| @Override |
| public ActionHandler reset(ContentLock reloadLock) { |
| mContentLock = reloadLock; |
| mActivity.getLoaderManager().destroyLoader(LOADER_ID); |
| return this; |
| } |
| |
| private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { |
| |
| @Override |
| public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { |
| Context context = mActivity; |
| |
| if (mState.stack.isRecents()) { |
| final LockingContentObserver observer = new LockingContentObserver( |
| mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); |
| MultiRootDocumentsLoader loader; |
| |
| if (mSearchMgr.isSearching()) { |
| if (DEBUG) { |
| Log.d(TAG, "Creating new GlobalSearchLoader."); |
| } |
| loader = new GlobalSearchLoader( |
| context, |
| mProviders, |
| mState, |
| mExecutors, |
| mInjector.fileTypeLookup, |
| mSearchMgr.buildQueryArgs(), |
| mState.stack.getRoot().userId); |
| } else { |
| if (DEBUG) { |
| Log.d(TAG, "Creating new loader recents."); |
| } |
| loader = new RecentsLoader( |
| context, |
| mProviders, |
| mState, |
| mExecutors, |
| mInjector.fileTypeLookup, |
| mState.stack.getRoot().userId); |
| } |
| loader.setObserver(observer); |
| return loader; |
| } else { |
| // There maybe no root docInfo |
| DocumentInfo rootDoc = mState.stack.peek(); |
| |
| String authority = rootDoc == null |
| ? mState.stack.getRoot().authority |
| : rootDoc.authority; |
| String documentId = rootDoc == null |
| ? mState.stack.getRoot().documentId |
| : rootDoc.documentId; |
| |
| Uri contentsUri = mSearchMgr.isSearching() |
| ? DocumentsContract.buildSearchDocumentsUri( |
| mState.stack.getRoot().authority, |
| mState.stack.getRoot().rootId, |
| mSearchMgr.getCurrentSearch()) |
| : DocumentsContract.buildChildDocumentsUri( |
| authority, |
| documentId); |
| |
| final Bundle queryArgs = mSearchMgr.isSearching() |
| ? mSearchMgr.buildQueryArgs() |
| : null; |
| |
| if (mInjector.config.managedModeEnabled(mState.stack)) { |
| contentsUri = DocumentsContract.setManageMode(contentsUri); |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, |
| "Creating new directory loader for: " |
| + DocumentInfo.debugString(mState.stack.peek())); |
| } |
| |
| return new DirectoryLoader( |
| mInjector.features, |
| context, |
| mState, |
| contentsUri, |
| mInjector.fileTypeLookup, |
| mContentLock, |
| queryArgs); |
| } |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { |
| if (DEBUG) { |
| Log.d(TAG, "Loader has finished for: " |
| + DocumentInfo.debugString(mState.stack.peek())); |
| } |
| assert(result != null); |
| |
| mInjector.getModel().update(result); |
| mLoaderSemaphore.release(); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<DirectoryResult> loader) { |
| mLoaderSemaphore.release(); |
| } |
| } |
| /** |
| * A class primarily for the support of isolating our tests |
| * from our concrete activity implementations. |
| */ |
| public interface CommonAddons { |
| void restoreRootAndDirectory(); |
| void refreshCurrentRootAndDirectory(@AnimationType int anim); |
| void onRootPicked(RootInfo root); |
| // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity. |
| void onDocumentsPicked(List<DocumentInfo> docs); |
| void onDocumentPicked(DocumentInfo doc); |
| RootInfo getCurrentRoot(); |
| DocumentInfo getCurrentDirectory(); |
| UserId getSelectedUser(); |
| /** |
| * Check whether current directory is root of recent. |
| */ |
| boolean isInRecents(); |
| void setRootsDrawerOpen(boolean open); |
| |
| /** |
| * Set the locked status of the DrawerController. |
| */ |
| void setRootsDrawerLocked(boolean locked); |
| |
| // TODO: Let navigator listens to State |
| void updateNavigator(); |
| |
| @VisibleForTesting |
| void notifyDirectoryNavigated(Uri docUri); |
| } |
| } |