summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-06 22:53:54 +0000
committerXin Li <delphij@google.com>2021-10-06 22:53:54 +0000
commit8c348c41a50cb3fe83e9d2403cc34db5d5ddcb70 (patch)
tree2ba30a68b1e5a2246c7e071219139cea61cab0e6 /src
parent4bc189377da5f3cb27fb439d677cb89f3390a7a6 (diff)
parent84af7ecf6ae45a2ab3aba9b0019baa191b430af5 (diff)
downloadplatform_packages_apps_DocumentsUI-master.tar.gz
platform_packages_apps_DocumentsUI-master.tar.bz2
platform_packages_apps_DocumentsUI-master.zip
Merge Android 12HEADmaster
Bug: 202323961 Merged-In: I64e7be8bd815a3f3bf84277ffb9ea801a5dceb24 Change-Id: Ib2ecaa196b974cec584f6ae5c1e8b4092818d73a
Diffstat (limited to 'src')
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java17
-rw-r--r--src/com/android/documentsui/ActionModeController.java6
-rw-r--r--src/com/android/documentsui/BaseActivity.java7
-rw-r--r--src/com/android/documentsui/BreadcrumbHolder.java2
-rw-r--r--src/com/android/documentsui/CreateDirectoryFragment.java58
-rw-r--r--src/com/android/documentsui/DirectoryLoader.java24
-rw-r--r--src/com/android/documentsui/DirectoryResult.java74
-rw-r--r--src/com/android/documentsui/DocsSelectionHelper.java4
-rw-r--r--src/com/android/documentsui/DrawerController.java10
-rw-r--r--src/com/android/documentsui/HorizontalBreadcrumb.java3
-rw-r--r--src/com/android/documentsui/Model.java43
-rw-r--r--src/com/android/documentsui/MultiRootDocumentsLoader.java47
-rw-r--r--src/com/android/documentsui/NavigationViewManager.java133
-rw-r--r--src/com/android/documentsui/PreBootReceiver.java11
-rw-r--r--src/com/android/documentsui/RootsMonitor.java3
-rw-r--r--src/com/android/documentsui/StubProfileTabsAddons.java (renamed from src/com/android/documentsui/DummyProfileTabsAddons.java)4
-rw-r--r--src/com/android/documentsui/StubSelectionTracker.java (renamed from src/com/android/documentsui/DummySelectionTracker.java)5
-rw-r--r--src/com/android/documentsui/ThreadHelper.java58
-rw-r--r--src/com/android/documentsui/archives/ArchiveEntryInputStream.java10
-rw-r--r--src/com/android/documentsui/base/FilteringCursorWrapper.java110
-rw-r--r--src/com/android/documentsui/base/Shared.java7
-rw-r--r--src/com/android/documentsui/base/StubLookup.java (renamed from src/com/android/documentsui/base/DummyLookup.java)4
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java21
-rw-r--r--src/com/android/documentsui/dirlist/DocumentsAdapter.java4
-rw-r--r--src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java4
-rw-r--r--src/com/android/documentsui/dirlist/DragStartListener.java2
-rw-r--r--src/com/android/documentsui/dirlist/GridDocumentHolder.java1
-rw-r--r--src/com/android/documentsui/dirlist/ListDocumentHolder.java83
-rw-r--r--src/com/android/documentsui/dirlist/RenameDocumentFragment.java20
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java7
-rw-r--r--src/com/android/documentsui/inspector/DebugView.java4
-rw-r--r--src/com/android/documentsui/picker/PickActivity.java1
-rw-r--r--src/com/android/documentsui/picker/PickFragment.java2
-rw-r--r--src/com/android/documentsui/queries/SearchChipViewManager.java21
-rw-r--r--src/com/android/documentsui/queries/SearchFragment.java18
-rw-r--r--src/com/android/documentsui/queries/SearchViewManager.java7
-rw-r--r--src/com/android/documentsui/roots/ProvidersCache.java128
-rw-r--r--src/com/android/documentsui/services/CompressJob.java22
-rw-r--r--src/com/android/documentsui/services/CopyJob.java3
-rw-r--r--src/com/android/documentsui/services/FileOperationService.java9
-rw-r--r--src/com/android/documentsui/services/Job.java9
-rw-r--r--src/com/android/documentsui/sidebar/RootItem.java12
-rw-r--r--src/com/android/documentsui/sidebar/RootItemListBuilder.java10
-rw-r--r--src/com/android/documentsui/sidebar/SpacerItem.java2
-rw-r--r--src/com/android/documentsui/util/VersionUtils.java15
45 files changed, 727 insertions, 318 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 0c612eddd..a310acee2 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -76,6 +76,7 @@ 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;
@@ -94,6 +95,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
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;
@@ -745,7 +747,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
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 dummy root and open it to show error message.
+ // the other profile. Create a placeholder root and open it to show error message.
root = RootInfo.copyRootInfo(mOriginalRoot);
root.userId = mSelectedUserId;
}
@@ -767,7 +769,13 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
// For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op.
// For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null.
- mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings);
+ // 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) {
@@ -942,10 +950,13 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
assert(result != null);
mInjector.getModel().update(result);
+ mLoaderSemaphore.release();
}
@Override
- public void onLoaderReset(Loader<DirectoryResult> loader) {}
+ public void onLoaderReset(Loader<DirectoryResult> loader) {
+ mLoaderSemaphore.release();
+ }
}
/**
* A class primarily for the support of isolating our tests
diff --git a/src/com/android/documentsui/ActionModeController.java b/src/com/android/documentsui/ActionModeController.java
index d8cf59000..89b8ff383 100644
--- a/src/com/android/documentsui/ActionModeController.java
+++ b/src/com/android/documentsui/ActionModeController.java
@@ -46,6 +46,7 @@ public class ActionModeController extends SelectionObserver<String>
private final Activity mActivity;
private final SelectionTracker<String> mSelectionMgr;
+ private final NavigationViewManager mNavigator;
private final MenuManager mMenuManager;
private final MessageBuilder mMessages;
@@ -58,11 +59,13 @@ public class ActionModeController extends SelectionObserver<String>
public ActionModeController(
Activity activity,
SelectionTracker<String> selectionMgr,
+ NavigationViewManager navigator,
MenuManager menuManager,
MessageBuilder messages) {
mActivity = activity;
mSelectionMgr = selectionMgr;
+ mNavigator = navigator;
mMenuManager = menuManager;
mMessages = messages;
}
@@ -132,6 +135,8 @@ public class ActionModeController extends SelectionObserver<String>
// Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
mScope.accessibilityImportanceSetter.setAccessibilityImportance(
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar);
+
+ mNavigator.setActionModeActivated(false);
}
@Override
@@ -141,6 +146,7 @@ public class ActionModeController extends SelectionObserver<String>
mode.setTitle(mActivity.getResources().getQuantityString(R.plurals.selected_count, size));
if (size > 0) {
+ mNavigator.setActionModeActivated(true);
// Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
// these controls when using linear navigation.
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 8c0d9693f..b4ab5bdf0 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -167,6 +167,11 @@ public abstract class BaseActivity
mNavigator = new NavigationViewManager(this, mDrawer, mState, this, breadcrumb,
profileTabsContainer, DocumentsApplication.getUserIdManager(this));
+ AppBarLayout appBarLayout = findViewById(R.id.app_bar);
+ if (appBarLayout != null) {
+ appBarLayout.addOnOffsetChangedListener(mNavigator);
+ }
+
SearchManagerListener searchListener = new SearchManagerListener() {
/**
* Called when search results changed. Refreshes the content of the directory. It
@@ -934,7 +939,7 @@ public abstract class BaseActivity
getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
@Override
public boolean queueIdle() {
- // If startup benchmark is requested by a whitelisted testing package, then
+ // If startup benchmark is requested by an allowedlist testing package, then
// close the activity once idle, and notify the testing activity.
if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
diff --git a/src/com/android/documentsui/BreadcrumbHolder.java b/src/com/android/documentsui/BreadcrumbHolder.java
index 68c0e6a0e..4baaf9ca5 100644
--- a/src/com/android/documentsui/BreadcrumbHolder.java
+++ b/src/com/android/documentsui/BreadcrumbHolder.java
@@ -27,14 +27,12 @@ public final class BreadcrumbHolder extends RecyclerView.ViewHolder {
protected TextView mTitle;
protected ImageView mArrow;
- protected int mDefaultTextColor;
protected boolean mLast;
public BreadcrumbHolder(View itemView) {
super(itemView);
mTitle = itemView.findViewById(R.id.breadcrumb_text);
mArrow = itemView.findViewById(R.id.breadcrumb_arrow);
- mDefaultTextColor = mTitle.getTextColors().getDefaultColor();
mLast = false;
}
diff --git a/src/com/android/documentsui/CreateDirectoryFragment.java b/src/com/android/documentsui/CreateDirectoryFragment.java
index 9ef58839b..4b066edb6 100644
--- a/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -25,7 +25,6 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -37,6 +36,7 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
@@ -59,6 +59,9 @@ import com.google.android.material.textfield.TextInputLayout;
*/
public class CreateDirectoryFragment extends DialogFragment {
private static final String TAG_CREATE_DIRECTORY = "create_directory";
+ private @Nullable DialogInterface mDialog;
+ private EditText mEditText;
+ private TextInputLayout mInputWrapper;
public static void show(FragmentManager fm) {
if (fm.isStateSaved()) {
@@ -78,30 +81,20 @@ public class CreateDirectoryFragment extends DialogFragment {
final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
final View view = dialogInflater.inflate(R.layout.dialog_file_name, null, false);
- final EditText editText = (EditText) view.findViewById(android.R.id.text1);
+ mEditText = (EditText) view.findViewById(android.R.id.text1);
- final TextInputLayout inputWrapper = view.findViewById(R.id.input_wrapper);
- inputWrapper.setHint(getString(R.string.input_hint_new_folder));
+ mInputWrapper = view.findViewById(R.id.input_wrapper);
+ mInputWrapper.setHint(getString(R.string.input_hint_new_folder));
builder.setTitle(R.string.menu_create_dir);
builder.setView(view);
-
- builder.setPositiveButton(
- android.R.string.ok,
- new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- createDirectory(editText.getText().toString());
- }
- });
-
+ builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, null);
final AlertDialog dialog = builder.create();
-
+ dialog.setOnShowListener(this::onShowDialog);
// Workaround for the problem - virtual keyboard doesn't show on the phone.
Shared.ensureKeyboardPresent(context, dialog);
-
- editText.setOnEditorActionListener(
+ mEditText.setOnEditorActionListener(
new OnEditorActionListener() {
@Override
public boolean onEditorAction(
@@ -109,24 +102,39 @@ public class CreateDirectoryFragment extends DialogFragment {
if ((actionId == EditorInfo.IME_ACTION_DONE) || (event != null
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER
&& event.hasNoModifiers())) {
- createDirectory(editText.getText().toString());
- dialog.dismiss();
+ createDirectory(mEditText.getText().toString());
return true;
}
return false;
}
});
- editText.requestFocus();
+ mEditText.requestFocus();
return dialog;
}
- private void createDirectory(String name) {
- final BaseActivity activity = (BaseActivity) getActivity();
- final DocumentInfo cwd = activity.getCurrentDirectory();
+ private void onShowDialog(DialogInterface dialog) {
+ mDialog = dialog;
+ Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
+ button.setOnClickListener(this::onClickDialog);
+ }
+
+ private void onClickDialog(View view) {
+ createDirectory(mEditText.getText().toString());
+ }
- new CreateDirectoryTask(activity, cwd, name).executeOnExecutor(
- ProviderExecutor.forAuthority(cwd.authority));
+ private void createDirectory(String name) {
+ if (name.isEmpty()) {
+ mInputWrapper.setError(getContext().getString(
+ R.string.add_folder_name_error));
+ } else {
+ final BaseActivity activity = (BaseActivity) getActivity();
+ final DocumentInfo cwd = activity.getCurrentDirectory();
+
+ new CreateDirectoryTask(activity, cwd, name).executeOnExecutor(
+ ProviderExecutor.forAuthority(cwd.authority));
+ mDialog.dismiss();
+ }
}
private class CreateDirectoryTask extends AsyncTask<Void, Void, DocumentInfo> {
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 31963962a..816144758 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -56,8 +56,7 @@ import java.util.concurrent.Executor;
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
private static final String TAG = "DirectoryLoader";
-
- private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
+ private static final String[] SEARCH_REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR};
private static final String[] PHOTO_PICKING_ACCEPT_MIMES = new String[]
{Document.MIME_TYPE_DIR, MimeTypes.IMAGE_MIME};
@@ -178,17 +177,16 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
}
cursor.registerContentObserver(mObserver);
- // Filter hidden files.
- cursor = new FilteringCursorWrapper(cursor, mState.showHiddenFiles);
-
+ FilteringCursorWrapper filteringCursor = new FilteringCursorWrapper(cursor);
+ filteringCursor.filterHiddenFiles(mState.showHiddenFiles);
if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) {
// There is no findDocumentPath API. Enable filtering on folders in search mode.
- cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
+ filteringCursor.filterMimes(/* acceptMimes= */ null, SEARCH_REJECT_MIMES);
}
-
if (mPhotoPicking) {
- cursor = new FilteringCursorWrapper(cursor, PHOTO_PICKING_ACCEPT_MIMES, null);
+ filteringCursor.filterMimes(PHOTO_PICKING_ACCEPT_MIMES, /* rejectMimes= */ null);
}
+ cursor = filteringCursor;
// TODO: When API tweaks have landed, use ContentResolver.EXTRA_HONORED_ARGS
// instead of checking directly for ContentResolver.QUERY_ARG_SORT_COLUMNS (won't work)
@@ -198,7 +196,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
} else {
cursor = mModel.sortCursor(cursor, mFileTypeLookup);
}
- result.cursor = cursor;
+ result.setCursor(cursor);
} catch (Exception e) {
Log.w(TAG, "Failed to query", e);
result.exception = e;
@@ -307,8 +305,8 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
// Ensure the loader is stopped
onStopLoading();
- if (mResult != null && mResult.cursor != null && mObserver != null) {
- mResult.cursor.unregisterContentObserver(mObserver);
+ if (mResult != null && mResult.getCursor() != null && mObserver != null) {
+ mResult.getCursor().unregisterContentObserver(mObserver);
}
FileUtils.closeQuietly(mResult);
@@ -316,10 +314,10 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
}
private boolean checkIfCursorStale(DirectoryResult result) {
- if (result == null || result.cursor == null || result.cursor.isClosed()) {
+ if (result == null || result.getCursor() == null || result.getCursor().isClosed()) {
return true;
}
- Cursor cursor = result.cursor;
+ Cursor cursor = result.getCursor();
try {
cursor.moveToPosition(-1);
for (int pos = 0; pos < cursor.getCount(); ++pos) {
diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java
index f820db988..15ab0076d 100644
--- a/src/com/android/documentsui/DirectoryResult.java
+++ b/src/com/android/documentsui/DirectoryResult.java
@@ -16,29 +16,97 @@
package com.android.documentsui;
+import static com.android.documentsui.base.DocumentInfo.getCursorString;
+
import android.content.ContentProviderClient;
import android.database.Cursor;
import android.os.FileUtils;
+import android.provider.DocumentsContract;
+import android.util.Log;
import com.android.documentsui.archives.ArchivesProvider;
import com.android.documentsui.base.DocumentInfo;
+import java.util.HashSet;
+import java.util.Set;
+
public class DirectoryResult implements AutoCloseable {
- public Cursor cursor;
+ private static final String TAG = "DirectoryResult";
+
public Exception exception;
public DocumentInfo doc;
ContentProviderClient client;
+ private Cursor mCursor;
+ private Set<String> mFileNames;
+ private String[] mModelIds;
+
@Override
public void close() {
- FileUtils.closeQuietly(cursor);
+ FileUtils.closeQuietly(mCursor);
if (client != null && doc.isInArchive()) {
ArchivesProvider.releaseArchive(client, doc.derivedUri);
}
FileUtils.closeQuietly(client);
- cursor = null;
client = null;
doc = null;
+
+ setCursor(null);
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ public String[] getModelIds() {
+ return mModelIds;
+ }
+
+ public Set<String> getFileNames() {
+ return mFileNames;
+ }
+
+ /** Update the cursor and populate cursor-related fields. */
+ public void setCursor(Cursor cursor) {
+ mCursor = cursor;
+
+ if (mCursor == null) {
+ mFileNames = null;
+ mModelIds = null;
+ } else {
+ loadDataFromCursor();
+ }
+ }
+
+ /** Populate cursor-related field. Must not be called from UI thread. */
+ private void loadDataFromCursor() {
+ ThreadHelper.assertNotOnMainThread();
+ int cursorCount = mCursor.getCount();
+ String[] modelIds = new String[cursorCount];
+ Set<String> fileNames = new HashSet<>();
+ try {
+ mCursor.moveToPosition(-1);
+ for (int pos = 0; pos < cursorCount; ++pos) {
+ if (!mCursor.moveToNext()) {
+ Log.e(TAG, "Fail to move cursor to next pos: " + pos);
+ return;
+ }
+
+ // Generates a Model ID for a cursor entry that refers to a document. The Model
+ // ID is a unique string that can be used to identify the document referred to by
+ // the cursor. Prefix the ids with the authority to avoid collisions.
+ modelIds[pos] = ModelId.build(mCursor);
+ fileNames.add(
+ getCursorString(mCursor, DocumentsContract.Document.COLUMN_DISPLAY_NAME));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception when moving cursor. Stale cursor?", e);
+ return;
+ }
+
+ // Model related data is only non-null when no error iterating through cursor.
+ mModelIds = modelIds;
+ mFileNames = fileNames;
}
}
diff --git a/src/com/android/documentsui/DocsSelectionHelper.java b/src/com/android/documentsui/DocsSelectionHelper.java
index b7a720c8b..956527a2e 100644
--- a/src/com/android/documentsui/DocsSelectionHelper.java
+++ b/src/com/android/documentsui/DocsSelectionHelper.java
@@ -37,10 +37,10 @@ public final class DocsSelectionHelper extends SelectionTracker<String> {
private final DelegateFactory mFactory;
- // initialize to a dummy object incase we get some input
+ // initialize to a stub object incase we get some input
// event drive calls before we're properly initialized.
// See: b/69306667.
- private SelectionTracker<String> mDelegate = new DummySelectionTracker<>();
+ private SelectionTracker<String> mDelegate = new StubSelectionTracker<>();
@VisibleForTesting
DocsSelectionHelper(DelegateFactory factory) {
diff --git a/src/com/android/documentsui/DrawerController.java b/src/com/android/documentsui/DrawerController.java
index cb536162d..56b3a879f 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -55,7 +55,7 @@ public abstract class DrawerController implements DrawerListener {
DrawerLayout layout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
if (layout == null) {
- return new DummyDrawerController();
+ return new StubDrawerController();
}
View drawer = activity.findViewById(R.id.drawer_roots);
@@ -76,8 +76,8 @@ public abstract class DrawerController implements DrawerListener {
/**
* Returns a controller suitable for {@code Layout}.
*/
- static DrawerController createDummy() {
- return new DummyDrawerController();
+ static DrawerController createStub() {
+ return new StubDrawerController();
}
private static int calculateDrawerWidth(Activity activity) {
@@ -235,9 +235,9 @@ public abstract class DrawerController implements DrawerListener {
}
/*
- * Dummy controller useful with clients that don't host a real drawer.
+ * Stub controller useful with clients that don't host a real drawer.
*/
- private static final class DummyDrawerController extends DrawerController {
+ private static final class StubDrawerController extends DrawerController {
@Override
public void setOpen(boolean open) {}
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index 5cfc65e09..94f0e13f9 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -176,7 +176,6 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
public void onBindViewHolder(BreadcrumbHolder holder, int position) {
final int padding = (int) holder.itemView.getResources()
.getDimension(R.dimen.breadcrumb_item_padding);
- final int enableColor = holder.itemView.getContext().getColor(R.color.primary);
final boolean isFirst = position == 0;
// Note that when isFirst is true, there might not be a DocumentInfo on the stack as it
// could be an error state screen accessible from the root info.
@@ -184,7 +183,7 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
holder.mTitle.setText(
isFirst ? mEnv.getCurrentRoot().title : mState.stack.get(position).displayName);
- holder.mTitle.setTextColor(isLast ? enableColor : holder.mDefaultTextColor);
+ holder.mTitle.setEnabled(isLast);
holder.mTitle.setPadding(isFirst ? padding * 3 : padding,
padding, isLast ? padding * 2 : padding, padding);
holder.mArrow.setVisibility(isLast ? View.GONE : View.VISIBLE);
diff --git a/src/com/android/documentsui/Model.java b/src/com/android/documentsui/Model.java
index 49e3c9b26..fdcebae3c 100644
--- a/src/com/android/documentsui/Model.java
+++ b/src/com/android/documentsui/Model.java
@@ -16,7 +16,6 @@
package com.android.documentsui;
-import static com.android.documentsui.base.DocumentInfo.getCursorString;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.SharedMinimal.VERBOSE;
@@ -25,7 +24,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Document;
import android.util.Log;
import androidx.annotation.IntDef;
@@ -125,11 +123,21 @@ public class Model {
return;
}
- mCursor = result.cursor;
+ mCursor = result.getCursor();
mCursorCount = mCursor.getCount();
doc = result.doc;
- updateModelData();
+ if (result.getModelIds() != null && result.getFileNames() != null) {
+ mIds = result.getModelIds();
+ mFileNames.clear();
+ mFileNames.addAll(result.getFileNames());
+
+ // Populate the positions.
+ mPositions.clear();
+ for (int i = 0; i < mCursorCount; ++i) {
+ mPositions.put(mIds[i], i);
+ }
+ }
final Bundle extras = mCursor.getExtras();
if (extras != null) {
@@ -146,33 +154,6 @@ public class Model {
return mCursorCount;
}
- /**
- * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
- * according to the current sort order.
- */
- private void updateModelData() {
- mIds = new String[mCursorCount];
- mFileNames.clear();
- mCursor.moveToPosition(-1);
- for (int pos = 0; pos < mCursorCount; ++pos) {
- if (!mCursor.moveToNext()) {
- Log.e(TAG, "Fail to move cursor to next pos: " + pos);
- return;
- }
- // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
- // unique string that can be used to identify the document referred to by the cursor.
- // Prefix the ids with the authority to avoid collisions.
- mIds[pos] = ModelId.build(mCursor);
- mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
- }
-
- // Populate the positions.
- mPositions.clear();
- for (int i = 0; i < mCursorCount; ++i) {
- mPositions.put(mIds[i], i);
- }
- }
-
public boolean hasFileWithName(String name) {
return mFileNames.contains(name);
}
diff --git a/src/com/android/documentsui/MultiRootDocumentsLoader.java b/src/com/android/documentsui/MultiRootDocumentsLoader.java
index e7d09bfd3..7668b0693 100644
--- a/src/com/android/documentsui/MultiRootDocumentsLoader.java
+++ b/src/com/android/documentsui/MultiRootDocumentsLoader.java
@@ -104,8 +104,6 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
* @param state current state
* @param executors the executors of authorities
* @param fileTypeMap the map of mime types and file types.
- * @param lock the selection lock
- * @param contentChangedCallback callback when content changed
*/
public MultiRootDocumentsLoader(Context context, ProvidersAccess providers, State state,
Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap) {
@@ -126,8 +124,13 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
@Override
public DirectoryResult loadInBackground() {
- synchronized (mTasks) {
- return loadInBackgroundLocked();
+ try {
+ synchronized (mTasks) {
+ return loadInBackgroundLocked();
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, "loadInBackground is interrupted: ", e);
+ return null;
}
}
@@ -135,7 +138,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
mObserver = observer;
}
- private DirectoryResult loadInBackgroundLocked() {
+ private DirectoryResult loadInBackgroundLocked() throws InterruptedException {
if (mFirstPassLatch == null) {
// First time through we kick off all the recent tasks, and wait
// around to see if everyone finishes quickly.
@@ -146,6 +149,11 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
getQueryTask(rootEntry.getKey(), rootEntry.getValue()));
}
+ if (isLoadInBackgroundCanceled()) {
+ // Loader is cancelled (e.g. about to be reset), preempt loading.
+ throw new InterruptedException("Loading is cancelled!");
+ }
+
mFirstPassLatch = new CountDownLatch(mTasks.size());
for (QueryTask task : mTasks.values()) {
mExecutors.lookup(task.authority).execute(task);
@@ -166,6 +174,11 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
int totalQuerySize = 0;
List<Cursor> cursors = new ArrayList<>(mTasks.size());
for (QueryTask task : mTasks.values()) {
+ if (isLoadInBackgroundCanceled()) {
+ // Loader is cancelled (e.g. about to be reset), preempt loading.
+ throw new InterruptedException("Loading is cancelled!");
+ }
+
if (task.isDone()) {
try {
final Cursor[] taskCursors = task.get();
@@ -181,17 +194,18 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
continue;
}
- // Filter hidden files.
- cursor = new FilteringCursorWrapper(cursor, mState.showHiddenFiles);
-
- final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
- cursor, mState.acceptMimes, getRejectMimes(), rejectBefore) {
+ final FilteringCursorWrapper filteredCursor =
+ new FilteringCursorWrapper(cursor) {
@Override
public void close() {
// Ignored, since we manage cursor lifecycle internally
}
};
- cursors.add(filtered);
+ filteredCursor.filterHiddenFiles(mState.showHiddenFiles);
+ filteredCursor.filterMimes(mState.acceptMimes, getRejectMimes());
+ filteredCursor.filterLastModified(rejectBefore);
+
+ cursors.add(filteredCursor);
}
} catch (InterruptedException e) {
@@ -238,7 +252,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
sorted.setExtras(extras);
- result.cursor = sorted;
+ result.setCursor(sorted);
return result;
}
@@ -292,7 +306,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
DirectoryResult oldResult = mResult;
mResult = result;
- if (isStarted()) {
+ if (isStarted() && !isAbandoned() && !isLoadInBackgroundCanceled()) {
super.deliverResult(result);
}
@@ -326,9 +340,6 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
protected void onReset() {
super.onReset();
- // Ensure the loader is stopped
- onStopLoading();
-
synchronized (mTasks) {
for (QueryTask task : mTasks.values()) {
mExecutors.lookup(task.authority).execute(() -> FileUtils.closeQuietly(task));
@@ -459,10 +470,10 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory
}
private boolean checkIfCursorStale(DirectoryResult result) {
- if (result == null || result.cursor == null || result.cursor.isClosed()) {
+ if (result == null || result.getCursor() == null || result.getCursor().isClosed()) {
return true;
}
- Cursor cursor = result.cursor;
+ Cursor cursor = result.getCursor();
try {
cursor.moveToPosition(-1);
for (int pos = 0; pos < cursor.getCount(); ++pos) {
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index 204971dce..83979ab6e 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -19,35 +19,44 @@ package com.android.documentsui;
import static com.android.documentsui.base.SharedMinimal.VERBOSE;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Outline;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewOutlineProvider;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.base.UserId;
import com.android.documentsui.dirlist.AnimationView;
+import com.android.documentsui.util.VersionUtils;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
-import com.google.android.material.tabs.TabLayout;
import java.util.function.IntConsumer;
/**
* A facade over the portions of the app and drawer toolbars.
*/
-public class NavigationViewManager {
+public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListener {
private static final String TAG = "NavigationViewManager";
private final DrawerController mDrawer;
private final Toolbar mToolbar;
+ private final BaseActivity mActivity;
+ private final View mHeader;
private final State mState;
private final NavigationViewManager.Environment mEnv;
private final Breadcrumb mBreadcrumb;
@@ -55,9 +64,13 @@ public class NavigationViewManager {
private final View mSearchBarView;
private final CollapsingToolbarLayout mCollapsingBarLayout;
private final Drawable mDefaultActionBarBackground;
+ private final ViewOutlineProvider mDefaultOutlineProvider;
private final ViewOutlineProvider mSearchBarOutlineProvider;
private final boolean mShowSearchBar;
+ private boolean mIsActionModeActivated = false;
+ private @ColorRes int mDefaultStatusBarColorResId;
+
public NavigationViewManager(
BaseActivity activity,
DrawerController drawer,
@@ -67,7 +80,9 @@ public class NavigationViewManager {
View tabLayoutContainer,
UserIdManager userIdManager) {
+ mActivity = activity;
mToolbar = activity.findViewById(R.id.toolbar);
+ mHeader = activity.findViewById(R.id.directory_header);
mDrawer = drawer;
mState = state;
mEnv = env;
@@ -85,8 +100,18 @@ public class NavigationViewManager {
mSearchBarView = activity.findViewById(R.id.searchbar_title);
mCollapsingBarLayout = activity.findViewById(R.id.collapsing_toolbar);
mDefaultActionBarBackground = mToolbar.getBackground();
+ mDefaultOutlineProvider = mToolbar.getOutlineProvider();
mShowSearchBar = activity.getResources().getBoolean(R.bool.show_search_bar);
+ final int[] styledAttrs = {android.R.attr.statusBarColor};
+ TypedArray a = mActivity.obtainStyledAttributes(styledAttrs);
+ mDefaultStatusBarColorResId = a.getResourceId(0, -1);
+ if (mDefaultStatusBarColorResId == -1) {
+ Log.w(TAG, "Retrieve statusBarColorResId from theme failed, assigned default");
+ mDefaultStatusBarColorResId = R.color.app_background_color;
+ }
+ a.recycle();
+
final Resources resources = mToolbar.getResources();
final int radius = resources.getDimensionPixelSize(R.dimen.search_bar_radius);
final int marginStart =
@@ -102,6 +127,36 @@ public class NavigationViewManager {
};
}
+ @Override
+ public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
+ if (!VersionUtils.isAtLeastS()) {
+ return;
+ }
+
+ // For S+ Only. Change toolbar color dynamically based on scroll offset.
+ // Usually this can be done in xml using app:contentScrim and app:statusBarScrim, however
+ // in our case since we also put directory_header.xml inside the CollapsingToolbarLayout,
+ // the scrim will also cover the directory header. Long term need to think about how to
+ // move directory_header out of the AppBarLayout.
+
+ Window window = mActivity.getWindow();
+ View actionBar = window.getDecorView().findViewById(R.id.action_mode_bar);
+ int dynamicHeaderColor = ContextCompat.getColor(mActivity,
+ offset == 0 ? mDefaultStatusBarColorResId : R.color.color_surface_header);
+ if (actionBar != null) {
+ // Action bar needs to be updated separately for selection mode.
+ actionBar.setBackgroundColor(dynamicHeaderColor);
+ }
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.setStatusBarColor(dynamicHeaderColor);
+ if (shouldShowSearchBar()) {
+ // Do not change search bar background.
+ } else {
+ mToolbar.setBackground(new ColorDrawable(dynamicHeaderColor));
+ }
+ }
+
public void setSearchBarClickListener(View.OnClickListener listener) {
mSearchBarView.setOnClickListener(listener);
}
@@ -138,6 +193,11 @@ public class NavigationViewManager {
return mProfileTabs.getSelectedUser();
}
+ public void setActionModeActivated(boolean actionModeActivated) {
+ mIsActionModeActivated = actionModeActivated;
+ update();
+ }
+
public void update() {
updateScrollFlag();
updateToolbar();
@@ -177,30 +237,57 @@ public class NavigationViewManager {
AppBarLayout.LayoutParams lp =
(AppBarLayout.LayoutParams) mCollapsingBarLayout.getLayoutParams();
- if (shouldShowSearchBar()) {
- lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
- | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
- | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED);
- } else {
- lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
- | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
- }
+ lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
+ | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
mCollapsingBarLayout.setLayoutParams(lp);
}
private void updateToolbar() {
- if (shouldShowSearchBar()) {
+ if (mCollapsingBarLayout == null) {
+ // Tablet mode does not use CollapsingBarLayout
+ // (res/layout-sw720dp/directory_app_bar.xml or res/layout/fixed_layout.xml)
+ if (shouldShowSearchBar()) {
+ mToolbar.setBackgroundResource(R.drawable.search_bar_background);
+ mToolbar.setOutlineProvider(mSearchBarOutlineProvider);
+ } else {
+ mToolbar.setBackground(mDefaultActionBarBackground);
+ mToolbar.setOutlineProvider(null);
+ }
+ return;
+ }
+
+ CollapsingToolbarLayout.LayoutParams toolbarLayoutParams =
+ (CollapsingToolbarLayout.LayoutParams) mToolbar.getLayoutParams();
+
+ int headerTopOffset = 0;
+ if (shouldShowSearchBar() && !mIsActionModeActivated) {
mToolbar.setBackgroundResource(R.drawable.search_bar_background);
mToolbar.setOutlineProvider(mSearchBarOutlineProvider);
+ int searchBarMargin = mToolbar.getResources().getDimensionPixelSize(
+ R.dimen.search_bar_margin);
+ toolbarLayoutParams.setMargins(searchBarMargin, searchBarMargin, searchBarMargin,
+ searchBarMargin);
+ mToolbar.setLayoutParams(toolbarLayoutParams);
+ mToolbar.setElevation(
+ mToolbar.getResources().getDimensionPixelSize(R.dimen.search_bar_elevation));
+ headerTopOffset = toolbarLayoutParams.height + searchBarMargin * 2;
} else {
mToolbar.setBackground(mDefaultActionBarBackground);
- mToolbar.setOutlineProvider(null);
+ mToolbar.setOutlineProvider(mDefaultOutlineProvider);
+ int actionBarMargin = mToolbar.getResources().getDimensionPixelSize(
+ R.dimen.action_bar_margin);
+ toolbarLayoutParams.setMargins(0, 0, 0, /* bottom= */ actionBarMargin);
+ mToolbar.setLayoutParams(toolbarLayoutParams);
+ mToolbar.setElevation(
+ mToolbar.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation));
+ headerTopOffset = toolbarLayoutParams.height + actionBarMargin;
}
- if (mCollapsingBarLayout != null) {
- View overlayBackground =
- mCollapsingBarLayout.findViewById(R.id.toolbar_background_layout);
- overlayBackground.setVisibility(shouldShowSearchBar() ? View.GONE : View.VISIBLE);
+ if (!mIsActionModeActivated) {
+ FrameLayout.LayoutParams headerLayoutParams =
+ (FrameLayout.LayoutParams) mHeader.getLayoutParams();
+ headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0);
+ mHeader.setLayoutParams(headerLayoutParams);
}
}
@@ -209,7 +296,8 @@ public class NavigationViewManager {
}
// Hamburger if drawer is present, else sad nullness.
- private @Nullable Drawable getActionBarIcon() {
+ private @Nullable
+ Drawable getActionBarIcon() {
if (mDrawer.isPresent()) {
return mToolbar.getContext().getDrawable(R.drawable.ic_hamburger);
} else {
@@ -223,16 +311,23 @@ public class NavigationViewManager {
interface Breadcrumb {
void setup(Environment env, State state, IntConsumer listener);
+
void show(boolean visibility);
+
void postUpdate();
}
interface Environment {
- @Deprecated // Use CommonAddones#getCurrentRoot
+ @Deprecated
+ // Use CommonAddones#getCurrentRoot
RootInfo getCurrentRoot();
+
String getDrawerTitle();
- @Deprecated // Use CommonAddones#refreshCurrentRootAndDirectory
+
+ @Deprecated
+ // Use CommonAddones#refreshCurrentRootAndDirectory
void refreshCurrentRootAndDirectory(int animation);
+
boolean isSearchExpanded();
}
}
diff --git a/src/com/android/documentsui/PreBootReceiver.java b/src/com/android/documentsui/PreBootReceiver.java
index c47631654..f5ad9395a 100644
--- a/src/com/android/documentsui/PreBootReceiver.java
+++ b/src/com/android/documentsui/PreBootReceiver.java
@@ -30,6 +30,7 @@ import android.content.res.Resources;
import android.util.Log;
import com.android.documentsui.theme.ThemeOverlayManager;
+import com.android.documentsui.util.VersionUtils;
/**
* A receiver listening action.PRE_BOOT_COMPLETED event for setting component enable or disable.
@@ -91,11 +92,15 @@ public class PreBootReceiver extends BroadcastReceiver {
int resId = overlayRes.getIdentifier(config, "bool", overlayPkg);
if (resId != 0) {
final ComponentName component = new ComponentName(packageName, className);
- final boolean value = overlayRes.getBoolean(resId);
+ boolean enabled = overlayRes.getBoolean(resId);
+ if (VersionUtils.isAtLeastS() && CONFIG_IS_LAUNCHER_ENABLED.equals(config)) {
+ enabled = false; // Do not allow LauncherActivity to be enabled for S+.
+ }
if (DEBUG) {
- Log.i(TAG, "Overlay package:" + overlayPkg + ", customize " + config + ":" + value);
+ Log.i(TAG,
+ "Overlay package:" + overlayPkg + ", customize " + config + ":" + enabled);
}
- pm.setComponentEnabledSetting(component, value
+ pm.setComponentEnabledSetting(component, enabled
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java
index d4354b4cb..e87e53a64 100644
--- a/src/com/android/documentsui/RootsMonitor.java
+++ b/src/com/android/documentsui/RootsMonitor.java
@@ -22,7 +22,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
-
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
@@ -137,7 +136,7 @@ final class RootsMonitor<T extends Activity & CommonAddons> {
// activity.
final Uri uri = mOwner.getIntent().getData();
if (uri != null && uri.equals(mCurrentRoot.getUri())) {
- mOwner.finishAndRemoveTask();
+ mOwner.finish();
return;
}
diff --git a/src/com/android/documentsui/DummyProfileTabsAddons.java b/src/com/android/documentsui/StubProfileTabsAddons.java
index 697025e23..cac175945 100644
--- a/src/com/android/documentsui/DummyProfileTabsAddons.java
+++ b/src/com/android/documentsui/StubProfileTabsAddons.java
@@ -17,9 +17,9 @@
package com.android.documentsui;
/**
- * A dummy {@ProfileTabsAddons} implementation.
+ * A stub {@ProfileTabsAddons} implementation.
*/
-public class DummyProfileTabsAddons implements ProfileTabsAddons {
+public class StubProfileTabsAddons implements ProfileTabsAddons {
@Override
public void setEnabled(boolean enabled) {
diff --git a/src/com/android/documentsui/DummySelectionTracker.java b/src/com/android/documentsui/StubSelectionTracker.java
index 49b9ad918..1a392144f 100644
--- a/src/com/android/documentsui/DummySelectionTracker.java
+++ b/src/com/android/documentsui/StubSelectionTracker.java
@@ -26,10 +26,11 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import java.util.Set;
/**
- * A dummy SelectionTracker used by DocsSelectionHelper before a real SelectionTracker has been
+ * A stub SelectionTracker used by DocsSelectionHelper before a real SelectionTracker has been
* initialized by DirectoryFragment.
+ * @param <K> Selection key type which extends {@link SelectionTracker}.
*/
-public class DummySelectionTracker<K> extends SelectionTracker<K> {
+public class StubSelectionTracker<K> extends SelectionTracker<K> {
@Override
public void addObserver(SelectionObserver observer) {
diff --git a/src/com/android/documentsui/ThreadHelper.java b/src/com/android/documentsui/ThreadHelper.java
new file mode 100644
index 000000000..bb123b69b
--- /dev/null
+++ b/src/com/android/documentsui/ThreadHelper.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.os.Looper;
+
+/** Class for handler/thread utils. */
+public final class ThreadHelper {
+ private ThreadHelper() {
+ }
+
+ static final String MUST_NOT_ON_MAIN_THREAD_MSG =
+ "This method should not be called on main thread.";
+ static final String MUST_ON_MAIN_THREAD_MSG =
+ "This method should only be called on main thread.";
+
+ /** Verifies that current thread is not the UI thread. */
+ public static void assertNotOnMainThread() {
+ if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
+ fatalAssert(MUST_NOT_ON_MAIN_THREAD_MSG);
+ }
+ }
+
+ /** Verifies that current thread is the UI thread. */
+ public static void assertOnMainThread() {
+ if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
+ fatalAssert(MUST_ON_MAIN_THREAD_MSG);
+ }
+ }
+
+ /**
+ * Exceptions thrown in background threads are silently swallowed on Android. Use the
+ * uncaught exception handler of the UI thread to force the app to crash.
+ */
+ public static void fatalAssert(final String message) {
+ crashMainThread(new AssertionError(message));
+ }
+
+ private static void crashMainThread(Throwable t) {
+ Thread.UncaughtExceptionHandler uiThreadExceptionHandler =
+ Looper.getMainLooper().getThread().getUncaughtExceptionHandler();
+ uiThreadExceptionHandler.uncaughtException(Thread.currentThread(), t);
+ }
+}
diff --git a/src/com/android/documentsui/archives/ArchiveEntryInputStream.java b/src/com/android/documentsui/archives/ArchiveEntryInputStream.java
index 0b1c0c2e6..2c34e5a71 100644
--- a/src/com/android/documentsui/archives/ArchiveEntryInputStream.java
+++ b/src/com/android/documentsui/archives/ArchiveEntryInputStream.java
@@ -20,16 +20,16 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
/**
* To simulate the input stream by using ZipFile, SevenZFile, or ArchiveInputStream.
*/
@@ -124,7 +124,7 @@ abstract class ArchiveEntryInputStream extends InputStream {
throw new IllegalArgumentException("ArchiveEntry is empty");
}
- if (archiveEntry.isDirectory() || archiveEntry.getSize() <= 0
+ if (archiveEntry.isDirectory() || archiveEntry.getSize() < 0
|| TextUtils.isEmpty(archiveEntry.getName())) {
throw new IllegalArgumentException("ArchiveEntry is an invalid file entry");
}
diff --git a/src/com/android/documentsui/base/FilteringCursorWrapper.java b/src/com/android/documentsui/base/FilteringCursorWrapper.java
index 9e557b4aa..577e47c33 100644
--- a/src/com/android/documentsui/base/FilteringCursorWrapper.java
+++ b/src/com/android/documentsui/base/FilteringCursorWrapper.java
@@ -29,67 +29,62 @@ import android.provider.DocumentsContract.Document;
import android.util.Log;
/**
- * Cursor wrapper that filters MIME types not matching given list.
+ * Cursor wrapper that filters cursor results by given conditions.
*/
public class FilteringCursorWrapper extends AbstractCursor {
private final Cursor mCursor;
- private final int[] mPosition;
+ private int[] mPositions;
private int mCount;
- public FilteringCursorWrapper(Cursor cursor, String[] acceptMimes) {
- this(cursor, acceptMimes, null, Long.MIN_VALUE);
- }
-
- public FilteringCursorWrapper(Cursor cursor, String[] acceptMimes, String[] rejectMimes) {
- this(cursor, acceptMimes, rejectMimes, Long.MIN_VALUE);
- }
-
- public FilteringCursorWrapper(
- Cursor cursor, String[] acceptMimes, String[] rejectMimes, long rejectBefore) {
+ public FilteringCursorWrapper(Cursor cursor) {
mCursor = cursor;
+ mCount = cursor.getCount();
+ mPositions = new int[mCount];
+ for (int i = 0; i < mCount; i++) {
+ mPositions[i] = i;
+ }
+ }
- final int count = cursor.getCount();
- mPosition = new int[count];
-
- cursor.moveToPosition(-1);
- while (cursor.moveToNext() && mCount < count) {
+ /**
+ * Filters cursor according to mimes. If both lists are empty, all mimes will be rejected.
+ *
+ * @param acceptMimes allowed list of mimes
+ * @param rejectMimes blocked list of mimes
+ */
+ public void filterMimes(String[] acceptMimes, String[] rejectMimes) {
+ filterByCondition((cursor) -> {
final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
- final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
if (rejectMimes != null && MimeTypes.mimeMatches(rejectMimes, mimeType)) {
- continue;
+ return false;
}
- if (lastModified < rejectBefore) {
- continue;
- }
- if (MimeTypes.mimeMatches(acceptMimes, mimeType)) {
- mPosition[mCount++] = cursor.getPosition();
- }
- }
-
- if (DEBUG && mCount != cursor.getCount()) {
- Log.d(TAG, "Before filtering " + cursor.getCount() + ", after " + mCount);
- }
+ return MimeTypes.mimeMatches(acceptMimes, mimeType);
+ });
}
- public FilteringCursorWrapper(Cursor cursor, boolean showHiddenFiles) {
- mCursor = cursor;
-
- final int count = cursor.getCount();
- mPosition = new int[count];
+ /** Filters cursor according to last modified time, and reject earlier than given timestamp. */
+ public void filterLastModified(long rejectBeforeTimestamp) {
+ filterByCondition((cursor) -> {
+ final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+ return lastModified >= rejectBeforeTimestamp;
+ });
+ }
- cursor.moveToPosition(-1);
- while (cursor.moveToNext() && mCount < count) {
- final String name = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
- if (!showHiddenFiles && name != null && name.startsWith(".")) {
- continue;
- }
- mPosition[mCount++] = cursor.getPosition();
+ /** Filter hidden files based on preference. */
+ public void filterHiddenFiles(boolean showHiddenFiles) {
+ if (showHiddenFiles) {
+ return;
}
- if (DEBUG && mCount != cursor.getCount()) {
- Log.d(TAG, "Before filtering " + cursor.getCount() + ", after " + mCount);
- }
+ filterByCondition((cursor) -> {
+ // Judge by name and documentId separately because for some providers
+ // e.g. DownloadProvider, documentId may not contain file name.
+ final String name = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+ final String documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
+ boolean documentIdHidden = documentId != null && documentId.contains("/.");
+ boolean fileNameHidden = name != null && name.startsWith(".");
+ return !(documentIdHidden || fileNameHidden);
+ });
}
@Override
@@ -105,7 +100,7 @@ public class FilteringCursorWrapper extends AbstractCursor {
@Override
public boolean onMove(int oldPosition, int newPosition) {
- return mCursor.moveToPosition(mPosition[newPosition]);
+ return mCursor.moveToPosition(mPositions[newPosition]);
}
@Override
@@ -167,4 +162,27 @@ public class FilteringCursorWrapper extends AbstractCursor {
public void unregisterContentObserver(ContentObserver observer) {
mCursor.unregisterContentObserver(observer);
}
+
+ private interface FilteringCondition {
+ boolean accept(Cursor cursor);
+ }
+
+ private void filterByCondition(FilteringCondition condition) {
+ final int oldCount = this.getCount();
+ int[] newPositions = new int[oldCount];
+ int newCount = 0;
+
+ this.moveToPosition(-1);
+ while (this.moveToNext() && newCount < oldCount) {
+ if (condition.accept(mCursor)) {
+ newPositions[newCount++] = mPositions[this.getPosition()];
+ }
+ }
+
+ if (DEBUG && newCount != this.getCount()) {
+ Log.d(TAG, "Before filtering " + oldCount + ", after " + newCount);
+ }
+ mCount = newCount;
+ mPositions = newPositions;
+ }
}
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index f0f650b7b..5ac9de4d7 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -171,6 +171,10 @@ public final class Shared {
* Whether the calling app should be restricted in Storage Access Framework or not.
*/
public static boolean shouldRestrictStorageAccessFramework(Activity activity) {
+ if (VersionUtils.isAtLeastS()) {
+ return true;
+ }
+
if (!VersionUtils.isAtLeastR()) {
return false;
}
@@ -314,7 +318,8 @@ public final class Shared {
public static void ensureKeyboardPresent(Context context, AlertDialog dialog) {
if (!isHardwareKeyboardAvailable(context)) {
dialog.getWindow().setSoftInputMode(
- WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
+ | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
}
diff --git a/src/com/android/documentsui/base/DummyLookup.java b/src/com/android/documentsui/base/StubLookup.java
index 11a637580..2a2dc9bf1 100644
--- a/src/com/android/documentsui/base/DummyLookup.java
+++ b/src/com/android/documentsui/base/StubLookup.java
@@ -17,8 +17,10 @@ package com.android.documentsui.base;
/**
* Lookup that always returns null.
+ * @param <K> input type (the "key") which implements {@link Lookup}.
+ * @param <V> output type (the "value") which implements {@link Lookup}.
*/
-public final class DummyLookup<K, V> implements Lookup<K, V> {
+public final class StubLookup<K, V> implements Lookup<K, V> {
@Override
public V lookup(K key) {
return null;
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 8fa02ca46..25efb8c50 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -180,6 +180,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
private SelectionMetadata mSelectionMetadata;
private KeyInputHandler mKeyListener;
private @Nullable DragHoverListener mDragHoverListener;
+ private View mRootView;
private IconHelper mIconHelper;
private SwipeRefreshLayout mRefreshLayout;
private RecyclerView mRecView;
@@ -348,12 +349,12 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
mHandler = new Handler(Looper.getMainLooper());
mActivity = (BaseActivity) getActivity();
- final View view = inflater.inflate(R.layout.fragment_directory, container, false);
+ mRootView = inflater.inflate(R.layout.fragment_directory, container, false);
- mProgressBar = view.findViewById(R.id.progressbar);
+ mProgressBar = mRootView.findViewById(R.id.progressbar);
assert mProgressBar != null;
- mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
+ mRecView = (RecyclerView) mRootView.findViewById(R.id.dir_list);
mRecView.setRecyclerListener(
new RecyclerListener() {
@Override
@@ -362,12 +363,12 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
}
});
- mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
+ mRefreshLayout = (SwipeRefreshLayout) mRootView.findViewById(R.id.refresh_layout);
mRefreshLayout.setOnRefreshListener(this);
mRecView.setItemAnimator(new DirectoryItemAnimator());
mInjector = mActivity.getInjector();
- // Initially, this selection tracker (delegator) uses a dummy implementation, so it must be
+ // Initially, this selection tracker (delegator) uses a stub implementation, so it must be
// updated (reset) when necessary things are ready.
mSelectionMgr = mInjector.selectionMgr;
mModel = mInjector.getModel();
@@ -398,7 +399,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
setPreDrawListenerEnabled(true);
- return view;
+ return mRootView;
}
@Override
@@ -492,7 +493,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
this::getModelId,
mRecView::findChildViewUnder,
DocumentsApplication.getDragAndDropManager(mActivity))
- : DragStartListener.DUMMY;
+ : DragStartListener.STUB;
{
// Limiting the scope of the localTracker so nobody uses it.
@@ -585,6 +586,9 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
// Add listener to update contents on sort model change
mState.sortModel.addListener(mSortListener);
+ // After SD card is formatted, we go out of the view and come back. Similarly when users
+ // go out of the app to delete some files, we want to refresh the directory.
+ onRefresh();
}
@Override
@@ -684,6 +688,9 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
public void onViewModeChanged() {
// Mode change is just visual change; no need to kick loader.
+ mRootView.announceForAccessibility(getString(
+ mState.derivedMode == State.MODE_GRID ? R.string.grid_mode_showing
+ : R.string.list_mode_showing));
onDisplayStateChanged();
}
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index 7d09d689f..41ce73c8c 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -36,8 +36,8 @@ import java.util.List;
/**
* DocumentsAdapter provides glue between a directory Model, and RecyclerView. We've
* abstracted this a bit in order to decompose some specialized support
- * for adding dummy layout objects (@see SectionBreakDocumentsAdapter). Handling of the
- * dummy layout objects was error prone when interspersed with the core mode / adapter code.
+ * for adding stub layout objects (@see SectionBreakDocumentsAdapter). Handling of the
+ * stub layout objects was error prone when interspersed with the core mode / adapter code.
*
* @see ModelBackedDocumentsAdapter
* @see DirectoryAddonsAdapter
diff --git a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
index a1963645b..4b66b857f 100644
--- a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
+++ b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
@@ -42,12 +42,12 @@ public class DocumentsSwipeRefreshLayout extends SwipeRefreshLayout {
public DocumentsSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- final int[] styledAttrs = {android.R.attr.colorPrimary};
+ final int[] styledAttrs = {android.R.attr.colorAccent};
TypedArray a = context.obtainStyledAttributes(styledAttrs);
@ColorRes int colorId = a.getResourceId(0, -1);
if (colorId == -1) {
- Log.w(TAG, "Retrive colorPrimary colorId from theme fail, assign R.color.primary");
+ Log.w(TAG, "Retrieve colorAccent colorId from theme fail, assign R.color.primary");
colorId = R.color.primary;
}
a.recycle();
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index 8fe087229..0adddcca8 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -48,7 +48,7 @@ import javax.annotation.Nullable;
*/
interface DragStartListener {
- static final DragStartListener DUMMY = new DragStartListener() {
+ DragStartListener STUB = new DragStartListener() {
@Override
public boolean onDragEvent(MotionEvent event) {
return false;
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index 9e429688a..631298f12 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -89,7 +89,6 @@ final class GridDocumentHolder extends DocumentHolder {
// But it should be an error to be set to selected && be disabled.
if (!itemView.isEnabled()) {
assert(!selected);
- return;
}
super.setSelected(selected, animate);
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index 1bbeec1dd..f6a900236 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -22,7 +22,9 @@ import static com.android.documentsui.base.DocumentInfo.getCursorString;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
+import android.text.TextUtils;
import android.text.format.Formatter;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -41,15 +43,20 @@ import com.android.documentsui.base.UserId;
import com.android.documentsui.roots.RootCursorWrapper;
import com.android.documentsui.ui.Views;
+import java.util.ArrayList;
import java.util.function.Function;
final class ListDocumentHolder extends DocumentHolder {
+ private static final String TAG = "ListDocumentHolder";
private final TextView mTitle;
- private final @Nullable LinearLayout mDetails; // Container of date/size/summary
- private final TextView mDate;
- private final TextView mSize;
- private final TextView mType;
+ private final @Nullable TextView mDate; // Non-null for tablets/sw720dp, null for other devices.
+ private final @Nullable TextView mSize; // Non-null for tablets/sw720dp, null for other devices.
+ private final @Nullable TextView mType; // Non-null for tablets/sw720dp, null for other devices.
+ // Container for date + size + summary, null only for tablets/sw720dp
+ private final @Nullable LinearLayout mDetails;
+ // TextView for date + size + summary, null only for tablets/sw720dp
+ private final @Nullable TextView mMetadataView;
private final ImageView mIconMime;
private final ImageView mIconThumb;
private final ImageView mIconCheck;
@@ -75,6 +82,7 @@ final class ListDocumentHolder extends DocumentHolder {
mSize = (TextView) itemView.findViewById(R.id.size);
mDate = (TextView) itemView.findViewById(R.id.date);
mType = (TextView) itemView.findViewById(R.id.file_type);
+ mMetadataView = (TextView) itemView.findViewById(R.id.metadata);
// Warning: mDetails view doesn't exists in layout-sw720dp-land layout
mDetails = (LinearLayout) itemView.findViewById(R.id.line2);
mPreviewIcon = itemView.findViewById(R.id.preview_icon);
@@ -97,8 +105,7 @@ final class ListDocumentHolder extends DocumentHolder {
}
if (!itemView.isEnabled()) {
- assert(!selected);
- return;
+ assert (!selected);
}
super.setSelected(selected, animate);
@@ -183,12 +190,13 @@ final class ListDocumentHolder extends DocumentHolder {
/**
* Bind this view to the given document for display.
+ *
* @param cursor Pointing to the item to be bound.
* @param modelId The model ID of the item.
*/
@Override
public void bind(Cursor cursor, String modelId) {
- assert(cursor != null);
+ assert (cursor != null);
mModelId = modelId;
@@ -208,33 +216,50 @@ final class ListDocumentHolder extends DocumentHolder {
mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE);
mTitle.setVisibility(View.VISIBLE);
-
- boolean hasDetails = false;
if (mDoc.isDirectory()) {
// Note, we don't show any details for any directory...ever.
- hasDetails = false;
- } else {
- if (mDoc.lastModified > 0) {
- hasDetails = true;
- mDate.setText(Shared.formatTime(mContext, mDoc.lastModified));
- } else {
- mDate.setText(null);
+ if (mDetails != null) {
+ // Non-tablets
+ mDetails.setVisibility(View.GONE);
}
-
- if (mDoc.size > -1) {
- hasDetails = true;
- mSize.setVisibility(View.VISIBLE);
- mSize.setText(Formatter.formatFileSize(mContext, mDoc.size));
+ } else {
+ // For tablets metadata is provided in columns mDate, mSize, mType.
+ // For other devices mMetadataView consolidates the metadata info.
+ if (mMetadataView != null) {
+ // Non-tablets
+ boolean hasDetails = false;
+ ArrayList<String> metadataList = new ArrayList<>();
+ if (mDoc.lastModified > 0) {
+ hasDetails = true;
+ metadataList.add(Shared.formatTime(mContext, mDoc.lastModified));
+ }
+ if (mDoc.size > -1) {
+ hasDetails = true;
+ metadataList.add(Formatter.formatFileSize(mContext, mDoc.size));
+ }
+ metadataList.add(mFileTypeLookup.lookup(mDoc.mimeType));
+ mMetadataView.setText(TextUtils.join(", ", metadataList));
+ if (mDetails != null) {
+ mDetails.setVisibility(hasDetails ? View.VISIBLE : View.GONE);
+ } else {
+ Log.w(TAG, "mDetails is unexpectedly null for non-tablet devices!");
+ }
} else {
- mSize.setVisibility(View.INVISIBLE);
+ // Tablets
+ if (mDoc.lastModified > 0) {
+ mDate.setVisibility(View.VISIBLE);
+ mDate.setText(Shared.formatTime(mContext, mDoc.lastModified));
+ } else {
+ mDate.setVisibility(View.INVISIBLE);
+ }
+ if (mDoc.size > -1) {
+ mSize.setVisibility(View.VISIBLE);
+ mSize.setText(Formatter.formatFileSize(mContext, mDoc.size));
+ } else {
+ mSize.setVisibility(View.INVISIBLE);
+ }
+ mType.setText(mFileTypeLookup.lookup(mDoc.mimeType));
}
-
- mType.setText(mFileTypeLookup.lookup(mDoc.mimeType));
- }
-
- // mDetails view doesn't exists in layout-sw720dp-land layout
- if (mDetails != null) {
- mDetails.setVisibility(hasDetails ? View.VISIBLE : View.GONE);
}
// TODO: Add document debug info
diff --git a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
index f46c4e54f..12873a218 100644
--- a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
+++ b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
@@ -158,16 +158,6 @@ public class RenameDocumentFragment extends DialogFragment {
}
/**
- * Validates if string is a proper document name.
- * Checks if string is not empty. More rules might be added later.
- * @param docName string representing document name
- * @returns true if string is a valid name.
- **/
- private boolean isValidDocumentName(String docName) {
- return !docName.isEmpty();
- }
-
- /**
* Fills text field with the file name and selects the name without extension.
*
* @param editText text field to be filled
@@ -193,17 +183,13 @@ public class RenameDocumentFragment extends DialogFragment {
if (newDisplayName.equals(mDocument.displayName)) {
mDialog.dismiss();
- } else if (!isValidDocumentName(newDisplayName)) {
- Log.w(TAG, "Failed to rename file - invalid name:" + newDisplayName);
- mRenameInputWrapper.setError(getContext().getString(R.string.rename_error));
- Metrics.logRenameFileError();
- } else if (activity.getInjector().getModel().hasFileWithName(newDisplayName)){
+ } else if (newDisplayName.isEmpty()) {
+ mRenameInputWrapper.setError(getContext().getString(R.string.missing_rename_error));
+ } else if (activity.getInjector().getModel().hasFileWithName(newDisplayName)) {
mRenameInputWrapper.setError(getContext().getString(R.string.name_conflict));
selectFileName(mEditText);
- Metrics.logRenameFileError();
} else {
new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
-
if (mDialog != null) {
mDialog.dismiss();
}
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 1afb38eac..7c09811c9 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -37,7 +37,6 @@ import com.android.documentsui.ActionModeController;
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DocsSelectionHelper;
import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.DummyProfileTabsAddons;
import com.android.documentsui.FocusManager;
import com.android.documentsui.Injector;
import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -49,6 +48,7 @@ import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.R;
import com.android.documentsui.SharedInputHandler;
import com.android.documentsui.ShortcutsUpdater;
+import com.android.documentsui.StubProfileTabsAddons;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Features;
import com.android.documentsui.base.RootInfo;
@@ -76,7 +76,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler
private Injector<ActionHandler<FilesActivity>> mInjector;
private ActivityInputHandler mActivityInputHandler;
private SharedInputHandler mSharedInputHandler;
- private final ProfileTabsAddons mProfileTabsAddonsStub = new DummyProfileTabsAddons();
+ private final ProfileTabsAddons mProfileTabsAddonsStub = new StubProfileTabsAddons();
public FilesActivity() {
super(R.layout.files_activity, TAG);
@@ -132,6 +132,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler
mInjector.actionModeController = new ActionModeController(
this,
mInjector.selectionMgr,
+ mNavigator,
mInjector.menuManager,
mInjector.messages);
@@ -150,7 +151,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler
mInjector.searchManager = mSearchManager;
- // No profile tabs will be shown on FilesActivity. Use a dummy to avoid unnecessary
+ // No profile tabs will be shown on FilesActivity. Use a stub to avoid unnecessary
// operations.
mInjector.profileTabsController = new ProfileTabsController(
mInjector.selectionMgr,
diff --git a/src/com/android/documentsui/inspector/DebugView.java b/src/com/android/documentsui/inspector/DebugView.java
index ffd4b7e05..908d19242 100644
--- a/src/com/android/documentsui/inspector/DebugView.java
+++ b/src/com/android/documentsui/inspector/DebugView.java
@@ -27,8 +27,8 @@ import androidx.annotation.StringRes;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.DummyLookup;
import com.android.documentsui.base.Lookup;
+import com.android.documentsui.base.StubLookup;
import com.android.documentsui.inspector.InspectorController.DebugDisplay;
import java.text.NumberFormat;
@@ -47,7 +47,7 @@ public class DebugView extends TableView implements DebugDisplay {
private final Context mContext;
private final Resources mRes;
- private Lookup<String, Executor> mExecutors = new DummyLookup<>();
+ private Lookup<String, Executor> mExecutors = new StubLookup<>();
public DebugView(Context context) {
this(context, null);
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 5328c640e..c2fbd50a0 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -121,6 +121,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons {
mInjector.actionModeController = new ActionModeController(
this,
mInjector.selectionMgr,
+ mNavigator,
mInjector.menuManager,
mInjector.messages);
diff --git a/src/com/android/documentsui/picker/PickFragment.java b/src/com/android/documentsui/picker/PickFragment.java
index d7bd9277e..e9610a510 100644
--- a/src/com/android/documentsui/picker/PickFragment.java
+++ b/src/com/android/documentsui/picker/PickFragment.java
@@ -98,7 +98,7 @@ public class PickFragment extends Fragment {
final PickFragment fragment = new PickFragment();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container_save, fragment, TAG);
- ft.commitAllowingStateLoss();
+ ft.commitNowAllowingStateLoss();
}
public static PickFragment get(FragmentManager fm) {
diff --git a/src/com/android/documentsui/queries/SearchChipViewManager.java b/src/com/android/documentsui/queries/SearchChipViewManager.java
index ebda22bf3..f80a3a7fa 100644
--- a/src/com/android/documentsui/queries/SearchChipViewManager.java
+++ b/src/com/android/documentsui/queries/SearchChipViewManager.java
@@ -24,6 +24,7 @@ import android.provider.DocumentsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.HorizontalScrollView;
import androidx.annotation.NonNull;
@@ -35,6 +36,7 @@ import com.android.documentsui.MetricConsts;
import com.android.documentsui.R;
import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.Shared;
+import com.android.documentsui.util.VersionUtils;
import com.google.android.material.chip.Chip;
import com.google.common.primitives.Ints;
@@ -96,9 +98,11 @@ public class SearchChipViewManager {
static {
sMimeTypesChipItems.put(TYPE_IMAGES,
new SearchChipData(TYPE_IMAGES, R.string.chip_title_images, IMAGES_MIMETYPES));
- sMimeTypesChipItems.put(TYPE_DOCUMENTS,
- new SearchChipData(TYPE_DOCUMENTS, R.string.chip_title_documents,
- DOCUMENTS_MIMETYPES));
+ if (VersionUtils.isAtLeastR()) {
+ sMimeTypesChipItems.put(TYPE_DOCUMENTS,
+ new SearchChipData(TYPE_DOCUMENTS, R.string.chip_title_documents,
+ DOCUMENTS_MIMETYPES));
+ }
sMimeTypesChipItems.put(TYPE_AUDIO,
new SearchChipData(TYPE_AUDIO, R.string.chip_title_audio, AUDIO_MIMETYPES));
sMimeTypesChipItems.put(TYPE_VIDEOS,
@@ -395,7 +399,7 @@ public class SearchChipViewManager {
* Reorder the chips in chip group. The checked chip has higher order.
*
* @param clickedChip the clicked chip, may be null.
- * @param hasAnim if true, play move animation. Otherwise, not.
+ * @param hasAnim if true, play move animation. Otherwise, not.
*/
private void reorderCheckedChips(@Nullable Chip clickedChip, boolean hasAnim) {
final ArrayList<Chip> chipList = new ArrayList<>();
@@ -421,9 +425,10 @@ public class SearchChipViewManager {
return;
}
- final int chipSpacing = mChipGroup.getPaddingEnd();
+ final int chipSpacing = mChipGroup.getResources().getDimensionPixelSize(
+ R.dimen.search_chip_spacing);
final boolean isRtl = mChipGroup.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- float lastX = isRtl ? mChipGroup.getWidth() - chipSpacing : chipSpacing;
+ float lastX = isRtl ? mChipGroup.getWidth() - chipSpacing / 2 : chipSpacing / 2;
// remove all chips except current clicked chip to avoid losing
// accessibility focus.
@@ -465,6 +470,10 @@ public class SearchChipViewManager {
if (parent instanceof HorizontalScrollView) {
final int scrollToX = isRtl ? parent.getWidth() : 0;
((HorizontalScrollView) parent).smoothScrollTo(scrollToX, 0);
+ if (mChipGroup.getChildCount() > 0) {
+ mChipGroup.getChildAt(0)
+ .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
}
}
}
diff --git a/src/com/android/documentsui/queries/SearchFragment.java b/src/com/android/documentsui/queries/SearchFragment.java
index 7f2e779bb..92cd91a3f 100644
--- a/src/com/android/documentsui/queries/SearchFragment.java
+++ b/src/com/android/documentsui/queries/SearchFragment.java
@@ -40,7 +40,7 @@ import com.android.documentsui.R;
import java.util.List;
-public class SearchFragment extends Fragment{
+public class SearchFragment extends Fragment {
private static final String TAG = "SearchFragment";
private static final String KEY_QUERY = "query";
@@ -97,8 +97,8 @@ public class SearchFragment extends Fragment{
}
@Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
final BaseActivity activity = (BaseActivity) getActivity();
final Injector injector = activity.getInjector();
@@ -121,14 +121,16 @@ public class SearchFragment extends Fragment{
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener(this::onHistoryItemClicked);
- View toolbar = getActivity().findViewById(R.id.toolbar_background_layout);
- if (toolbar != null) {
- // Align top with the bottom of search bar.
+ View toolbar = getActivity().findViewById(R.id.toolbar);
+ View collapsingBarLayout = getActivity().findViewById(R.id.collapsing_toolbar);
+ if (toolbar != null && collapsingBarLayout != null) {
+ // If collapsingBarLayout is used (i.e. not in Tablet mode),
+ // need to align top with the bottom of search bar.
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.setMargins(0, getResources().getDimensionPixelSize(
- R.dimen.action_bar_space_height), 0, 0);
+ R.dimen.action_bar_margin) + toolbar.getLayoutParams().height, 0, 0);
getView().setLayoutParams(layoutParams);
}
@@ -150,7 +152,7 @@ public class SearchFragment extends Fragment{
final String item = mHistoryList.get(position);
mSearchViewManager.setHistorySearch();
mSearchViewManager.setCurrentSearch(item);
- mSearchViewManager.restoreSearch(true);
+ mSearchViewManager.restoreSearch(/* keepFocus= */ false);
}
private void dismiss() {
diff --git a/src/com/android/documentsui/queries/SearchViewManager.java b/src/com/android/documentsui/queries/SearchViewManager.java
index e94e900e8..b1016952f 100644
--- a/src/com/android/documentsui/queries/SearchViewManager.java
+++ b/src/com/android/documentsui/queries/SearchViewManager.java
@@ -333,8 +333,6 @@ public class SearchViewManager implements
public boolean cancelSearch() {
if (mSearchView != null && (isExpanded() || isSearching())) {
cancelQueuedSearch();
- // If the query string is not empty search view won't get iconified
- mSearchView.setQuery("", false);
if (mFullBar) {
onClose();
@@ -420,6 +418,11 @@ public class SearchViewManager implements
// Refresh the directory if a search was done
if (mCurrentSearch != null || mChipViewManager.hasCheckedItems()) {
+ // Make sure SearchFragment was dismissed.
+ if (mFragmentManager != null) {
+ SearchFragment.dismissFragment(mFragmentManager);
+ }
+
// Clear checked chips
mChipViewManager.clearCheckedChips();
mCurrentSearch = null;
diff --git a/src/com/android/documentsui/roots/ProvidersCache.java b/src/com/android/documentsui/roots/ProvidersCache.java
index f35d05e2f..ebd54972e 100644
--- a/src/com/android/documentsui/roots/ProvidersCache.java
+++ b/src/com/android/documentsui/roots/ProvidersCache.java
@@ -62,6 +62,7 @@ import com.android.documentsui.base.UserId;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Collection;
@@ -72,6 +73,10 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@@ -90,6 +95,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
// ArchivesProvider doesn't support any roots.
add(ArchivesProvider.AUTHORITY);
}};
+ private static final int FIRST_LOAD_TIMEOUT_MS = 5000;
private final Context mContext;
@@ -111,6 +117,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
private Multimap<UserAuthority, RootInfo> mRoots = ArrayListMultimap.create();
@GuardedBy("mLock")
private HashSet<UserAuthority> mStoppedAuthorities = new HashSet<>();
+ private final Semaphore mMultiProviderUpdateTaskSemaphore = new Semaphore(1);
@GuardedBy("mObservedAuthoritiesDetails")
private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>();
@@ -205,13 +212,16 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
assert (recentRoot.availableBytes == -1);
}
- new UpdateTask(forceRefreshAll, null, callback).executeOnExecutor(
+ new MultiProviderUpdateTask(forceRefreshAll, null, callback).executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR);
}
public void updatePackageAsync(UserId userId, String packageName) {
- new UpdateTask(false, new UserPackage(userId, packageName),
- /* callback= */ null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ new MultiProviderUpdateTask(
+ /* forceRefreshAll= */ false,
+ new UserPackage(userId, packageName),
+ /* callback= */ null)
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void updateAuthorityAsync(UserId userId, String authority) {
@@ -235,7 +245,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
}
/**
- * Block until the first {@link UpdateTask} pass has finished.
+ * Block until the first {@link MultiProviderUpdateTask} pass has finished.
*
* @return {@code true} if cached roots is ready to roll, otherwise
* {@code false} if we timed out while waiting.
@@ -243,7 +253,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
private boolean waitForFirstLoad() {
boolean success = false;
try {
- success = mFirstLoad.await(15, TimeUnit.SECONDS);
+ success = mFirstLoad.await(FIRST_LOAD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
if (!success) {
@@ -254,7 +264,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
/**
* Load roots from authorities that are in stopped state. Normal
- * {@link UpdateTask} passes ignore stopped applications.
+ * {@link MultiProviderUpdateTask} passes ignore stopped applications.
*/
private void loadStoppedAuthorities() {
synchronized (mLock) {
@@ -266,7 +276,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
}
/**
- * Load roots from a stopped authority. Normal {@link UpdateTask} passes
+ * Load roots from a stopped authority. Normal {@link MultiProviderUpdateTask} passes
* ignore stopped applications.
*/
private void loadStoppedAuthority(UserAuthority userAuthority) {
@@ -433,7 +443,7 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
waitForFirstLoad();
loadStoppedAuthorities();
synchronized (mLock) {
- return mRoots.values();
+ return new HashSet<>(mRoots.values());
}
}
@@ -485,15 +495,17 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
Log.i(TAG, output.toString());
}
- private class UpdateTask extends AsyncTask<Void, Void, Void> {
+ private class MultiProviderUpdateTask extends AsyncTask<Void, Void, Void> {
private final boolean mForceRefreshAll;
@Nullable
private final UserPackage mForceRefreshUserPackage;
@Nullable
private final Runnable mCallback;
- private final Multimap<UserAuthority, RootInfo> mTaskRoots = ArrayListMultimap.create();
- private final HashSet<UserAuthority> mTaskStoppedAuthorities = new HashSet<>();
+ @GuardedBy("mLock")
+ private Multimap<UserAuthority, RootInfo> mLocalRoots = ArrayListMultimap.create();
+ @GuardedBy("mLock")
+ private HashSet<UserAuthority> mLocalStoppedAuthorities = new HashSet<>();
/**
* Create task to update roots cache.
@@ -504,7 +516,9 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
* values for this specific user package should be ignored.
* @param callback when non-null, it will be invoked after the task is executed.
*/
- UpdateTask(boolean forceRefreshAll, @Nullable UserPackage forceRefreshUserPackage,
+ MultiProviderUpdateTask(
+ boolean forceRefreshAll,
+ @Nullable UserPackage forceRefreshUserPackage,
@Nullable Runnable callback) {
mForceRefreshAll = forceRefreshAll;
mForceRefreshUserPackage = forceRefreshUserPackage;
@@ -513,12 +527,25 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
@Override
protected Void doInBackground(Void... params) {
+ if (!mMultiProviderUpdateTaskSemaphore.tryAcquire()) {
+ // Abort, since previous update task is still running.
+ return null;
+ }
+
+ int previousPriority = Thread.currentThread().getPriority();
+ Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
+
final long start = SystemClock.elapsedRealtime();
for (UserId userId : mUserIdManager.getUserIds()) {
final RootInfo recents = createOrGetRecentsRoot(userId);
- mTaskRoots.put(new UserAuthority(recents.userId, recents.authority), recents);
+ synchronized (mLock) {
+ mLocalRoots.put(new UserAuthority(recents.userId, recents.authority), recents);
+ }
+ }
+ List<SingleProviderUpdateTaskInfo> taskInfos = new ArrayList<>();
+ for (UserId userId : mUserIdManager.getUserIds()) {
final PackageManager pm = userId.getPackageManager(mContext);
// Pick up provider with action string
final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
@@ -526,25 +553,55 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
for (ResolveInfo info : providers) {
ProviderInfo providerInfo = info.providerInfo;
if (providerInfo.authority != null) {
- handleDocumentsProvider(providerInfo, userId);
+ taskInfos.add(new SingleProviderUpdateTaskInfo(providerInfo, userId));
}
}
}
+ if (!taskInfos.isEmpty()) {
+ CountDownLatch updateTaskInternalCountDown = new CountDownLatch(taskInfos.size());
+ ExecutorService executor = MoreExecutors.getExitingExecutorService(
+ (ThreadPoolExecutor) Executors.newCachedThreadPool());
+ for (SingleProviderUpdateTaskInfo taskInfo: taskInfos) {
+ executor.submit(() ->
+ startSingleProviderUpdateTask(
+ taskInfo.providerInfo,
+ taskInfo.userId,
+ updateTaskInternalCountDown));
+ }
+
+ // Block until all SingleProviderUpdateTask threads finish executing.
+ // Use a shorter timeout for first load since it could block picker UI.
+ long timeoutMs = mFirstLoadDone ? 15000 : FIRST_LOAD_TIMEOUT_MS;
+ boolean success = false;
+ try {
+ success = updateTaskInternalCountDown.await(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ }
+ if (!success) {
+ Log.w(TAG, "Timeout executing update task!");
+ }
+ }
+
final long delta = SystemClock.elapsedRealtime() - start;
- if (VERBOSE) Log.v(TAG,
- "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
synchronized (mLock) {
mFirstLoadDone = true;
if (mBootCompletedResult != null) {
mBootCompletedResult.finish();
mBootCompletedResult = null;
}
- mRoots = mTaskRoots;
- mStoppedAuthorities = mTaskStoppedAuthorities;
+ mRoots = mLocalRoots;
+ mStoppedAuthorities = mLocalStoppedAuthorities;
+ }
+ if (VERBOSE) {
+ Log.v(TAG, "Update found " + mLocalRoots.size() + " roots in " + delta + "ms");
}
+
mFirstLoad.countDown();
LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION));
+ mMultiProviderUpdateTaskSemaphore.release();
+
+ Thread.currentThread().setPriority(previousPriority);
return null;
}
@@ -555,6 +612,17 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
}
}
+ private void startSingleProviderUpdateTask(
+ ProviderInfo providerInfo,
+ UserId userId,
+ CountDownLatch updateCountDown) {
+ int previousPriority = Thread.currentThread().getPriority();
+ Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
+ handleDocumentsProvider(providerInfo, userId);
+ updateCountDown.countDown();
+ Thread.currentThread().setPriority(previousPriority);
+ }
+
private void handleDocumentsProvider(ProviderInfo info, UserId userId) {
UserAuthority userAuthority = new UserAuthority(userId, info.authority);
// Ignore stopped packages for now; we might query them
@@ -563,16 +631,20 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
if (VERBOSE) {
Log.v(TAG, "Ignoring stopped authority " + info.authority + ", user " + userId);
}
- mTaskStoppedAuthorities.add(userAuthority);
+ synchronized (mLock) {
+ mLocalStoppedAuthorities.add(userAuthority);
+ }
return;
}
final boolean forceRefresh = mForceRefreshAll
- || Objects.equals(new UserPackage(userId, info.packageName),
- mForceRefreshUserPackage);
- mTaskRoots.putAll(userAuthority, loadRootsForAuthority(userAuthority, forceRefresh));
+ || Objects.equals(
+ new UserPackage(userId, info.packageName), mForceRefreshUserPackage);
+ synchronized (mLock) {
+ mLocalRoots.putAll(userAuthority,
+ loadRootsForAuthority(userAuthority, forceRefresh));
+ }
}
-
}
private static class UserAuthority {
@@ -611,6 +683,16 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
}
}
+ private static class SingleProviderUpdateTaskInfo {
+ private final ProviderInfo providerInfo;
+ private final UserId userId;
+
+ SingleProviderUpdateTaskInfo(ProviderInfo providerInfo, UserId userId) {
+ this.providerInfo = providerInfo;
+ this.userId = userId;
+ }
+ }
+
private static class PackageDetails {
private String applicationName;
private String packageName;
diff --git a/src/com/android/documentsui/services/CompressJob.java b/src/com/android/documentsui/services/CompressJob.java
index 1f2ea4436..e9ba6e4c8 100644
--- a/src/com/android/documentsui/services/CompressJob.java
+++ b/src/com/android/documentsui/services/CompressJob.java
@@ -16,6 +16,8 @@
package com.android.documentsui.services;
+import static android.content.ContentResolver.wrap;
+
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import android.app.Notification;
@@ -45,6 +47,8 @@ final class CompressJob extends CopyJob {
private static final String TAG = "CompressJob";
private static final String NEW_ARCHIVE_EXTENSION = ".zip";
+ private Uri mArchiveUri;
+
/**
* Moves files to a destination identified by {@code destination}.
* Performs most work by delegating to CopyJob, then deleting
@@ -99,17 +103,16 @@ final class CompressJob extends CopyJob {
displayName = service.getString(R.string.new_archive_file_name, NEW_ARCHIVE_EXTENSION);
}
- Uri archiveUri;
try {
- archiveUri = DocumentsContract.createDocument(
- resolver, mDstInfo.derivedUri, "application/zip", displayName);
+ mArchiveUri = DocumentsContract.createDocument(
+ resolver, mDstInfo.derivedUri, "application/zip", displayName);
} catch (Exception e) {
- archiveUri = null;
+ mArchiveUri = null;
}
try {
mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive(
- archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY), UserId.DEFAULT_USER);
+ mArchiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY), UserId.DEFAULT_USER);
ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri);
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to create dstInfo.", e);
@@ -132,7 +135,14 @@ final class CompressJob extends CopyJob {
Log.e(TAG, "Failed to release the archive.");
}
- // TODO: Remove the archive file in case of an error.
+ // Remove the archive file in case of an error.
+ try {
+ if (!isFinished() || isCanceled()) {
+ DocumentsContract.deleteDocument(wrap(getClient(mArchiveUri)), mArchiveUri);
+ }
+ } catch (RemoteException | FileNotFoundException e) {
+ Log.w(TAG, "Failed to cleanup after compress error: " + mDstInfo.toString(), e);
+ }
super.finish();
}
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index f81966eb6..c972c33ef 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -187,7 +187,8 @@ class CopyJob extends ResolvedResourcesJob {
.setContentText(service.getString(
R.string.notification_touch_for_details))
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT
+ | PendingIntent.FLAG_IMMUTABLE))
.setCategory(Notification.CATEGORY_ERROR)
.setSmallIcon(R.drawable.ic_menu_copy)
.setAutoCancel(true);
diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java
index ca6166a7c..c7be5f4fc 100644
--- a/src/com/android/documentsui/services/FileOperationService.java
+++ b/src/com/android/documentsui/services/FileOperationService.java
@@ -18,7 +18,6 @@ package com.android.documentsui.services;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import androidx.annotation.IntDef;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -29,9 +28,11 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.UserManager;
-import androidx.annotation.VisibleForTesting;
import android.util.Log;
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
import com.android.documentsui.R;
import com.android.documentsui.base.Features;
@@ -97,7 +98,9 @@ public class FileOperationService extends Service implements Job.Listener {
static final String NOTIFICATION_CHANNEL_ID = "channel_id";
- private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
+ // This is a temporary solution, we will gray out the UI when a transaction is in progress to
+ // not enable users to make a transaction.
+ private static final int POOL_SIZE = 1; // Allow only 1 executor operation
@VisibleForTesting static final int NOTIFICATION_ID_PROGRESS = 1;
private static final int NOTIFICATION_ID_FAILURE = 2;
diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java
index 5659c06bd..71f0ae861 100644
--- a/src/com/android/documentsui/services/Job.java
+++ b/src/com/android/documentsui/services/Job.java
@@ -310,7 +310,8 @@ abstract public class Job implements Runnable {
failureCount, failureCount))
.setContentText(service.getString(R.string.notification_touch_for_details))
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT
+ | PendingIntent.FLAG_MUTABLE))
.setCategory(Notification.CATEGORY_ERROR)
.setSmallIcon(icon)
.setAutoCancel(true);
@@ -327,7 +328,8 @@ abstract public class Job implements Runnable {
.setContentTitle(title)
.setContentIntent(
PendingIntent.getActivity(appContext, 0,
- buildNavigateIntent(INTENT_TAG_PROGRESS), 0))
+ buildNavigateIntent(INTENT_TAG_PROGRESS),
+ PendingIntent.FLAG_IMMUTABLE))
.setCategory(Notification.CATEGORY_PROGRESS)
.setSmallIcon(icon)
.setOngoing(true);
@@ -341,7 +343,8 @@ abstract public class Job implements Runnable {
service,
0,
cancelIntent,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
+ | PendingIntent.FLAG_MUTABLE));
return progressBuilder;
}
diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java
index 9af396e4c..a0a3210f8 100644
--- a/src/com/android/documentsui/sidebar/RootItem.java
+++ b/src/com/android/documentsui/sidebar/RootItem.java
@@ -210,13 +210,13 @@ public class RootItem extends Item {
}
/**
- * Creates a dummy root item for a user. A dummy root item is used as a place holder when
+ * Creates a stub root item for a user. A stub root item is used as a place holder when
* there is no such root available. We can therefore show the item on the UI.
*/
- public static RootItem createDummyItem(RootItem item, UserId targetUser) {
- RootInfo dummyRootInfo = RootInfo.copyRootInfo(item.root);
- dummyRootInfo.userId = targetUser;
- RootItem dummy = new RootItem(dummyRootInfo, item.mActionHandler, item.mMaybeShowBadge);
- return dummy;
+ public static RootItem createStubItem(RootItem item, UserId targetUser) {
+ RootInfo stubRootInfo = RootInfo.copyRootInfo(item.root);
+ stubRootInfo.userId = targetUser;
+ RootItem stub = new RootItem(stubRootInfo, item.mActionHandler, item.mMaybeShowBadge);
+ return stub;
}
}
diff --git a/src/com/android/documentsui/sidebar/RootItemListBuilder.java b/src/com/android/documentsui/sidebar/RootItemListBuilder.java
index 4bdce15f3..b29bd0d87 100644
--- a/src/com/android/documentsui/sidebar/RootItemListBuilder.java
+++ b/src/com/android/documentsui/sidebar/RootItemListBuilder.java
@@ -37,7 +37,7 @@ import java.util.List;
* selected user.
*
* <p>If no root of the selected user was added but that of the other user was added,
- * a dummy root of that root for the selected user will be generated.
+ * a stub root of that root for the selected user will be generated.
*
* <p>The builder group the roots using {@link Item#stringId} as key.
*
@@ -45,9 +45,9 @@ import java.util.List;
* itemC[10], itemX[0],itemY[10] where root itemX, itemY do not support cross profile.
*
* <p>When the selected user is user 0, {@link #getList()} returns itemA[0], itemB[0],
- * dummyC[0], itemX[0], itemY[10].
+ * stubC[0], itemX[0], itemY[10].
*
- * <p>When the selected user is user 10, {@link #getList()} returns itemA[10], dummyB[10],
+ * <p>When the selected user is user 10, {@link #getList()} returns itemA[10], stubB[10],
* itemC[10], itemX[0], itemY[10].
*/
class RootItemListBuilder {
@@ -87,7 +87,7 @@ class RootItemListBuilder {
return items;
}
- // If the root supports cross-profile, we return the added root or create a dummy root if
+ // If the root supports cross-profile, we return the added root or create a stub root if
// it was not added for the selected user.
for (RootItem item : items) {
if (item.userId.equals(mSelectedUser)) {
@@ -96,6 +96,6 @@ class RootItemListBuilder {
}
}
- return Collections.singletonList(RootItem.createDummyItem(testRootItem, mSelectedUser));
+ return Collections.singletonList(RootItem.createStubItem(testRootItem, mSelectedUser));
}
}
diff --git a/src/com/android/documentsui/sidebar/SpacerItem.java b/src/com/android/documentsui/sidebar/SpacerItem.java
index d0f49c9d1..44dd75cbb 100644
--- a/src/com/android/documentsui/sidebar/SpacerItem.java
+++ b/src/com/android/documentsui/sidebar/SpacerItem.java
@@ -25,7 +25,7 @@ import com.android.documentsui.R;
import com.android.documentsui.base.UserId;
/**
- * Dummy {@link Item} for dividers between different types of {@link Item}s.
+ * Stub {@link Item} for dividers between different types of {@link Item}s.
*/
class SpacerItem extends Item {
private static final String TAG = "SpacerItem";
diff --git a/src/com/android/documentsui/util/VersionUtils.java b/src/com/android/documentsui/util/VersionUtils.java
index 58ae3cdca..aba6374ec 100644
--- a/src/com/android/documentsui/util/VersionUtils.java
+++ b/src/com/android/documentsui/util/VersionUtils.java
@@ -27,10 +27,19 @@ public class VersionUtils {
}
/**
- * Returns whether the device is running on the Android R or newer.
+ * Returns whether the device is running on Android R or newer.
*/
public static boolean isAtLeastR() {
- return Build.VERSION.CODENAME.equals("R")
- || (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 30);
+ return isAtLeastS() // Keep reference to isAtLeastS() so it's not stripped from test apk
+ || Build.VERSION.CODENAME.equals("R")
+ || Build.VERSION.SDK_INT >= 30;
+ }
+
+ /**
+ * Returns whether the device is running on Android S or newer.
+ */
+ public static boolean isAtLeastS() {
+ return Build.VERSION.CODENAME.equals("S")
+ || Build.VERSION.SDK_INT >= 31;
}
}