diff options
| author | Xin Li <delphij@google.com> | 2021-10-06 22:53:54 +0000 |
|---|---|---|
| committer | Xin Li <delphij@google.com> | 2021-10-06 22:53:54 +0000 |
| commit | 8c348c41a50cb3fe83e9d2403cc34db5d5ddcb70 (patch) | |
| tree | 2ba30a68b1e5a2246c7e071219139cea61cab0e6 /src | |
| parent | 4bc189377da5f3cb27fb439d677cb89f3390a7a6 (diff) | |
| parent | 84af7ecf6ae45a2ab3aba9b0019baa191b430af5 (diff) | |
| download | platform_packages_apps_DocumentsUI-master.tar.gz platform_packages_apps_DocumentsUI-master.tar.bz2 platform_packages_apps_DocumentsUI-master.zip | |
Bug: 202323961
Merged-In: I64e7be8bd815a3f3bf84277ffb9ea801a5dceb24
Change-Id: Ib2ecaa196b974cec584f6ae5c1e8b4092818d73a
Diffstat (limited to 'src')
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; } } |
