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

Commit 8dffd1f5 authored by Tony Huang's avatar Tony Huang
Browse files

Enable preview when no quick viewer

On aosp or work profile case, it may have no quick viewer or
quick viewer package was not installed. To make preview work,
use normal open behavior on such case.

Bug: 148839352
Test: manual
Test: atest DocumentsUIGoogleTests
Change-Id: I9b37539099302731189f03564b5346282fd821d9
parent 22d0816e
Loading
Loading
Loading
Loading
+200 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static com.android.documentsui.base.DocumentInfo.getCursorString;
import static com.android.documentsui.base.SharedMinimal.DEBUG;

import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -50,6 +51,7 @@ import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.Lookup;
import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.Providers;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
@@ -59,6 +61,7 @@ import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
import com.android.documentsui.dirlist.FocusHandler;
import com.android.documentsui.files.LauncherActivity;
import com.android.documentsui.files.QuickViewIntentBuilder;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.LoadFirstRootTask;
@@ -66,6 +69,7 @@ import com.android.documentsui.roots.LoadRootTask;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.sidebar.EjectRootTask;
import com.android.documentsui.sorting.SortListFragment;
import com.android.documentsui.ui.DialogController;
import com.android.documentsui.ui.Snackbars;

import java.util.ArrayList;
@@ -100,6 +104,8 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
    protected final SelectionTracker<String> mSelectionMgr;
    protected final SearchViewManager mSearchMgr;
    protected final Lookup<String, Executor> mExecutors;
    protected final DialogController mDialogs;
    protected final Model mModel;
    protected final Injector<?> mInjector;

    private final LoaderBindings mBindings;
@@ -143,6 +149,8 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
        mSelectionMgr = injector.selectionMgr;
        mSearchMgr = searchMgr;
        mExecutors = executors;
        mDialogs = injector.dialogs;
        mModel = injector.getModel();
        mInjector = injector;

        mBindings = new LoaderBindings();
