/* * Copyright (C) 2008 The Android Open Source Project * 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.dialogs; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.PatternMatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CheckBox; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.Toast; import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.adapters.AssociationsAdapter; import com.cyanogenmod.filemanager.ui.ThemeManager; import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; import com.cyanogenmod.filemanager.ui.policy.IntentsActionPolicy; import com.cyanogenmod.filemanager.util.AndroidHelper; import com.cyanogenmod.filemanager.util.DialogHelper; import com.cyanogenmod.filemanager.util.ExceptionUtil; import java.util.Iterator; import java.util.List; import java.util.Set; /** * A class that wraps a dialog for showing the list of available intents that can handle an * action. This dialog allows predetermined */ public class AssociationsDialog implements OnItemClickListener { private static final String TAG = "AssociationsDialog"; //$NON-NLS-1$ private final Context mContext; private final List<ResolveInfo> mIntents; private final ResolveInfo mPreferred; private final boolean mAllowPreferred; /** * @hide */ final Intent mRequestIntent; private AlertDialog mDialog; /** * @hide */ GridView mGrid; /** * @hide */ CheckBox mRemember; private boolean mLoaded; /** * Constructor of <code>AssociationsDialog</code>. * * @param context The current context * @param icon The icon of the dialog * @param title The title dialog * @param action The title of the action button * @param requestIntent The original request * @param intents The list of available intents that can handle an action * @param preferred The preferred intent. null if no preferred exists * @param allowPreferred If allow the user to mark the selected app as preferred * @param onCancelListener The cancel listener * @param onDismissListener The dismiss listener */ public AssociationsDialog( Context context, int icon, String title, String action, Intent requestIntent, List<ResolveInfo> intents, ResolveInfo preferred, boolean allowPreferred, OnCancelListener onCancelListener, OnDismissListener onDismissListener) { super(); //Save the data this.mContext = context; this.mRequestIntent = requestIntent; this.mIntents = intents; this.mPreferred = preferred; this.mAllowPreferred = allowPreferred; this.mLoaded = false; //Initialize dialog init(icon, title, action, onCancelListener, onDismissListener); } /** * Method that initializes the dialog. * * @param context The current context * @param icon The icon of the dialog * @param title The title of the dialog * @param action The title of the action button * @param onCancelListener The cancel listener * @param onCancelListener The dismiss listener */ private void init(int icon, String title, String action, OnCancelListener onCancelListener, OnDismissListener onDismissListener) { boolean isPlatformSigned = AndroidHelper.isAppPlatformSignature(this.mContext); //Create the layout, and retrieve the views LayoutInflater li = (LayoutInflater)this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = li.inflate(R.layout.associations_dialog, null, false); this.mRemember = (CheckBox)v.findViewById(R.id.associations_remember); this.mRemember.setVisibility( isPlatformSigned && this.mAllowPreferred ? View.VISIBLE : View.GONE); this.mGrid = (GridView)v.findViewById(R.id.associations_gridview); AssociationsAdapter adapter = new AssociationsAdapter(this.mContext, this.mIntents, this); this.mGrid.setAdapter(adapter); // Ensure a default title dialog String dialogTitle = title; if (dialogTitle == null) { dialogTitle = this.mContext.getString(R.string.associations_dialog_title); } // Apply the current theme Theme theme = ThemeManager.getCurrentTheme(this.mContext); theme.setBackgroundDrawable(this.mContext, v, "background_drawable"); //$NON-NLS-1$ theme.setTextColor(this.mContext, this.mRemember, "text_color"); //$NON-NLS-1$ //Create the dialog this.mDialog = DialogHelper.createDialog( this.mContext, icon, dialogTitle, v); this.mDialog.setButton( DialogInterface.BUTTON_POSITIVE, action, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ResolveInfo ri = getSelected(); Intent intent = IntentsActionPolicy.getIntentFromResolveInfo( ri, AssociationsDialog.this.mRequestIntent); // Open the intent (and remember the action is the check is marked) onIntentSelected( ri, intent, AssociationsDialog.this.mRemember.isChecked()); } }); this.mDialog.setButton( DialogInterface.BUTTON_NEGATIVE, this.mContext.getString(android.R.string.cancel), (DialogInterface.OnClickListener)null); this.mDialog.setOnCancelListener(onCancelListener); this.mDialog.setOnDismissListener(onDismissListener); } /** * Method that shows the dialog. */ public void show() { DialogHelper.delegateDialogShow(this.mContext, this.mDialog); // Set user preferences this.mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); this.mGrid.post(new Runnable() { @Override public void run() { if (!checkUserPreferences()) { // Recall for check user preferences AssociationsDialog.this.mGrid.postDelayed(this, 50L); } } }); } /** * {@inheritDoc} */ @Override 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); } /** * Method that check the user preferences * * @return boolean Indicates if the user preferences was set * @hide */ boolean checkUserPreferences() { boolean ret = false; if (!this.mLoaded) { // Check that the view is loaded if ((ViewGroup)this.mGrid.getChildAt(0) == null) return false; if (this.mPreferred != null) { boolean found = false; int cc = this.mIntents.size(); for (int i = 0; i < cc; i++) { ResolveInfo info = this.mIntents.get(i); if (info.activityInfo.name.equals(this.mPreferred.activityInfo.name)) { // Select the item ViewGroup item = (ViewGroup)this.mGrid.getChildAt(i); if (item != null) { if (!item.isSelected()) { onItemClick(null, item, i, item.getId()); this.mRemember.setChecked(true); ret = false; } else { this.mLoaded = true; ret = true; } } found = true; break; } } // Is there is no user preferences? if (!found) { this.mLoaded = true; ret = true; } } else { // There is no user preferences this.mLoaded = true; ret = true; } } return ret; } /** * Method that returns if the preferred intent is still selected * * @return if the preferred intent is selected */ private boolean isPreferredSelected() { if (this.mPreferred != null) { int cc = this.mIntents.size(); for (int i = 0; i < cc; i++) { ResolveInfo info = this.mIntents.get(i); if (info.activityInfo.name.equals(this.mPreferred.activityInfo.name)) { ViewGroup item = (ViewGroup)this.mGrid.getChildAt(i); if (item != null) { if (item.isSelected()) { return true; } } } } } return false; } /** * Method that deselect all the items of the grid view */ private void deselectAll() { ListAdapter adapter = this.mGrid.getAdapter(); int cc = adapter.getCount(); for (int i = 0; i < cc; i++) { ViewGroup item = (ViewGroup)this.mGrid.getChildAt(i); item.setSelected(false); } } /** * Method that deselect all the items of the grid view * * @return ResolveInfo The selected item * @hide */ ResolveInfo getSelected() { AssociationsAdapter adapter = (AssociationsAdapter)this.mGrid.getAdapter(); int count = adapter.getCount(); for (int i = 0; i < count; i++) { ViewGroup item = (ViewGroup)this.mGrid.getChildAt(i); if (item.isSelected()) { return adapter.getItem(i); } } return null; } /** * Method that opens the associated intent. * * @param ri The resolve information * @param intent The user selection * @param remember If the user selection, must be remembered * @see ResolverActivity (copied from) * @hide */ @SuppressWarnings({"deprecation"}) void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) { 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(); if (intent.getAction() != null) { filter.addAction(intent.getAction()); } Set<String> categories = intent.getCategories(); if (categories != null) { for (String cat : categories) { filter.addCategory(cat); } } 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; } } } 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<IntentFilter.AuthorityEntry> 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<PatternMatcher> 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 we don't have a filter then don't try to associate if (filter != null) { try { AssociationsAdapter adapter = (AssociationsAdapter)this.mGrid.getAdapter(); final int cc = adapter.getCount(); ComponentName[] set = new ComponentName[cc]; int bestMatch = 0; for (int i = 0; i < cc; i++) { ResolveInfo r = adapter.getItem(i); set[i] = new ComponentName( r.activityInfo.packageName, r.activityInfo.name); // Use the match of the selected intent if (intent.getComponent().compareTo(set[i]) == 0) { bestMatch = r.match; } } // The only way i found to ensure of the use of the preferred activity // selected is to clear preferred activity associations 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 pm.addPreferredActivity(filter, bestMatch, set, intent.getComponent()); } catch (Exception e) { // Capture the exception ExceptionUtil.translateException(this.mContext, e, true, false); DialogHelper.showToast( this.mContext, R.string.msgs_action_association_failed, Toast.LENGTH_SHORT); } } } } if (intent != null) { this.mContext.startActivity(intent); } } }