From 2fa90d221eff9dd4843ea65c6fd424730a9a85d2 Mon Sep 17 00:00:00 2001 From: Jorge Ruesga Date: Sun, 18 Nov 2012 04:08:59 +0100 Subject: [PATCH] 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 --- AndroidManifest.xml | 20 +- .../activities/EditorActivity.java | 5 +- .../ui/dialogs/AssociationsDialog.java | 214 ++++++++--------- .../ui/policy/IntentsActionPolicy.java | 224 ++++++++++++++++-- 4 files changed, 329 insertions(+), 134 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bc947fdb0..c890c954a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -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"> + + + + + + + + + + + + + + + + + diff --git a/src/com/cyanogenmod/filemanager/activities/EditorActivity.java b/src/com/cyanogenmod/filemanager/activities/EditorActivity.java index 912c0befb..6b682ad2c 100644 --- a/src/com/cyanogenmod/filemanager/activities/EditorActivity.java +++ b/src/com/cyanogenmod/filemanager/activities/EditorActivity.java @@ -459,7 +459,10 @@ private void readFile() { 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(); diff --git a/src/com/cyanogenmod/filemanager/ui/dialogs/AssociationsDialog.java b/src/com/cyanogenmod/filemanager/ui/dialogs/AssociationsDialog.java index 370043319..096bacdbd 100644 --- a/src/com/cyanogenmod/filemanager/ui/dialogs/AssociationsDialog.java +++ b/src/com/cyanogenmod/filemanager/ui/dialogs/AssociationsDialog.java @@ -127,8 +127,7 @@ public AssociationsDialog( */ 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 @@ private void init(int icon, String title, String action, 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 @@ private void init(int icon, String title, String action, @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 void onItemClick(AdapterView parent, View view, int position, long id) 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 @@ boolean checkUserPreferences() { 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,82 +338,103 @@ ResolveInfo getSelected() { */ @SuppressWarnings({"deprecation"}) void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) { - if (remember && !isInternalEditor(ri) && ri.filter != null) { - // Build a reasonable intent filter, based on what matched. - IntentFilter filter = new IntentFilter(); - if (intent.getAction() != null) { - filter.addAction(intent.getAction()); - } - Set categories = intent.getCategories(); - if (categories != null) { - for (String cat : categories) { - filter.addCategory(cat); - } + 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); } - filter.addCategory(Intent.CATEGORY_DEFAULT); - int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK; - Uri data = intent.getData(); - if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { - String mimeType = intent.resolveType(this.mContext); - if (mimeType != null) { - try { - filter.addDataType(mimeType); - } catch (IntentFilter.MalformedMimeTypeException e) { - Log.w(TAG, e); - filter = null; + // 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(); + + if (intent.getAction() != null) { + filter.addAction(intent.getAction()); + } + Set categories = intent.getCategories(); + if (categories != null) { + for (String cat : categories) { + filter.addCategory(cat); } } - } - if (data != null && data.getScheme() != null && filter != null) { - // We need the data specification if there was no type, - // OR if the scheme is not one of our magical "file:" - // or "content:" schemes (see IntentFilter for the reason). - if (cat != IntentFilter.MATCH_CATEGORY_TYPE - || (!"file".equals(data.getScheme()) //$NON-NLS-1$ - && !"content".equals(data.getScheme()))) { //$NON-NLS-1$ - filter.addDataScheme(data.getScheme()); - - // Look through the resolved filter to determine which part - // of it matched the original Intent. - Iterator aIt = ri.filter.authoritiesIterator(); - if (aIt != null) { - while (aIt.hasNext()) { - IntentFilter.AuthorityEntry a = aIt.next(); - if (a.match(data) >= 0) { - int port = a.getPort(); - filter.addDataAuthority(a.getHost(), - port >= 0 ? Integer.toString(port) : null); - break; - } + filter.addCategory(Intent.CATEGORY_DEFAULT); + + int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK; + Uri data = intent.getData(); + if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { + String mimeType = intent.resolveType(this.mContext); + if (mimeType != null) { + try { + filter.addDataType(mimeType); + } catch (IntentFilter.MalformedMimeTypeException e) { + Log.w(TAG, e); + filter = null; } } - Iterator pIt = ri.filter.pathsIterator(); - if (pIt != null) { - String path = data.getPath(); - while (path != null && pIt.hasNext()) { - PatternMatcher p = pIt.next(); - if (p.match(path)) { - filter.addDataPath(p.getPath(), p.getType()); - break; + } + if (data != null && data.getScheme() != null && filter != null) { + // We need the data specification if there was no type, + // OR if the scheme is not one of our magical "file:" + // or "content:" schemes (see IntentFilter for the reason). + if (cat != IntentFilter.MATCH_CATEGORY_TYPE + || (!"file".equals(data.getScheme()) //$NON-NLS-1$ + && !"content".equals(data.getScheme()))) { //$NON-NLS-1$ + filter.addDataScheme(data.getScheme()); + + // Look through the resolved filter to determine which part + // of it matched the original Intent. + // ri.filter should not be null here because the activity matches a filter + // Anyway protect the access + if (ri.filter != null) { + Iterator aIt = + ri.filter.authoritiesIterator(); + if (aIt != null) { + while (aIt.hasNext()) { + IntentFilter.AuthorityEntry a = aIt.next(); + if (a.match(data) >= 0) { + int port = a.getPort(); + filter.addDataAuthority(a.getHost(), + port >= 0 ? Integer.toString(port) : null); + break; + } + } + } + Iterator pIt = ri.filter.pathsIterator(); + if (pIt != null) { + String path = data.getPath(); + while (path != null && pIt.hasNext()) { + PatternMatcher p = pIt.next(); + if (p.match(path)) { + filter.addDataPath(p.getPath(), p.getType()); + break; + } + } } } } } - } - // 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 @@ void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) { } } - 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? - pm.clearPackagePreferredActivities( - this.mPreferred.activityInfo.packageName); + 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 @@ void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) { 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); - } } diff --git a/src/com/cyanogenmod/filemanager/ui/policy/IntentsActionPolicy.java b/src/com/cyanogenmod/filemanager/ui/policy/IntentsActionPolicy.java index 004719b0e..774509957 100644 --- a/src/com/cyanogenmod/filemanager/ui/policy/IntentsActionPolicy.java +++ b/src/com/cyanogenmod/filemanager/ui/policy/IntentsActionPolicy.java @@ -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 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 static void openFileSystemObject( 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 @@ private static void resolveIntent( List info = packageManager. queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + Collections.sort(info, new Comparator() { + @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 @@ private static void resolveIntent( int cc = internals.size(); for (int i = 0; i < cc; i++) { Intent ii = internals.get(i); - List ris = + List 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 + //--- + // 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); - if (ri.activityInfo.metaData == null || - !ri.activityInfo.metaData.getBoolean(CATEGORY_INTERNAL_VIEWER, false)) { - ctx.startActivity(intent); - return; - } + 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 @@ private static List createEditorIntent(Context ctx, FileSystemObject fso 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 @@ private static List createEditorIntent(Context ctx, FileSystemObject fso 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 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 pref = new ArrayList(); + 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 prefActList = new ArrayList(); + List intentList = new ArrayList(); + 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() { + @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); + } }