/*
* Copyright (C) 2012 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import com.cyanogenmod.filemanager.R;
import com.cyanogenmod.filemanager.activities.ShortcutActivity;
import com.cyanogenmod.filemanager.model.FileSystemObject;
import com.cyanogenmod.filemanager.model.RegularFile;
import com.cyanogenmod.filemanager.ui.dialogs.AssociationsDialog;
import com.cyanogenmod.filemanager.util.DialogHelper;
import com.cyanogenmod.filemanager.util.ExceptionUtil;
import com.cyanogenmod.filemanager.util.FileHelper;
import com.cyanogenmod.filemanager.util.MimeTypeHelper;
import com.cyanogenmod.filemanager.util.MimeTypeHelper.MimeTypeCategory;
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;
/**
* A class with the convenience methods for resolve intents related actions
*/
public final class IntentsActionPolicy extends ActionsPolicy {
private static final String TAG = "IntentsActionPolicy"; //$NON-NLS-1$
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
*/
public static final String EXTRA_INTERNAL_ACTION =
"com.cyanogenmod.filemanager.extra.INTERNAL_ACTION"; //$NON-NLS-1$
/**
* Category for all the internal app viewers
*/
public static final String CATEGORY_INTERNAL_VIEWER =
"com.cyanogenmod.filemanager.category.INTERNAL_VIEWER"; //$NON-NLS-1$
/**
* Category for all the app editor
*/
public static final String CATEGORY_EDITOR =
"com.cyanogenmod.filemanager.category.EDITOR"; //$NON-NLS-1$
/**
* Method that opens a {@link FileSystemObject} with the default registered application
* by the system, or ask the user for select a registered application.
*
* @param ctx The current context
* @param fso The file system object
* @param choose If allow the user to select the application to open with
* @param onCancelListener The cancel listener
* @param onDismissListener The dismiss listener
*/
public static void openFileSystemObject(
final Context ctx, final FileSystemObject fso, final boolean choose,
OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
try {
// Create the intent to open the file
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
// Obtain the mime/type and passed it to intent
String mime = MimeTypeHelper.getMimeType(ctx, fso);
File file = new File(fso.getFullPath());
if (mime != null) {
intent.setDataAndType(Uri.fromFile(file), mime);
} else {
intent.setData(Uri.fromFile(file));
}
// Resolve the intent
resolveIntent(
ctx,
intent,
choose,
createInternalIntents(ctx, fso),
0,
R.string.associations_dialog_openwith_title,
R.string.associations_dialog_openwith_action,
true, onCancelListener, onDismissListener);
} catch (Exception e) {
ExceptionUtil.translateException(ctx, e);
}
}
/**
* Method that sends a {@link FileSystemObject} with the default registered application
* by the system, or ask the user for select a registered application.
*
* @param ctx The current context
* @param fso The file system object
* @param onCancelListener The cancel listener
* @param onDismissListener The dismiss listener
*/
public static void sendFileSystemObject(
final Context ctx, final FileSystemObject fso,
OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
try {
// Create the intent to
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType(MimeTypeHelper.getMimeType(ctx, fso));
Uri uri = Uri.fromFile(new File(fso.getFullPath()));
intent.putExtra(Intent.EXTRA_STREAM, uri);
// Resolve the intent
resolveIntent(
ctx,
intent,
false,
null,
0,
R.string.associations_dialog_sendwith_title,
R.string.associations_dialog_sendwith_action,
false, onCancelListener, onDismissListener);
} catch (Exception e) {
ExceptionUtil.translateException(ctx, e);
}
}
/**
* Method that resolve
*
* @param ctx The current context
* @param intent The intent to resolve
* @param choose If allow the user to select the application to select the registered
* application. If no preferred app or more than one exists the dialog is shown.
* @param internals The list of internals intents that can handle the action
* @param icon The icon of the dialog
* @param title The title of the dialog
* @param action The button title of the dialog
* @param allowPreferred If allow the user to mark the selected app as preferred
* @param onCancelListener The cancel listener
* @param onDismissListener The dismiss listener
*/
private static void resolveIntent(
Context ctx, Intent intent, boolean choose, List<Intent> internals,
int icon, int title, int action, boolean allowPreferred,
OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
//Retrieve the activities that can handle the file
final PackageManager packageManager = ctx.getPackageManager();
if (DEBUG) {
intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
}
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;
if (internals != null) {
int cc = internals.size();
for (int i = 0; i < cc; i++) {
Intent ii = internals.get(i);
List<ResolveInfo> ie =
packageManager.
queryIntentActivities(ii, 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 (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, rie);
count++;
}
}
}
// 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 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;
}
// If we have multiples apps and there is not a preferred application then show
// open with dialog
AssociationsDialog dialog =
new AssociationsDialog(
ctx,
icon,
ctx.getString(title),
ctx.getString(action),
intent,
info,
mPreferredInfo,
allowPreferred,
onCancelListener,
onDismissListener);
dialog.show();
}
/**
* Method that creates a shortcut in the desktop of the device of {@link FileSystemObject}.
*
* @param ctx The current context
* @param fso The file system object
*/
public static void createShortcut(Context ctx, FileSystemObject fso) {
try {
// Create the intent that will handle the shortcut
Intent shortcutIntent = new Intent(ctx, ShortcutActivity.class);
shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (FileHelper.isDirectory(fso)) {
shortcutIntent.putExtra(
ShortcutActivity.EXTRA_TYPE,ShortcutActivity.SHORTCUT_TYPE_NAVIGATE);
} else {
shortcutIntent.putExtra(
ShortcutActivity.EXTRA_TYPE, ShortcutActivity.SHORTCUT_TYPE_OPEN);
}
shortcutIntent.putExtra(ShortcutActivity.EXTRA_FSO, fso.getFullPath());
// Obtain the icon drawable (don't use here the themeable drawable)
String resid = MimeTypeHelper.getIcon(ctx, fso);
int dwid =
ResourcesHelper.getIdentifier(
ctx.getResources(), "drawable", resid); //$NON-NLS-1$
// The intent to send to broadcast for register the shortcut intent
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, fso.getName());
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(ctx, dwid));
intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); //$NON-NLS-1$
ctx.sendBroadcast(intent);
// Show the confirmation
DialogHelper.showToast(
ctx, R.string.shortcut_creation_success_msg, Toast.LENGTH_SHORT);
} catch (Exception e) {
Log.e(TAG, "Failed to create the shortcut", e); //$NON-NLS-1$
DialogHelper.showToast(
ctx, R.string.shortcut_creation_failed_msg, Toast.LENGTH_SHORT);
}
}
/**
* This method creates a list of internal activities that could handle the fso.
*
* @param ctx The current context
* @param fso The file system object to open
*/
private static List<Intent> createInternalIntents(Context ctx, FileSystemObject fso) {
List<Intent> intents = new ArrayList<Intent>();
intents.addAll(createEditorIntent(ctx, fso));
return intents;
}
/**
* This method creates a list of internal activities for editing files
*
* @param ctx The current context
* @param fso FileSystemObject
*/
private static List<Intent> createEditorIntent(Context ctx, FileSystemObject fso) {
List<Intent> intents = new ArrayList<Intent>();
MimeTypeCategory category = MimeTypeHelper.getCategory(ctx, fso);
//- Internal Editor. This editor can handle TEXT and NONE mime categories but
// not system files, directories, ..., only regular files (no symlinks)
if (fso instanceof RegularFile &&
(category.compareTo(MimeTypeCategory.NONE) == 0 ||
category.compareTo(MimeTypeCategory.EXEC) == 0 ||
category.compareTo(MimeTypeCategory.TEXT) == 0)) {
Intent editorIntent = new Intent();
editorIntent.setAction(Intent.ACTION_VIEW);
editorIntent.addCategory(CATEGORY_INTERNAL_VIEWER);
editorIntent.addCategory(CATEGORY_EDITOR);
intents.add(editorIntent);
}
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);
}
}