Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 239ab977 authored by Garfield Tan's avatar Garfield Tan
Browse files

[multi-part] Make context menu match spec.

* Isolate context menus to managers only
* Support open in new window and open with (with exception of managed
  download files)
* Move menu inflation logic into menu manager

Change-Id: I49478c3793e22db6ee8309b64bc366d171f444c2
parent ae84a18a
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
@@ -16,8 +16,11 @@

package com.android.documentsui;

import android.app.Fragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;

import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
@@ -36,6 +39,44 @@ public final class FilesMenuManager extends MenuManager {
        mSearchManager.updateMenu();
    }

    @Override
    public void showContextMenu(Fragment f, View v, float x, float y) {
        // Register context menu here so long-press doesn't trigger this context floating menu.
        f.registerForContextMenu(v);
        v.showContextMenu(x, y);
        f.unregisterForContextMenu(v);
    }

    @Override
    public void inflateContextMenuForContainer(
            Menu menu, MenuInflater inflater, DirectoryDetails directoryDetails) {
        inflater.inflate(R.menu.container_context_menu, menu);
        updateContextMenuForContainer(menu, directoryDetails);
    }

    @Override
    public void inflateContextMenuForDocs(
            Menu menu, MenuInflater inflater, SelectionDetails selectionDetails) {
        final boolean hasDir = selectionDetails.containsDirectories();
        final boolean hasFile = selectionDetails.containsFiles();

        assert(hasDir || hasFile);
        if (!hasDir) {
            inflater.inflate(R.menu.file_context_menu, menu);
            updateContextMenuForFiles(menu, selectionDetails);
            return;
        }

        if (!hasFile) {
            inflater.inflate(R.menu.dir_context_menu, menu);
            updateContextMenuForDirs(menu, selectionDetails);
            return;
        }

        inflater.inflate(R.menu.mixed_context_menu, menu);
        updateContextMenu(menu, selectionDetails);
    }

    @Override
    void updateSettings(MenuItem settings, RootInfo root) {
        settings.setVisible(true);
+34 −4
Original line number Diff line number Diff line
@@ -16,14 +16,18 @@

package com.android.documentsui;

import android.app.Fragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;

import com.android.documentsui.base.Menus;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.internal.annotations.VisibleForTesting;

public abstract class MenuManager {

@@ -61,6 +65,28 @@ public abstract class MenuManager {
        Menus.disableHiddenItems(menu);
    }

    /**
     * Called when we needs {@link MenuManager} to ask Android to show context menu for us.
     * {@link MenuManager} can choose to defeat this request.
     *
     * {@link #inflateContextMenuForDocs} and {@link #inflateContextMenuForContainer} are called
     * afterwards when Android asks us to provide the content of context menus, so they're not
     * correct locations to suppress context menus.
     */
    public void showContextMenu(Fragment f, View v, float x, float y) {
        // Pickers don't have any context menu at this moment.
    }

    public void inflateContextMenuForContainer(
            Menu menu, MenuInflater inflater, DirectoryDetails directoryDetails) {
        throw new UnsupportedOperationException("Pickers don't allow context menu.");
    }

    public void inflateContextMenuForDocs(
            Menu menu, MenuInflater inflater, SelectionDetails selectionDetails) {
        throw new UnsupportedOperationException("Pickers don't allow context menu.");
    }

    /**
     * @see DirectoryFragment#onCreateContextMenu
     *
@@ -71,7 +97,8 @@ public abstract class MenuManager {
     *      containsFiles may return false because this may be called when user right clicks on an
     *      unselectable item in pickers
     */
    public void updateContextMenuForFiles(Menu menu, SelectionDetails selectionDetails) {
    @VisibleForTesting
    void updateContextMenuForFiles(Menu menu, SelectionDetails selectionDetails) {
        assert(selectionDetails != null);

        MenuItem share = menu.findItem(R.id.menu_share);
@@ -97,7 +124,8 @@ public abstract class MenuManager {
     *      containDirectories may return false because this may be called when user right clicks on
     *      an unselectable item in pickers
     */
    public void updateContextMenuForDirs(Menu menu, SelectionDetails selectionDetails) {
    @VisibleForTesting
    void updateContextMenuForDirs(Menu menu, SelectionDetails selectionDetails) {
        assert(selectionDetails != null);

        MenuItem openInNewWindow = menu.findItem(R.id.menu_open_in_new_window);
@@ -116,7 +144,8 @@ public abstract class MenuManager {
     *
     * Update shared context menu items of both files and folders context menus.
     */
    public void updateContextMenu(Menu menu, SelectionDetails selectionDetails) {
    @VisibleForTesting
    void updateContextMenu(Menu menu, SelectionDetails selectionDetails) {
        assert(selectionDetails != null);

        MenuItem cut = menu.findItem(R.id.menu_cut_to_clipboard);
@@ -136,7 +165,8 @@ public abstract class MenuManager {
     *
     * Called when user tries to generate a context menu anchored to an empty pane.
     */
    public void updateContextMenuForContainer(Menu menu, DirectoryDetails directoryDetails) {
    @VisibleForTesting
    void updateContextMenuForContainer(Menu menu, DirectoryDetails directoryDetails) {
        MenuItem paste = menu.findItem(R.id.menu_paste_from_clipboard);
        MenuItem selectAll = menu.findItem(R.id.menu_select_all);

+40 −50
Original line number Diff line number Diff line
@@ -441,43 +441,11 @@ public class DirectoryFragment extends Fragment

        final String modelId = getModelId(v);
        if (modelId == null) {
            inflater.inflate(R.menu.container_context_menu, menu);
            mMenuManager.updateContextMenuForContainer(
                    menu, getBaseActivity().getDirectoryDetails());
            return;
        }

        final boolean hasDir = mSelectionMetadata.containsDirectories();
        final boolean hasFile = mSelectionMetadata.containsFiles();
        if (!hasDir && !hasFile) {
            // User triggered a context menu on a doc without any selection. This is a legitimate
            // case in pickers while user right clicks on an unselectable item.
            final String mimeType = DocumentInfo.getCursorString(
                    mModel.getItem(modelId), Document.COLUMN_MIME_TYPE);
            if (Document.MIME_TYPE_DIR.equals(mimeType)) {
                inflater.inflate(R.menu.dir_context_menu, menu);
                mMenuManager.updateContextMenuForDirs(menu, mSelectionMetadata);
            mMenuManager.inflateContextMenuForContainer(
                    menu, inflater, getBaseActivity().getDirectoryDetails());
        } else {
                inflater.inflate(R.menu.file_context_menu, menu);
                mMenuManager.updateContextMenuForFiles(menu, mSelectionMetadata);
            }
            return;
        }

        if (!hasDir) {
            inflater.inflate(R.menu.file_context_menu, menu);
            mMenuManager.updateContextMenuForFiles(menu, mSelectionMetadata);
            return;
            mMenuManager.inflateContextMenuForDocs(menu, inflater, mSelectionMetadata);
        }

        if (!hasFile) {
            inflater.inflate(R.menu.dir_context_menu, menu);
            mMenuManager.updateContextMenuForDirs(menu, mSelectionMetadata);
            return;
        }

        inflater.inflate(R.menu.mixed_context_menu, menu);
        mMenuManager.updateContextMenu(menu, mSelectionMetadata);
    }

    @Override
@@ -504,25 +472,23 @@ public class DirectoryFragment extends Fragment
    }

    protected boolean onRightClick(InputEvent e) {
        final View v;
        final float x, y;
        if (e.isOverModelItem()) {
            DocumentHolder doc = (DocumentHolder) e.getDocumentDetails();

            // We are registering for context menu here so long-press doesn't trigger this
            // floating context menu, and then quickly unregister right afterwards
            registerForContextMenu(doc.itemView);
            mRecView.showContextMenuForChild(
                    doc.itemView,
                    e.getX() - doc.itemView.getLeft(), e.getY() - doc.itemView.getTop());
            unregisterForContextMenu(doc.itemView);
            return true;
            v = doc.itemView;
            x = e.getX() - v.getLeft();
            y = e.getY() - v.getTop();
        } else {
            v = (mEmptyView.getVisibility() == View.VISIBLE)
                    ? mEmptyView
                    : mRecView;
            x = e.getX();
            y = e.getY();
        }

        View v = (mEmptyView.getVisibility() == View.VISIBLE)
                ? mEmptyView : mRecView;

        registerForContextMenu(v);
        v.showContextMenu(e.getX(), e.getY());
        unregisterForContextMenu(v);
        mMenuManager.showContextMenu(this, v, x, y);

        return true;
    }
@@ -625,6 +591,14 @@ public class DirectoryFragment extends Fragment
                mActionModeController.finishActionMode();
                return true;

            case R.id.menu_open_with:
                showChooserForDoc(selection);
                return true;

            case R.id.menu_open_in_new_window:
                openInNewWindow(selection);
                return true;

            case R.id.menu_share:
                shareDocuments(selection);
                // TODO: Only finish selection if share action is completed.
@@ -717,6 +691,22 @@ public class DirectoryFragment extends Fragment
        }
    }

    private void showChooserForDoc(final Selection selected) {
        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);

        assert(selected.size() == 1);
        DocumentInfo doc =
                DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
        mTuner.showChooserForDoc(doc);
    }

    private void openInNewWindow(final Selection selected) {
        assert(selected.size() == 1);
        DocumentInfo doc =
                DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
        mTuner.openInNewWindow(getDisplayState().stack, doc);
    }

    private void shareDocuments(final Selection selected) {
        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);

+21 −0
Original line number Diff line number Diff line
@@ -26,9 +26,12 @@ import android.content.Context;
import android.provider.DocumentsContract.Document;

import com.android.documentsui.BaseActivity;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.MimePredicate;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
import com.android.documentsui.manager.ManageActivity;
import com.android.documentsui.sorting.SortController;

/**
@@ -79,6 +82,14 @@ public abstract class FragmentTuner {

    abstract void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch);

    void showChooserForDoc(DocumentInfo doc) {
        throw new UnsupportedOperationException("Show chooser not supported!");
    }

    void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
        throw new UnsupportedOperationException("Open in new window not supported!");
    }

    /**
     * Provides support for Platform specific specializations of DirectoryFragment.
     */
@@ -204,5 +215,15 @@ public abstract class FragmentTuner {
        public boolean dragAndDropEnabled() {
            return true;
        }

        @Override
        public void showChooserForDoc(DocumentInfo doc) {
            ((ManageActivity) mContext).showChooserForDoc(doc);
        }

        @Override
        public void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
            ((ManageActivity) mContext).openInNewWindow(stack, doc);
        }
    }
}
+91 −45
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.documentsui.manager;
import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
import static com.android.documentsui.base.Shared.DEBUG;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.ActivityNotFoundException;
@@ -42,22 +43,22 @@ import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.Metrics;
import com.android.documentsui.OperationDialogFragment;
import com.android.documentsui.OperationDialogFragment.DialogType;
import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.R;
import com.android.documentsui.Snackbars;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.PairedTask;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.R;
import com.android.documentsui.Snackbars;
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.dirlist.FragmentTuner;
import com.android.documentsui.dirlist.FragmentTuner.FilesTuner;
import com.android.documentsui.roots.RootsCache;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.roots.RootsCache;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.sidebar.RootsFragment;

@@ -233,7 +234,7 @@ public class ManageActivity extends BaseActivity {
                showCreateDirectoryDialog();
                break;
            case R.id.menu_new_window:
                createNewWindow();
                openInNewWindow(mState.stack, null);
                break;
            case R.id.menu_paste_from_clipboard:
                DirectoryFragment dir = getDirectoryFragment();
@@ -258,11 +259,17 @@ public class ManageActivity extends BaseActivity {
        startActivity(intent);
    }

    private void createNewWindow() {
    /**
     * Opens a new window at given location. If doc is null then it opens the stack. If doc is not
     * null it pushes the doc to the stack and opens it.
     */
    public void openInNewWindow(DocumentStack stack, @Nullable DocumentInfo doc) {
        Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW);

        Intent intent = LauncherActivity.createLaunchIntent(this);
        intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);

        stack = (doc == null) ? stack : new DocumentStack(stack, doc);
        intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);

        // With new multi-window mode we have to pick how we are launched.
        // By default we'd be launched in-place above the existing app.
@@ -297,33 +304,8 @@ public class ManageActivity extends BaseActivity {

    @Override
    public void onDocumentPicked(DocumentInfo doc, Model model) {
        // Anything on downloads goes through the back through downloads manager
        // (that's the MANAGE_DOCUMENT bit).
        // This is done for two reasons:
        // 1) The file in question might be a failed/queued or otherwise have some
        //    specialized download handling.
        // 2) For APKs, the download manager will add on some important security stuff
        //    like origin URL.
        // All other files not on downloads, event APKs, would get no benefit from this
        // treatment, thusly the "isDownloads" check.

        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
        // files in archives. Also, if the activity is already browsing a ZIP from downloads,
        // then skip MANAGE_DOCUMENTS.
        final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
        final boolean isInArchive = mState.stack.size() > 1;
        if (getCurrentRoot().isDownloads() && !isInArchive && !isViewing) {
            // First try managing the document; we expect manager to filter
            // based on authority, so we don't grant.
            final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
            manage.setData(doc.derivedUri);

            try {
                startActivity(manage);
        if (manageDocument(doc)) {
            return;
            } catch (ActivityNotFoundException ex) {
                // fall back to regular handling below.
            }
        }

        if (doc.isContainer()) {
@@ -346,6 +328,65 @@ public class ManageActivity extends BaseActivity {
        openContainerDocument(doc);
    }

    public void showChooserForDoc(DocumentInfo doc) {
        assert(!doc.isContainer());

        if (manageDocument(doc)) {
            // TODO(b/31551845): support it.
            Log.w(TAG, "Open with is not yet supported for managed doc.");
        }

        Intent intent = Intent.createChooser(buildViewIntent(doc), null);
        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Snackbars.makeSnackbar(
                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
        }
    }

    private boolean manageDocument(DocumentInfo doc) {
        if (isManagedDocument()) {
            // First try managing the document; we expect manager to filter
            // based on authority, so we don't grant.
            Intent manage = buildManageIntent(doc);
            try {
                startActivity(manage);
                return true;
            } catch (ActivityNotFoundException ex) {
                // Fall back to regular handling.
            }
        }

        return false;
    }

    private boolean isManagedDocument() {
        // Anything on downloads goes through the back through downloads manager
        // (that's the MANAGE_DOCUMENT bit).
        // This is done for two reasons:
        // 1) The file in question might be a failed/queued or otherwise have some
        //    specialized download handling.
        // 2) For APKs, the download manager will add on some important security stuff
        //    like origin URL.
        // All other files not on downloads, event APKs, would get no benefit from this
        // treatment, thusly the "isDownloads" check.

        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
        // files in archives. Also, if the activity is already browsing a ZIP from downloads,
        // then skip MANAGE_DOCUMENTS.
        final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
        final boolean isInArchive = mState.stack.size() > 1;
        return getCurrentRoot().isDownloads() && !isInArchive && !isViewing;
    }

    private Intent buildManageIntent(DocumentInfo doc) {
        final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
        manage.setData(doc.derivedUri);

        return manage;
    }

    /**
     * Launches an intent to view the specified document.
     */
@@ -365,7 +406,21 @@ public class ManageActivity extends BaseActivity {
        }

        // Fall back to traditional VIEW action...
        intent = new Intent(Intent.ACTION_VIEW);
        intent = buildViewIntent(doc);
        if (DEBUG && intent.getClipData() != null) {
            Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
        }

        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Snackbars.makeSnackbar(
                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
        }
    }

    private Intent buildViewIntent(DocumentInfo doc) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(doc.derivedUri, doc.mimeType);

        // Downloads has traditionally added the WRITE permission
@@ -379,16 +434,7 @@ public class ManageActivity extends BaseActivity {
        }
        intent.setFlags(flags);

        if (DEBUG && intent.getClipData() != null) {
            Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
        }

        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Snackbars.makeSnackbar(
                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
        }
        return intent;
    }

    @Override
Loading