@@ -356,6 +364,198 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
        }
    }

    // TODO: Make this private and make tests call interface method instead.
    /**
     * Behavior when a document is opened.
     */
    @VisibleForTesting
    public void onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback,
            boolean fromPicker) {
        // In picker mode, don't access archive container to avoid pick file in archive files.
        if (doc.isContainer() && !fromPicker) {
            openContainerDocument(doc);
            return;
        }

        if (manageDocument(doc)) {
            return;
        }

        // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow
        // PackageManager to install it.  This allows users to install APKs from any root.
        // The Downloads special case is handled above in #manageDocument.
        if (MimeTypes.isApkType(doc.mimeType)) {
            viewDocument(doc);
            return;
        }

        switch (type) {
            case VIEW_TYPE_REGULAR:
                if (viewDocument(doc)) {
                    return;
                }
                break;

            case VIEW_TYPE_PREVIEW:
                if (previewDocument(doc, fromPicker)) {
                    return;
                }
                break;

            default:
                throw new IllegalArgumentException("Illegal view type.");
        }

        switch (fallback) {
            case VIEW_TYPE_REGULAR:
                if (viewDocument(doc)) {
                    return;
                }
                break;

            case VIEW_TYPE_PREVIEW:
                if (previewDocument(doc, fromPicker)) {
                    return;
                }
                break;

            case VIEW_TYPE_NONE:
                break;

            default:
                throw new IllegalArgumentException("Illegal fallback view type.");
        }

        // Failed to view including fallback, and it's in an archive.
        if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) {
            mDialogs.showViewInArchivesUnsupported();
        }
    }

    private boolean viewDocument(DocumentInfo doc) {
        if (doc.isPartial()) {
            Log.w(TAG, "Can't view partial file.");
            return false;
        }

        if (doc.isInArchive()) {
            Log.w(TAG, "Can't view files in archives.");
            return false;
        }

        if (doc.isDirectory()) {
            Log.w(TAG, "Can't view directories.");
            return true;
        }

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

        try {
            mActivity.startActivity(intent);
            return true;
        } catch (ActivityNotFoundException e) {
            mDialogs.showNoApplicationFound();
        }
        return false;
    }

    private boolean previewDocument(DocumentInfo doc, boolean fromPicker) {
        if (doc.isPartial()) {
            Log.w(TAG, "Can't view partial file.");
            return false;
        }

        Intent intent = new QuickViewIntentBuilder(
                mActivity.getPackageManager(),
                mActivity.getResources(),
                doc,
                mModel,
                fromPicker).build();

        if (intent != null) {
            // TODO: un-work around issue b/24963914. Should be fixed soon.
            try {
                mActivity.startActivity(intent);
                return true;
            } catch (SecurityException e) {
                // Carry on to regular view mode.
                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
            }
        }

        return false;
    }


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

        return false;
    }

    private boolean isManagedDownload(DocumentInfo doc) {
        // Anything on downloads goes through the back through downloads manager
        // (that's the MANAGE_DOCUMENT bit).
        // This is done for two reasons:
        // 1) The file in question might be a failed/queued or otherwise have some
        //    specialized download handling.
        // 2) For APKs, the download manager will add on some important security stuff
        //    like origin URL.
        // 3) For partial files, the download manager will offer to restart/retry downloads.

        // All other files not on downloads, event APKs, would get no benefit from this
        // treatment, thusly the "isDownloads" check.

        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
        // files in archives or in child folders. Also, if the activity is already browsing
        // a ZIP from downloads, then skip MANAGE_DOCUMENTS.
        if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
                && mState.stack.size() > 1) {
            // viewing the contents of an archive.
            return false;
        }

        // management is only supported in Downloads root or downloaded files show in Recent root.
        if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) {
            // only on APKs or partial files.
            return MimeTypes.isApkType(doc.mimeType) || doc.isPartial();
        }

        return false;
    }

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

        // Downloads has traditionally added the WRITE permission
        // in the TrampolineActivity. Since this behavior is long
        // established, we set the same permission for non-managed files
        // This ensures consistent behavior between the Downloads root
        // and other roots.
        int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP;
        if (doc.isWriteSupported()) {
            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        }
        intent.setFlags(flags);

        return intent;
    }

    @Override
    public boolean previewItem(ItemDetails<String> doc) {
        throw new UnsupportedOperationException("Can't handle preview.");
+1 −3
Original line number Diff line number Diff line
@@ -33,7 +33,6 @@ import com.android.documentsui.Model;
import com.android.documentsui.Model.Update;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Lookup;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;

import java.util.ArrayList;
@@ -140,8 +139,7 @@ final class ModelBackedDocumentsAdapter extends DocumentsAdapter {
        holder.setEnabled(enabled);
        holder.setSelected(mEnv.isSelected(modelId), false);
        holder.setAction(mEnv.getDisplayState().action);
        holder.bindPreviewIcon(Shared.hasQuickViewer(mEnv.getContext())
                        && mEnv.getDisplayState().shouldShowPreview() && enabled,
        holder.bindPreviewIcon(mEnv.getDisplayState().shouldShowPreview() && enabled,
                view -> mEnv.getActionHandler().previewItem(holder.getItemDetails()));

        mEnv.onBindDocumentHolder(holder, cursor);
+1 −191
Original line number Diff line number Diff line
@@ -48,7 +48,6 @@ import com.android.documentsui.DragAndDropManager;
import com.android.documentsui.Injector;
import com.android.documentsui.MetricConsts;
import com.android.documentsui.Metrics;
import com.android.documentsui.Model;
import com.android.documentsui.R;
import com.android.documentsui.TimeoutTask;
import com.android.documentsui.base.DebugFlags;
@@ -73,7 +72,6 @@ import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperations;
import com.android.documentsui.ui.DialogController;

import java.util.ArrayList;
import java.util.List;
@@ -95,11 +93,9 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
    private final ActionModeAddons mActionModeAddons;
    private final Features mFeatures;
    private final ActivityConfig mConfig;
    private final DialogController mDialogs;
    private final DocumentClipper mClipper;
    private final ClipStore mClipStore;
    private final DragAndDropManager mDragAndDropManager;
    private final Model mModel;

    ActionHandler(
            T activity,
@@ -119,11 +115,9 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
        mActionModeAddons = actionModeAddons;
        mFeatures = injector.features;
        mConfig = injector.config;
        mDialogs = injector.dialogs;
        mClipper = clipper;
        mClipStore = clipStore;
        mDragAndDropManager = dragAndDropManager;
        mModel = injector.getModel();
    }

    @Override
@@ -219,7 +213,7 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
    @VisibleForTesting
    public boolean openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
        if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
            onDocumentPicked(doc, type, fallback);
            onDocumentOpened(doc, type, fallback, false);
            mSelectionMgr.clearSelection();
            return !doc.isContainer();
        }
@@ -558,190 +552,6 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
        }
    }

    private void onDocumentPicked(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
        if (doc.isContainer()) {
            openContainerDocument(doc);
            return;
        }

        if (manageDocument(doc)) {
            return;
        }

        // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow
        // PackageManager to install it.  This allows users to install APKs from any root.
        // The Downloads special case is handled above in #manageDocument.
        if (MimeTypes.isApkType(doc.mimeType)) {
            viewDocument(doc);
            return;
        }

        switch (type) {
          case VIEW_TYPE_REGULAR:
            if (viewDocument(doc)) {
                return;
            }
            break;

          case VIEW_TYPE_PREVIEW:
            if (previewDocument(doc)) {
                return;
            }
            break;

          default:
            throw new IllegalArgumentException("Illegal view type.");
        }

        switch (fallback) {
          case VIEW_TYPE_REGULAR:
            if (viewDocument(doc)) {
                return;
            }
            break;

          case VIEW_TYPE_PREVIEW:
            if (previewDocument(doc)) {
                return;
            }
            break;

          case VIEW_TYPE_NONE:
            break;

          default:
            throw new IllegalArgumentException("Illegal fallback view type.");
        }

        // Failed to view including fallback, and it's in an archive.
        if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) {
            mDialogs.showViewInArchivesUnsupported();
        }
    }

    private boolean viewDocument(DocumentInfo doc) {
        if (doc.isPartial()) {
            Log.w(TAG, "Can't view partial file.");
            return false;
        }

        if (doc.isInArchive()) {
            Log.w(TAG, "Can't view files in archives.");
            return false;
        }

        if (doc.isDirectory()) {
            Log.w(TAG, "Can't view directories.");
            return true;
        }

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

        try {
            mActivity.startActivity(intent);
            return true;
        } catch (ActivityNotFoundException e) {
            mDialogs.showNoApplicationFound();
        }
        return false;
    }

    private boolean previewDocument(DocumentInfo doc) {
        if (doc.isPartial()) {
            Log.w(TAG, "Can't view partial file.");
            return false;
        }

        Intent intent = new QuickViewIntentBuilder(
                mActivity.getPackageManager(),
                mActivity.getResources(),
                doc,
                mModel,
                false /* fromPicker */).build();

        if (intent != null) {
            // TODO: un-work around issue b/24963914. Should be fixed soon.
            try {
                mActivity.startActivity(intent);
                return true;
            } catch (SecurityException e) {
                // Carry on to regular view mode.
                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
            }
        }

        return false;
    }

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

        return false;
    }

    private boolean isManagedDownload(DocumentInfo doc) {
        // Anything on downloads goes through the back through downloads manager
        // (that's the MANAGE_DOCUMENT bit).
        // This is done for two reasons:
        // 1) The file in question might be a failed/queued or otherwise have some
        //    specialized download handling.
        // 2) For APKs, the download manager will add on some important security stuff
        //    like origin URL.
        // 3) For partial files, the download manager will offer to restart/retry downloads.

        // All other files not on downloads, event APKs, would get no benefit from this
        // treatment, thusly the "isDownloads" check.

        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
        // files in archives or in child folders. Also, if the activity is already browsing
        // a ZIP from downloads, then skip MANAGE_DOCUMENTS.
        if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
                && mState.stack.size() > 1) {
            // viewing the contents of an archive.
            return false;
        }

        // management is only supported in Downloads root or downloaded files show in Recent root.
        if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) {
            // only on APKs or partial files.
            return MimeTypes.isApkType(doc.mimeType) || doc.isPartial();
        }

        return false;
    }

    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
        // in the TrampolineActivity. Since this behavior is long
        // established, we set the same permission for non-managed files
        // This ensures consistent behavior between the Downloads root
        // and other roots.
        int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP;
        if (doc.isWriteSupported()) {
            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        }
        intent.setFlags(flags);

        return intent;
    }

    @Override
    public void showInspector(DocumentInfo doc) {
        Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR);
