2 * Copyright (C) 2012 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.cyanogenmod.filemanager.ui.policy;
19 import android.content.ComponentName;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.DialogInterface.OnCancelListener;
23 import android.content.DialogInterface.OnDismissListener;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.MediaStore;
31 import android.util.Log;
32 import android.widget.Toast;
34 import com.cyanogenmod.filemanager.R;
35 import com.cyanogenmod.filemanager.activities.ShortcutActivity;
36 import com.cyanogenmod.filemanager.console.secure.SecureConsole;
37 import com.cyanogenmod.filemanager.model.FileSystemObject;
38 import com.cyanogenmod.filemanager.model.RegularFile;
39 import com.cyanogenmod.filemanager.providers.SecureResourceProvider;
40 import com.cyanogenmod.filemanager.providers.SecureResourceProvider.AuthorizationResource;
41 import com.cyanogenmod.filemanager.ui.dialogs.AssociationsDialog;
42 import com.cyanogenmod.filemanager.util.DialogHelper;
43 import com.cyanogenmod.filemanager.util.ExceptionUtil;
44 import com.cyanogenmod.filemanager.util.FileHelper;
45 import com.cyanogenmod.filemanager.util.MediaHelper;
46 import com.cyanogenmod.filemanager.util.MimeTypeHelper;
47 import com.cyanogenmod.filemanager.util.MimeTypeHelper.MimeTypeCategory;
48 import com.cyanogenmod.filemanager.util.ResourcesHelper;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.Comparator;
54 import java.util.List;
57 * A class with the convenience methods for resolve intents related actions
59 public final class IntentsActionPolicy extends ActionsPolicy {
61 private static final String TAG = "IntentsActionPolicy"; //$NON-NLS-1$
63 private static boolean DEBUG = false;
65 // The preferred package when sorting intents
66 private static final String PREFERRED_PACKAGE = "com.cyanogenmod.filemanager"; //$NON-NLS-1$
69 * Extra field for the internal action
71 public static final String EXTRA_INTERNAL_ACTION =
72 "com.cyanogenmod.filemanager.extra.INTERNAL_ACTION"; //$NON-NLS-1$
75 * Category for all the internal app viewers
77 public static final String CATEGORY_INTERNAL_VIEWER =
78 "com.cyanogenmod.filemanager.category.INTERNAL_VIEWER"; //$NON-NLS-1$
81 * Category for all the app editor
83 public static final String CATEGORY_EDITOR =
84 "com.cyanogenmod.filemanager.category.EDITOR"; //$NON-NLS-1$
87 * The package name of Gallery2.
89 public static final String GALLERY2_PACKAGE = "com.android.gallery3d";
92 * Method that opens a {@link FileSystemObject} with the default registered application
93 * by the system, or ask the user for select a registered application.
95 * @param ctx The current context
96 * @param fso The file system object
97 * @param choose If allow the user to select the application to open with
98 * @param onCancelListener The cancel listener
99 * @param onDismissListener The dismiss listener
101 public static void openFileSystemObject(
102 final Context ctx, final FileSystemObject fso, final boolean choose,
103 OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
105 // Create the intent to open the file
106 Intent intent = new Intent();
107 intent.setAction(android.content.Intent.ACTION_VIEW);
109 // Obtain the mime/type and passed it to intent
110 String mime = MimeTypeHelper.getMimeType(ctx, fso);
112 intent.setDataAndType(getUriFromFile(ctx, fso), mime);
114 intent.setData(getUriFromFile(ctx, fso));
117 // Resolve the intent
122 createInternalIntents(ctx, fso),
124 R.string.associations_dialog_openwith_title,
125 R.string.associations_dialog_openwith_action,
126 true, onCancelListener, onDismissListener);
128 } catch (Exception e) {
129 ExceptionUtil.translateException(ctx, e);
134 * Method that sends a {@link FileSystemObject} with the default registered application
135 * by the system, or ask the user for select a registered application.
137 * @param ctx The current context
138 * @param fso The file system object
139 * @param onCancelListener The cancel listener
140 * @param onDismissListener The dismiss listener
142 public static void sendFileSystemObject(
143 final Context ctx, final FileSystemObject fso,
144 OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
146 // Create the intent to
147 Intent intent = new Intent();
148 intent.setAction(android.content.Intent.ACTION_SEND);
149 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
150 intent.setType(MimeTypeHelper.getMimeType(ctx, fso));
151 Uri uri = getUriFromFile(ctx, fso);
152 intent.putExtra(Intent.EXTRA_STREAM, uri);
154 // Resolve the intent
161 R.string.associations_dialog_sendwith_title,
162 R.string.associations_dialog_sendwith_action,
163 false, onCancelListener, onDismissListener);
165 } catch (Exception e) {
166 ExceptionUtil.translateException(ctx, e);
171 * Method that sends a {@link FileSystemObject} with the default registered application
172 * by the system, or ask the user for select a registered application.
174 * @param ctx The current context
175 * @param fsos The file system objects
176 * @param onCancelListener The cancel listener
177 * @param onDismissListener The dismiss listener
179 public static void sendMultipleFileSystemObject(
180 final Context ctx, final List<FileSystemObject> fsos,
181 OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
183 // Create the intent to
184 Intent intent = new Intent();
185 intent.setAction(android.content.Intent.ACTION_SEND_MULTIPLE);
186 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
188 // Create an array list of the uris to send
189 ArrayList<Uri> uris = new ArrayList<Uri>();
190 int cc = fsos.size();
191 String lastMimeType = null;
192 boolean sameMimeType = true;
193 for (int i = 0; i < cc; i++) {
194 FileSystemObject fso = fsos.get(i);
196 // Folders are not allowed
197 if (FileHelper.isDirectory(fso)) continue;
199 // Check if we can use a unique mime/type
200 String mimeType = MimeTypeHelper.getMimeType(ctx, fso);
201 if (mimeType == null) {
202 sameMimeType = false;
205 (mimeType != null && lastMimeType != null &&
206 mimeType.compareTo(lastMimeType) != 0)) {
207 sameMimeType = false;
209 lastMimeType = mimeType;
212 uris.add(getUriFromFile(ctx, fso));
215 intent.setType(lastMimeType);
217 intent.setType(MimeTypeHelper.ALL_MIME_TYPES);
219 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
221 // Resolve the intent
228 R.string.associations_dialog_sendwith_title,
229 R.string.associations_dialog_sendwith_action,
230 false, onCancelListener, onDismissListener);
232 } catch (Exception e) {
233 ExceptionUtil.translateException(ctx, e);
238 * Method that resolve
240 * @param ctx The current context
241 * @param intent The intent to resolve
242 * @param choose If allow the user to select the application to select the registered
243 * application. If no preferred app or more than one exists the dialog is shown.
244 * @param internals The list of internals intents that can handle the action
245 * @param icon The icon of the dialog
246 * @param title The title of the dialog
247 * @param action The button title of the dialog
248 * @param allowPreferred If allow the user to mark the selected app as preferred
249 * @param onCancelListener The cancel listener
250 * @param onDismissListener The dismiss listener
252 private static void resolveIntent(
253 Context ctx, Intent intent, boolean choose, List<Intent> internals,
254 int icon, int title, int action, boolean allowPreferred,
255 OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
256 //Retrieve the activities that can handle the file
257 final PackageManager packageManager = ctx.getPackageManager();
259 intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
261 List<ResolveInfo> info =
263 queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
264 Collections.sort(info, new Comparator<ResolveInfo>() {
266 public int compare(ResolveInfo lhs, ResolveInfo rhs) {
268 lhs.activityInfo.packageName.compareTo(PREFERRED_PACKAGE) == 0;
270 rhs.activityInfo.packageName.compareTo(PREFERRED_PACKAGE) == 0;
271 if (isLshCMFM && !isRshCMFM) {
274 if (!isLshCMFM && isRshCMFM) {
277 return lhs.activityInfo.name.compareTo(rhs.activityInfo.name);
281 // Add the internal editors
283 if (internals != null) {
284 int cc = internals.size();
285 for (int i = 0; i < cc; i++) {
286 Intent ii = internals.get(i);
287 List<ResolveInfo> ie =
289 queryIntentActivities(ii, 0);
291 ResolveInfo rie = ie.get(0);
293 // Only if the internal is not in the query list
294 boolean exists = false;
295 int ccc = info.size();
296 for (int j = 0; j < ccc; j++) {
297 ResolveInfo ri = info.get(j);
298 if (ri.activityInfo.packageName.compareTo(
299 rie.activityInfo.packageName) == 0 &&
300 ri.activityInfo.name.compareTo(
301 rie.activityInfo.name) == 0) {
304 if (ri.activityInfo.metaData == null) {
305 ri.activityInfo.metaData = new Bundle();
306 ri.activityInfo.metaData.putString(
307 EXTRA_INTERNAL_ACTION, ii.getAction());
308 ri.activityInfo.metaData.putBoolean(
309 CATEGORY_INTERNAL_VIEWER, true);
320 if (rie.activityInfo.metaData == null) {
321 rie.activityInfo.metaData = new Bundle();
322 rie.activityInfo.metaData.putString(EXTRA_INTERNAL_ACTION, ii.getAction());
323 rie.activityInfo.metaData.putBoolean(CATEGORY_INTERNAL_VIEWER, true);
326 // Only one result must be matched
327 info.add(count, rie);
333 // No registered application
334 if (info.size() == 0) {
335 DialogHelper.showToast(ctx, R.string.msgs_not_registered_app, Toast.LENGTH_SHORT);
336 if (onDismissListener != null) {
337 onDismissListener.onDismiss(null);
342 // Retrieve the preferred activity that can handle the file. We only want the
343 // resolved activity if the activity is a preferred activity. Other case, the
344 // resolved activity was never added by addPreferredActivity
345 ResolveInfo mPreferredInfo = findPreferredActivity(ctx, intent, info);
347 // Is a simple open and we have an application that can handle the file?
349 // If we have a preferred application, then use it
350 if (!choose && (mPreferredInfo != null && mPreferredInfo.match != 0)) {
351 ctx.startActivity(getIntentFromResolveInfo(mPreferredInfo, intent));
352 if (onDismissListener != null) {
353 onDismissListener.onDismiss(null);
357 // If there are only one activity (app or internal editor), then use it
358 if (!choose && info.size() == 1) {
359 ResolveInfo ri = info.get(0);
360 ctx.startActivity(getIntentFromResolveInfo(ri, intent));
361 if (onDismissListener != null) {
362 onDismissListener.onDismiss(null);
367 // If we have multiples apps and there is not a preferred application then show
369 AssociationsDialog dialog =
370 new AssociationsDialog(
373 ctx.getString(title),
374 ctx.getString(action),
385 * Method that creates a shortcut in the desktop of the device of {@link FileSystemObject}.
387 * @param ctx The current context
388 * @param fso The file system object
390 public static void createShortcut(Context ctx, FileSystemObject fso) {
392 // Create the intent that will handle the shortcut
393 Intent shortcutIntent = new Intent(ctx, ShortcutActivity.class);
394 shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
395 shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
396 if (FileHelper.isDirectory(fso)) {
397 shortcutIntent.putExtra(
398 ShortcutActivity.EXTRA_TYPE,ShortcutActivity.SHORTCUT_TYPE_NAVIGATE);
400 shortcutIntent.putExtra(
401 ShortcutActivity.EXTRA_TYPE, ShortcutActivity.SHORTCUT_TYPE_OPEN);
403 shortcutIntent.putExtra(ShortcutActivity.EXTRA_FSO, fso.getFullPath());
405 // Obtain the icon drawable (don't use here the themeable drawable)
406 String resid = MimeTypeHelper.getIcon(ctx, fso);
408 ResourcesHelper.getIdentifier(
409 ctx.getResources(), "drawable", resid); //$NON-NLS-1$
411 // The intent to send to broadcast for register the shortcut intent
412 Intent intent = new Intent();
413 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
414 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, fso.getName());
415 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
416 Intent.ShortcutIconResource.fromContext(ctx, dwid));
417 intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); //$NON-NLS-1$
418 ctx.sendBroadcast(intent);
420 // Show the confirmation
421 DialogHelper.showToast(
422 ctx, R.string.shortcut_creation_success_msg, Toast.LENGTH_SHORT);
424 } catch (Exception e) {
425 Log.e(TAG, "Failed to create the shortcut", e); //$NON-NLS-1$
426 DialogHelper.showToast(
427 ctx, R.string.shortcut_creation_failed_msg, Toast.LENGTH_SHORT);
432 * This method creates a list of internal activities that could handle the fso.
434 * @param ctx The current context
435 * @param fso The file system object to open
437 private static List<Intent> createInternalIntents(Context ctx, FileSystemObject fso) {
438 List<Intent> intents = new ArrayList<Intent>();
439 intents.addAll(createEditorIntent(ctx, fso));
444 * This method creates a list of internal activities for editing files
446 * @param ctx The current context
447 * @param fso FileSystemObject
449 private static List<Intent> createEditorIntent(Context ctx, FileSystemObject fso) {
450 List<Intent> intents = new ArrayList<Intent>();
451 MimeTypeCategory category = MimeTypeHelper.getCategory(ctx, fso);
453 //- Internal Editor. This editor can handle TEXT and NONE mime categories but
454 // not system files, directories, ..., only regular files (no symlinks)
455 if (fso instanceof RegularFile &&
456 (category.compareTo(MimeTypeCategory.NONE) == 0 ||
457 category.compareTo(MimeTypeCategory.EXEC) == 0 ||
458 category.compareTo(MimeTypeCategory.TEXT) == 0)) {
459 Intent editorIntent = new Intent();
460 editorIntent.setAction(Intent.ACTION_VIEW);
461 editorIntent.addCategory(CATEGORY_INTERNAL_VIEWER);
462 editorIntent.addCategory(CATEGORY_EDITOR);
463 intents.add(editorIntent);
470 * Method that returns an {@link Intent} from his {@link ResolveInfo}
472 * @param ri The ResolveInfo
473 * @param request The requested intent
474 * @return Intent The intent
476 public static final Intent getIntentFromResolveInfo(ResolveInfo ri, Intent request) {
478 getIntentFromComponentName(
480 ri.activityInfo.applicationInfo.packageName,
481 ri.activityInfo.name),
483 boolean isInternalEditor = isInternalEditor(ri);
484 if (isInternalEditor) {
485 String a = Intent.ACTION_VIEW;
486 if (ri.activityInfo.metaData != null) {
487 a = ri.activityInfo.metaData.getString(
488 IntentsActionPolicy.EXTRA_INTERNAL_ACTION,
493 // Opening image files with Gallery2 will behave incorrectly when started
494 // as a new task. We want to be able to return to CMFM with the back button.
495 if (!(Intent.ACTION_VIEW.equals(intent.getAction())
497 && intent.getData() != null
498 && MediaStore.AUTHORITY.equals(intent.getData().getAuthority()))) {
499 // Create a new stack for the activity
500 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
502 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
505 // Grant access to resources if needed
506 grantSecureAccessIfNeeded(intent, ri);
512 * Method that add grant access to secure resources if needed
514 * @param intent The intent to grant access
515 * @param ri The resolved info associated with the intent
517 public static final void grantSecureAccessIfNeeded(Intent intent, ResolveInfo ri) {
518 // If this intent will be serve by the SecureResourceProvider then this uri must
519 // be granted before we start it, only for external apps. The internal editor
520 // must receive an file scheme uri
521 Uri uri = intent.getData();
522 String authority = null;
524 authority = uri.getAuthority();
525 grantSecureAccess(intent, authority, ri, uri);
526 } else if (intent.getExtras() != null) {
527 Object obj = intent.getExtras().get(Intent.EXTRA_STREAM);
528 if (obj instanceof Uri) {
529 uri = (Uri) intent.getExtras().get(Intent.EXTRA_STREAM);
530 authority = uri.getAuthority();
531 grantSecureAccess(intent, authority, ri, uri);
532 } else if (obj instanceof ArrayList) {
533 ArrayList<Uri> uris = (ArrayList<Uri>) intent.getExtras().get(Intent.EXTRA_STREAM);
535 authority = u.getAuthority();
536 grantSecureAccess(intent, authority, ri, u);
542 private static final void grantSecureAccess(Intent intent, String authority, ResolveInfo ri,
544 if (authority != null && authority.equals(SecureResourceProvider.AUTHORITY)) {
545 boolean isInternalEditor = isInternalEditor(ri);
546 if (isInternalEditor) {
547 // remove the authorization and change request to file scheme
548 AuthorizationResource auth = SecureResourceProvider.revertAuthorization(uri);
549 intent.setData(Uri.fromFile(new File(auth.mFile.getFullPath())));
552 // Grant access to the package
553 SecureResourceProvider.grantAuthorizationUri(uri,
554 ri.activityInfo.applicationInfo.packageName);
560 * Method that returns an {@link Intent} from his {@link ComponentName}
562 * @param cn The ComponentName
563 * @param request The requested intent
564 * @return Intent The intent
566 public static final Intent getIntentFromComponentName(ComponentName cn, Intent request) {
567 Intent intent = new Intent(request);
570 Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
572 Intent.FLAG_ACTIVITY_FORWARD_RESULT |
573 Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
582 * Method that returns if the selected resolve info is about an internal viewer
584 * @param ri The resolve info
585 * @return boolean If the selected resolve info is about an internal viewer
588 public static final boolean isInternalEditor(ResolveInfo ri) {
589 return ri.activityInfo.metaData != null &&
590 ri.activityInfo.metaData.getBoolean(
591 IntentsActionPolicy.CATEGORY_INTERNAL_VIEWER, false);
594 public static final boolean isGallery2(ResolveInfo ri) {
595 return GALLERY2_PACKAGE.equals(ri.activityInfo.packageName);
599 * Method that retrieve the finds the preferred activity, if one exists. In case
600 * of multiple preferred activity exists the try to choose the better
602 * @param ctx The current context
603 * @param intent The query intent
604 * @param info The initial info list
605 * @return ResolveInfo The resolved info
607 private static final ResolveInfo findPreferredActivity(
608 Context ctx, Intent intent, List<ResolveInfo> info) {
610 final PackageManager packageManager = ctx.getPackageManager();
612 // Retrieve the preferred activity that can handle the file. We only want the
613 // resolved activity if the activity is a preferred activity. Other case, the
614 // resolved activity was never added by addPreferredActivity
615 List<ResolveInfo> pref = new ArrayList<ResolveInfo>();
616 int cc = info.size();
617 for (int i = 0; i < cc; i++) {
618 ResolveInfo ri = info.get(i);
619 if (isInternalEditor(ri)) continue;
620 if (ri.activityInfo == null || ri.activityInfo.packageName == null) continue;
621 List<ComponentName> prefActList = new ArrayList<ComponentName>();
622 List<IntentFilter> intentList = new ArrayList<IntentFilter>();
623 IntentFilter filter = new IntentFilter();
624 filter.addAction(intent.getAction());
626 filter.addDataType(intent.getType());
627 } catch (Exception ex) {/**NON BLOCK**/}
628 intentList.add(filter);
629 packageManager.getPreferredActivities(
630 intentList, prefActList, ri.activityInfo.packageName);
631 if (prefActList.size() > 0) {
636 // No preferred activity is selected
637 if (pref.size() == 0) {
641 // Sort and return the first activity
642 Collections.sort(pref, new Comparator<ResolveInfo>() {
644 public int compare(ResolveInfo lhs, ResolveInfo rhs) {
645 if (lhs.priority > rhs.priority) {
647 } else if (lhs.priority < rhs.priority) {
650 if (lhs.preferredOrder > rhs.preferredOrder) {
652 } else if (lhs.preferredOrder < rhs.preferredOrder) {
655 if (lhs.isDefault && !rhs.isDefault) {
657 } else if (!lhs.isDefault && rhs.isDefault) {
660 if (lhs.match > rhs.match) {
662 } else if (lhs.match > rhs.match) {
672 * Method that returns the best Uri for the file (content uri, file uri, ...)
674 * @param ctx The current context
675 * @param file The file to resolve
677 private static Uri getUriFromFile(Context ctx, FileSystemObject fso) {
678 // If the passed object is secure file then we have to provide access with
679 // the internal resource provider
680 if (fso.isSecure() && SecureConsole.isVirtualStorageResource(fso.getFullPath())
681 && fso instanceof RegularFile) {
682 RegularFile file = (RegularFile) fso;
683 return SecureResourceProvider.createAuthorizationUri(file);
686 // Try to resolve media data or return a file uri
687 final File file = new File(fso.getFullPath());
688 Uri uri = MediaHelper.fileToContentUri(ctx, file);
690 uri = Uri.fromFile(file);