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

Commit 24665762 authored by Jorge Ruesga's avatar Jorge Ruesga
Browse files

Issue 6606: CM File Manager will not Remember Selection for "Open With" menu

Issue: http://code.google.com/p/cyanogenmod/issues/detail?id=6606

This patch makes the next changes:

* Make the internal editor exportable. Now it can be treated as another activity and can be mark
  as preferred activity, but only for text/* and some text mime/types. For undefined mime/types
  categories, the internal editor is still used in a non preferred mode (internal editor cannot
  be marked as preferred)
* When the internal editor in a non preferred mode is selected, 'remember' checkbox is hidden.
* Improve preferred activity resolution
* Allow clear a preferred activity on the open with dialog (when 'remember' checkbox is unchecked)
* For better compatibility, the internal editor now ignores the ACTION_EDIT action, so opened
  files are always editables (with the exception of binary files that they are opened always as
  read-only)
* Improved onIntentSelected with better NPE and internal editor checks

Change-Id: Ie42990a6c0ccbdd4bfab6ec23ae27cc808cac7b7
parent 0f3469ab
Loading
Loading
Loading
Loading
+18 −2
Original line number Diff line number Diff line
@@ -153,10 +153,26 @@
      android:name=".activities.EditorActivity"
      android:label="@string/editor"
      android:configChanges="orientation|keyboardHidden|screenSize"
      android:icon="@drawable/ic_launcher_editor"
      android:exported="false">
      android:icon="@drawable/ic_launcher_editor">
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <action android:name="android.intent.action.EDIT" />
        <category android:name="android.intent.category.DEFAULT" />

        <data android:scheme="file" />
        <data android:mimeType="text/*" />
        <data android:mimeType="application/javascript" />
        <data android:mimeType="application/json" />
        <data android:mimeType="application/xhtml+xml" />
        <data android:mimeType="application/xml" />
        <data android:mimeType="application/x-msdownload" />
        <data android:mimeType="application/x-csh" />
        <data android:mimeType="application/x-sh" />
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <action android:name="android.intent.action.EDIT" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="com.cyanogenmod.filemanager.category.INTERNAL_VIEWER" />
        <category android:name="com.cyanogenmod.filemanager.category.EDITOR" />
      </intent-filter>
+4 −1
Original line number Diff line number Diff line
@@ -459,7 +459,10 @@ public class EditorActivity extends Activity implements TextWatcher {
                    this, R.string.editor_invalid_file_msg, Toast.LENGTH_SHORT);
            return;
        }
        this.mReadOnly = (action.compareTo(Intent.ACTION_VIEW) == 0);
        // This var should be set depending on ACTION_VIEW or ACTION_EDIT action, but for
        // better compatibility, IntentsActionPolicy use always ACTION_VIEW, so we have
        // to ignore this check here
        this.mReadOnly = false;

        // Read the intent and check that is has a valid request
        String path = getIntent().getData().getPath();
+105 −109
Original line number Diff line number Diff line
@@ -127,8 +127,7 @@ public class AssociationsDialog implements OnItemClickListener {
     */
    private void init(int icon, String title, String action,
            OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
        boolean isPlatformSigned =
                AndroidHelper.isAppPlatformSignature(this.mContext);
        boolean isPlatformSigned = AndroidHelper.isAppPlatformSignature(this.mContext);

        //Create the layout, and retrieve the views
        LayoutInflater li =
@@ -138,7 +137,9 @@ public class AssociationsDialog implements OnItemClickListener {
        this.mRemember.setVisibility(
                isPlatformSigned && this.mAllowPreferred ? View.VISIBLE : View.GONE);
        this.mGrid = (GridView)v.findViewById(R.id.associations_gridview);
        this.mGrid.setAdapter(new AssociationsAdapter(this.mContext, this.mIntents, this));
        AssociationsAdapter adapter =
                new AssociationsAdapter(this.mContext, this.mIntents, this);
        this.mGrid.setAdapter(adapter);

        // Ensure a default title dialog
        String dialogTitle = title;
@@ -164,27 +165,9 @@ public class AssociationsDialog implements OnItemClickListener {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        ResolveInfo ri = getSelected();
                        Intent intent = new Intent(AssociationsDialog.this.mRequestIntent);
                        if (isInternalEditor(ri)) {
                            // The action for internal editors (for default VIEW)
                            String a = Intent.ACTION_VIEW;
                            if (ri.activityInfo.metaData != null) {
                                a = ri.activityInfo.metaData.getString(
                                        IntentsActionPolicy.EXTRA_INTERNAL_ACTION,
                                        Intent.ACTION_VIEW);
                            }
                            intent.setAction(a);
                        }
                        intent.setFlags(
                                intent.getFlags() &~
                                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
                        intent.addFlags(
                                Intent.FLAG_ACTIVITY_FORWARD_RESULT |
                                Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
                        intent.setComponent(
                                new ComponentName(
                                        ri.activityInfo.applicationInfo.packageName,
                                        ri.activityInfo.name));
                        Intent intent =
                                IntentsActionPolicy.getIntentFromResolveInfo(
                                        ri, AssociationsDialog.this.mRequestIntent);

                        // Open the intent (and remember the action is the check is marked)
                        onIntentSelected(
@@ -228,6 +211,16 @@ public class AssociationsDialog implements OnItemClickListener {
        deselectAll();
        ((ViewGroup)view).setSelected(true);

        // Internal editors can be associated
        boolean isPlatformSigned = AndroidHelper.isAppPlatformSignature(this.mContext);
        if (isPlatformSigned && this.mAllowPreferred) {
            ResolveInfo ri = getSelected();
            this.mRemember.setVisibility(
                    IntentsActionPolicy.isInternalEditor(ri) ?
                           View.INVISIBLE :
                           View.VISIBLE);
        }

        // Enable action button
        this.mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
    }
@@ -255,10 +248,7 @@ public class AssociationsDialog implements OnItemClickListener {
                        if (item != null) {
                            if (!item.isSelected()) {
                                onItemClick(null, item, i, item.getId());

                                // Not allow to revert remember status
                                this.mRemember.setChecked(true);
                                this.mRemember.setEnabled(false);
                                ret = false;
                            } else {
                                this.mLoaded = true;
@@ -348,7 +338,29 @@ public class AssociationsDialog implements OnItemClickListener {
     */
    @SuppressWarnings({"deprecation"})
    void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) {
        if (remember && !isInternalEditor(ri) && ri.filter != null) {

        boolean isPlatformSigned = AndroidHelper.isAppPlatformSignature(this.mContext);

        // Register preferred association is only allowed by platform signature
        // The app will be signed with this signature, but when is launch from
        // inside ADT, the app is signed with testkey.
        if (isPlatformSigned && this.mAllowPreferred) {

            PackageManager pm = this.mContext.getPackageManager();

            // Remove preferred application if user don't want to remember it
            if (this.mPreferred != null && !remember) {
                pm.clearPackagePreferredActivities(
                        this.mPreferred.activityInfo.packageName);
            }

            // Associate the activity under these circumstances:
            //  - The user has selected the remember option
            //  - The selected intent is not an internal editor (internal editors are private and
            //    can be associated)
            //  - The selected intent is not the current preferred selection
            if (remember && !IntentsActionPolicy.isInternalEditor(ri) && !isPreferredSelected()) {

                // Build a reasonable intent filter, based on what matched.
                IntentFilter filter = new IntentFilter();

@@ -387,7 +399,11 @@ public class AssociationsDialog implements OnItemClickListener {

                        // Look through the resolved filter to determine which part
                        // of it matched the original Intent.
                    Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
                        // ri.filter should not be null here because the activity matches a filter
                        // Anyway protect the access
                        if (ri.filter != null) {
                            Iterator<IntentFilter.AuthorityEntry> aIt =
                                                        ri.filter.authoritiesIterator();
                            if (aIt != null) {
                                while (aIt.hasNext()) {
                                    IntentFilter.AuthorityEntry a = aIt.next();
@@ -412,18 +428,13 @@ public class AssociationsDialog implements OnItemClickListener {
                            }
                        }
                    }
                }

            // Register preferred association is only allowed by platform signature
            // The app will be signed with this signature, but when is launch from
            // inside ADT, the app is signed with testkey.
            // Ignore it if the preferred can be saved. Only notify the user and open the
            // intent
            boolean isPlatformSigned =
                    AndroidHelper.isAppPlatformSignature(this.mContext);
            if (isPlatformSigned && this.mAllowPreferred) {
                if (filter != null && !isPreferredSelected()) {
                // If we don't have a filter then don't try to associate
                if (filter != null) {
                    try {
                        AssociationsAdapter adapter = (AssociationsAdapter)this.mGrid.getAdapter();
                        AssociationsAdapter adapter =
                                (AssociationsAdapter)this.mGrid.getAdapter();
                        final int cc = adapter.getCount();
                        ComponentName[] set = new ComponentName[cc];
                        int bestMatch = 0;
@@ -437,13 +448,12 @@ public class AssociationsDialog implements OnItemClickListener {
                            }
                        }

                        PackageManager pm = this.mContext.getPackageManager();

                        // The only way i found to ensure of the use of the preferred activity
                        // selected is to clear preferred activity associations
                        // Maybe it's necessary also remove the rest of activities?
                        if (this.mPreferred != null) {
                            pm.clearPackagePreferredActivities(
                                    this.mPreferred.activityInfo.packageName);
                        }

                        // This is allowed for now in AOSP, but probably in the future this will
                        // not work at all
@@ -465,18 +475,4 @@ public class AssociationsDialog implements OnItemClickListener {
            this.mContext.startActivity(intent);
        }
    }

    /**
     * Method that returns if the selected resolve info is about an internal viewer
     *
     * @param ri The resolve info
     * @return boolean  If the selected resolve info is about an internal viewer
     * @hide
     */
    @SuppressWarnings("static-method")
    boolean isInternalEditor(ResolveInfo ri) {
        return ri.activityInfo.metaData != null &&
                ri.activityInfo.metaData.getBoolean(
                        IntentsActionPolicy.CATEGORY_INTERNAL_VIEWER, false);
    }
}
+202 −22
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.cyanogenmod.filemanager.ui.policy;

import android.content.ComponentName;
import android.content.Context;
import android.content.IntentFilter;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
@@ -41,6 +43,8 @@ import com.cyanogenmod.filemanager.util.ResourcesHelper;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
@@ -52,6 +56,9 @@ public final class IntentsActionPolicy extends ActionsPolicy {

    private static boolean DEBUG = false;

    // The preferred package when sorting intents
    private static final String PREFERRED_PACKAGE = "com.cyanogenmod.filemanager"; //$NON-NLS-1$

    /**
     * Extra field for the internal action
     */
@@ -84,7 +91,7 @@ public final class IntentsActionPolicy extends ActionsPolicy {
            final Context ctx, final FileSystemObject fso, final boolean choose,
            OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
        try {
            // Create the intent to
            // Create the intent to open the file
            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);

@@ -177,6 +184,22 @@ public final class IntentsActionPolicy extends ActionsPolicy {
        List<ResolveInfo> info =
                packageManager.
                    queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        Collections.sort(info, new Comparator<ResolveInfo>() {
            @Override
            public int compare(ResolveInfo lhs, ResolveInfo rhs) {
                boolean isLshCMFM =
                        lhs.activityInfo.packageName.compareTo(PREFERRED_PACKAGE) == 0;
                boolean isRshCMFM =
                        rhs.activityInfo.packageName.compareTo(PREFERRED_PACKAGE) == 0;
                if (isLshCMFM && !isRshCMFM) {
                    return -1;
                }
                if (!isLshCMFM && isRshCMFM) {
                    return 1;
                }
                return lhs.activityInfo.name.compareTo(rhs.activityInfo.name);
            }
        });

        // Add the internal editors
        int count = 0;
@@ -184,47 +207,70 @@ public final class IntentsActionPolicy extends ActionsPolicy {
            int cc = internals.size();
            for (int i = 0; i < cc; i++) {
                Intent ii = internals.get(i);
                List<ResolveInfo> ris =
                List<ResolveInfo> ie =
                        packageManager.
                            queryIntentActivities(ii, 0);
                if (ris.size() > 0) {
                    ResolveInfo ri = ris.get(0);
                if (ie.size() > 0) {
                    ResolveInfo rie = ie.get(0);

                    // Only if the internal is not in the query list
                    boolean exists = false;
                    int ccc = info.size();
                    for (int j = 0; j < ccc; j++) {
                        ResolveInfo ri = info.get(j);
                        if (ri.activityInfo.packageName.compareTo(
                                rie.activityInfo.packageName) == 0 &&
                            ri.activityInfo.name.compareTo(
                                    rie.activityInfo.name) == 0) {
                            exists = true;
                            break;
                        }
                    }
                    if (exists) {
                        continue;
                    }

                    // Mark as internal
                    if (ri.activityInfo.metaData == null) {
                        ri.activityInfo.metaData = new Bundle();
                        ri.activityInfo.metaData.putString(EXTRA_INTERNAL_ACTION, ii.getAction());
                        ri.activityInfo.metaData.putBoolean(CATEGORY_INTERNAL_VIEWER, true);
                    if (rie.activityInfo.metaData == null) {
                        rie.activityInfo.metaData = new Bundle();
                        rie.activityInfo.metaData.putString(EXTRA_INTERNAL_ACTION, ii.getAction());
                        rie.activityInfo.metaData.putBoolean(CATEGORY_INTERNAL_VIEWER, true);
                    }

                    // Only one result must be matched
                    info.add(count, ri);
                    info.add(count, rie);
                    count++;
                }
            }
        }

        // Retrieve the preferred activity that can handle the file
        final ResolveInfo mPreferredInfo = packageManager.resolveActivity(intent, 0);

        // No registered application
        if (info.size() == 0) {
            DialogHelper.showToast(ctx, R.string.msgs_not_registered_app, Toast.LENGTH_SHORT);
            return;
        }

        // Retrieve the preferred activity that can handle the file. We only want the
        // resolved activity if the activity is a preferred activity. Other case, the
        // resolved activity was never added by addPreferredActivity
        ResolveInfo mPreferredInfo = findPreferredActivity(ctx, intent, info);

        // Is a simple open and we have an application that can handle the file?
        if (!choose &&
                ((mPreferredInfo  != null && mPreferredInfo.match != 0) || info.size() == 1)) {
            // But not if the only match is the an internal editor
            ResolveInfo ri = info.get(0);
            if (ri.activityInfo.metaData == null ||
                    !ri.activityInfo.metaData.getBoolean(CATEGORY_INTERNAL_VIEWER, false)) {
                ctx.startActivity(intent);
        //---
        // If we have a preferred application, then use it
        if (!choose && (mPreferredInfo  != null && mPreferredInfo.match != 0)) {
            ctx.startActivity(getIntentFromResolveInfo(mPreferredInfo, intent));
            return;
        }
        // If there are only one activity (app or internal editor), then use it
        if (!choose && info.size() == 1) {
            ResolveInfo ri = info.get(0);
            ctx.startActivity(getIntentFromResolveInfo(ri, intent));
            return;
        }

        // Otherwise, we have to show the open with dialog
        // If we have multiples apps and there is not a preferred application then show
        // open with dialog
        AssociationsDialog dialog =
                new AssociationsDialog(
                        ctx,
@@ -316,7 +362,7 @@ public final class IntentsActionPolicy extends ActionsPolicy {
             category.compareTo(MimeTypeCategory.EXEC) == 0 ||
             category.compareTo(MimeTypeCategory.TEXT) == 0)) {
            Intent editorIntent = new Intent();
            editorIntent.setAction(Intent.ACTION_EDIT);
            editorIntent.setAction(Intent.ACTION_VIEW);
            editorIntent.addCategory(CATEGORY_INTERNAL_VIEWER);
            editorIntent.addCategory(CATEGORY_EDITOR);
            intents.add(editorIntent);
@@ -324,4 +370,138 @@ public final class IntentsActionPolicy extends ActionsPolicy {

        return intents;
    }

    /**
     * Method that returns an {@link Intent} from his {@link ResolveInfo}
     *
     * @param ri The ResolveInfo
     * @param request The requested intent
     * @return Intent The intent
     */
    public static final Intent getIntentFromResolveInfo(ResolveInfo ri, Intent request) {
        Intent intent =
                getIntentFromComponentName(
                    new ComponentName(
                        ri.activityInfo.applicationInfo.packageName,
                        ri.activityInfo.name),
                    request);
        if (isInternalEditor(ri)) {
            String a = Intent.ACTION_VIEW;
            if (ri.activityInfo.metaData != null) {
                a = ri.activityInfo.metaData.getString(
                        IntentsActionPolicy.EXTRA_INTERNAL_ACTION,
                        Intent.ACTION_VIEW);
            }
            intent.setAction(a);
        }
        return intent;
    }

    /**
     * Method that returns an {@link Intent} from his {@link ComponentName}
     *
     * @param cn The ComponentName
     * @param request The requested intent
     * @return Intent The intent
     */
    public static final Intent getIntentFromComponentName(ComponentName cn, Intent request) {
        Intent intent = new Intent(request);
        intent.setFlags(
                intent.getFlags() &~
                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        intent.addFlags(
                Intent.FLAG_ACTIVITY_FORWARD_RESULT |
                Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
        intent.setComponent(
                new ComponentName(
                        cn.getPackageName(),
                        cn.getClassName()));
        return intent;
    }

    /**
     * Method that returns if the selected resolve info is about an internal viewer
     *
     * @param ri The resolve info
     * @return boolean  If the selected resolve info is about an internal viewer
     * @hide
     */
    public static final boolean isInternalEditor(ResolveInfo ri) {
        return ri.activityInfo.metaData != null &&
                ri.activityInfo.metaData.getBoolean(
                        IntentsActionPolicy.CATEGORY_INTERNAL_VIEWER, false);
    }

    /**
     * Method that retrieve the finds the preferred activity, if one exists. In case
     * of multiple preferred activity exists the try to choose the better
     *
     * @param ctx The current context
     * @param intent The query intent
     * @param info The initial info list
     * @return ResolveInfo The resolved info
     */
    private static final ResolveInfo findPreferredActivity(
            Context ctx, Intent intent, List<ResolveInfo> info) {

        final PackageManager packageManager = ctx.getPackageManager();

        // Retrieve the preferred activity that can handle the file. We only want the
        // resolved activity if the activity is a preferred activity. Other case, the
        // resolved activity was never added by addPreferredActivity
        List<ResolveInfo> pref = new ArrayList<ResolveInfo>();
        int cc = info.size();
        for (int i = 0; i < cc; i++) {
            ResolveInfo ri = info.get(i);
            if (isInternalEditor(ri)) continue;
            if (ri.activityInfo == null || ri.activityInfo.packageName == null) continue;
            List<ComponentName> prefActList = new ArrayList<ComponentName>();
            List<IntentFilter> intentList = new ArrayList<IntentFilter>();
            IntentFilter filter = new IntentFilter();
            filter.addAction(intent.getAction());
            try {
                filter.addDataType(intent.getType());
            } catch (Exception ex) {/**NON BLOCK**/}
            intentList.add(filter);
            packageManager.getPreferredActivities(
                    intentList, prefActList, ri.activityInfo.packageName);
            if (prefActList.size() > 0) {
                pref.add(ri);
            }
        }

        // No preferred activity is selected
        if (pref.size() == 0) {
            return null;
        }

        // Sort and return the first activity
        Collections.sort(pref, new Comparator<ResolveInfo>() {
            @Override
            public int compare(ResolveInfo lhs, ResolveInfo rhs) {
                if (lhs.priority > rhs.priority) {
                    return -1;
                } else if (lhs.priority < rhs.priority) {
                    return 1;
                }
                if (lhs.preferredOrder > rhs.preferredOrder) {
                    return -1;
                } else if (lhs.preferredOrder < rhs.preferredOrder) {
                    return 1;
                }
                if (lhs.isDefault && !rhs.isDefault) {
                    return -1;
                } else if (!lhs.isDefault && rhs.isDefault) {
                    return 1;
                }
                if (lhs.match > rhs.match) {
                    return -1;
                } else if (lhs.match > rhs.match) {
                    return 1;
                }
                return 0;
            }
        });
        return pref.get(0);
    }
}