+2 −33
Original line number Diff line number Diff line
@@ -27,7 +27,6 @@ import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.content.QuickViewConstants;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
@@ -47,7 +46,6 @@ import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.Injector;
import com.android.documentsui.MetricConsts;
import com.android.documentsui.Metrics;
import com.android.documentsui.Model;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
@@ -57,7 +55,6 @@ import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.files.QuickViewIntentBuilder;
import com.android.documentsui.picker.ActionHandler.Addons;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.roots.ProvidersAccess;
@@ -74,13 +71,9 @@ import javax.annotation.Nullable;
class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionHandler<T> {

    private static final String TAG = "PickerActionHandler";
    private static final String[] PREVIEW_FEATURES = {
            QuickViewConstants.FEATURE_VIEW
    };

    private final Features mFeatures;
    private final ActivityConfig mConfig;
    private final Model mModel;
    private final LastAccessedStorage mLastAccessed;

    private UpdatePickResultTask mUpdatePickResultTask;
@@ -98,7 +91,6 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH

        mConfig = injector.config;
        mFeatures = injector.features;
        mModel = injector.getModel();
        mLastAccessed = lastAccessed;
        mUpdatePickResultTask = new UpdatePickResultTask(
            activity.getApplicationContext(), mInjector.pickResult);
@@ -346,32 +338,9 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH
                    + details.getSelectionKey());
            return false;
        }
        return priviewDocument(doc);

    }

    @VisibleForTesting
    boolean priviewDocument(DocumentInfo doc) {
        Intent intent = new QuickViewIntentBuilder(
                mActivity.getPackageManager(),
                mActivity.getResources(),
                doc,
                mModel,
                true /* fromPicker */).build();

        if (intent != null) {
            try {
                mActivity.startActivity(intent);
                return true;
            } catch (SecurityException e) {
                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
            }
        } else {
            Log.e(TAG, "Quick view intetn is null");
        }

        mInjector.dialogs.showNoApplicationFound();
        return false;
        onDocumentOpened(doc, VIEW_TYPE_PREVIEW, VIEW_TYPE_REGULAR, true);
        return !doc.isContainer();
    }

    void pickDocument(FragmentManager fm, DocumentInfo pickTarget) {
+21 −1
Original line number Diff line number Diff line
@@ -587,10 +587,30 @@ public class ActionHandlerTest {
        mActivity.resources.setQuickViewerPackage("corptropolis.viewer");
        mActivity.currentRoot = TestProvidersAccess.HOME;

        mHandler.priviewDocument(TestEnv.FILE_GIF);
        mHandler.onDocumentOpened(TestEnv.FILE_GIF, ActionHandler.VIEW_TYPE_PREVIEW,
                ActionHandler.VIEW_TYPE_REGULAR, true);
        mActivity.assertActivityStarted(Intent.ACTION_QUICK_VIEW);
    }

    @Test
    public void testPreviewItem_archives() throws Exception {
        mActivity.resources.setQuickViewerPackage("corptropolis.viewer");
        mActivity.currentRoot = TestProvidersAccess.HOME;

        mHandler.onDocumentOpened(TestEnv.FILE_ARCHIVE, ActionHandler.VIEW_TYPE_PREVIEW,
                ActionHandler.VIEW_TYPE_REGULAR, true);
        mActivity.assertActivityStarted(Intent.ACTION_QUICK_VIEW);
    }

    @Test
    public void testPreviewItem_noQuickViewer() throws Exception {
        mActivity.currentRoot = TestProvidersAccess.HOME;

        mHandler.onDocumentOpened(TestEnv.FILE_GIF, ActionHandler.VIEW_TYPE_PREVIEW,
                ActionHandler.VIEW_TYPE_REGULAR, true);
        mActivity.assertActivityStarted(Intent.ACTION_VIEW);
    }

    private void testInitLocationDefaultToRecentsOnAction(@ActionType int action)
            throws Exception {
        mEnv.state.action = action;