OSDN Git Service

Open and Open with actions
authorjruesga <jorge@ruesga.com>
Sat, 6 Oct 2012 10:23:03 +0000 (12:23 +0200)
committerjruesga <jorge@ruesga.com>
Sat, 6 Oct 2012 10:23:03 +0000 (12:23 +0200)
Implementation of Open and Open with actions. This create a custom
dialog that allow show all registered application even when only one
application exists or has a preferred application.
This change requires a platform signature to allow use
addPreferredActivity or removePreferredActivity.
For this is not working. Need be tested and checked inside CM.

18 files changed:
AndroidManifest.xml
res/drawable-hdpi/ic_holo_light_open.png [new file with mode: 0644]
res/drawable-mdpi/ic_holo_light_open.png [new file with mode: 0644]
res/drawable-xhdpi/ic_holo_light_open.png [new file with mode: 0644]
res/drawable/holo_selection.xml [new file with mode: 0644]
res/layout/associations_dialog.xml [new file with mode: 0644]
res/layout/associations_item.xml [new file with mode: 0644]
res/values/dimen.xml
res/values/overlay.xml
res/values/strings.xml
res/xml/command_list.xml
res/xml/filesystem_bookmarks.xml
src/com/cyanogenmod/explorer/ExplorerApplication.java
src/com/cyanogenmod/explorer/activities/BookmarksActivity.java
src/com/cyanogenmod/explorer/adapters/AssociationsAdapter.java [new file with mode: 0644]
src/com/cyanogenmod/explorer/model/Query.java
src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java [new file with mode: 0644]
src/com/cyanogenmod/explorer/ui/policy/ActionsPolicy.java

index 58ff7fb..df5473f 100644 (file)
@@ -25,6 +25,7 @@
 
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
+  <uses-permission android:name="android.permission.SET_PREFERRED_APPLICATIONS" />
 
   <application
     android:name="ExplorerApplication"
diff --git a/res/drawable-hdpi/ic_holo_light_open.png b/res/drawable-hdpi/ic_holo_light_open.png
new file mode 100644 (file)
index 0000000..3db304f
Binary files /dev/null and b/res/drawable-hdpi/ic_holo_light_open.png differ
diff --git a/res/drawable-mdpi/ic_holo_light_open.png b/res/drawable-mdpi/ic_holo_light_open.png
new file mode 100644 (file)
index 0000000..fda13f1
Binary files /dev/null and b/res/drawable-mdpi/ic_holo_light_open.png differ
diff --git a/res/drawable-xhdpi/ic_holo_light_open.png b/res/drawable-xhdpi/ic_holo_light_open.png
new file mode 100644 (file)
index 0000000..d132401
Binary files /dev/null and b/res/drawable-xhdpi/ic_holo_light_open.png differ
diff --git a/res/drawable/holo_selection.xml b/res/drawable/holo_selection.xml
new file mode 100644 (file)
index 0000000..2be8ac5
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ** 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="@android:integer/config_shortAnimTime">
+
+  <item
+    android:drawable="@android:color/holo_blue_dark"
+    android:state_pressed="true"/>
+  <item
+    android:drawable="@android:color/holo_blue_light"
+    android:state_enabled="true"
+    android:state_focused="true"/>
+  <item
+    android:drawable="@android:color/holo_blue_light"
+    android:state_enabled="true"
+    android:state_checked="true"/>
+  <item
+    android:drawable="@android:color/holo_blue_light"
+    android:state_enabled="true"
+    android:state_selected="true"/>
+  <item
+    android:drawable="@android:color/transparent"/>
+
+</selector>
\ No newline at end of file
diff --git a/res/layout/associations_dialog.xml b/res/layout/associations_dialog.xml
new file mode 100644 (file)
index 0000000..80afce6
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ** 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:layout_margin="@dimen/default_margin">
+
+    <GridView android:id="@+id/associations_gridview"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:stretchMode="columnWidth"
+        android:scrollbars="vertical"
+        android:horizontalSpacing="@dimen/default_margin"
+        android:verticalSpacing="@dimen/default_margin"
+        android:numColumns="3" />
+
+    <CheckBox android:id="@+id/associations_remember"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/extra_margin"
+        android:textAppearance="@style/secondary_text_appearance"
+        android:text="@string/associations_dialog_remember" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/associations_item.xml b/res/layout/associations_item.xml
new file mode 100644 (file)
index 0000000..857098f
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ** 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.\r
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="@dimen/grid_width"
+  android:layout_height="wrap_content"
+  android:background="@drawable/holo_selection"
+  android:orientation="vertical" >
+
+    <ImageView android:id="@+id/associations_item_icon"
+      android:width="@dimen/grid_image_width"
+      android:height="@dimen/grid_image_height"
+      android:layout_width="@dimen/grid_width"
+      android:layout_height="@dimen/grid_height"
+      android:contentDescription="@null"
+      android:layout_centerInParent="true"
+      android:scaleType="center"
+      android:src="@null" />
+
+    <TextView android:id="@+id/associations_item_text"
+      android:layout_width="@dimen/grid_width"
+      android:layout_height="wrap_content"
+      android:layout_alignParentBottom="true"
+      android:textAppearance="@style/secondary_text_appearance"
+      android:paddingBottom="@dimen/default_margin"
+      android:gravity="center"
+      android:textStyle="bold"
+      android:singleLine="true"
+      android:ellipsize="end" />
+
+</RelativeLayout>
\ No newline at end of file
index fced2f2..c9f99b8 100644 (file)
   <!-- The navigation grid item height -->
   <dimen name="navigation_grid_item_height">48dp</dimen>
 
+  <!-- The grid width -->
+  <dimen name="grid_width">96dp</dimen>
+  <!-- The grid height -->
+  <dimen name="grid_height">96dp</dimen>
+  <!-- The grid image width -->
+  <dimen name="grid_image_width">48dp</dimen>
+  <!-- The grid image height -->
+  <dimen name="grid_image_height">48dp</dimen>
+
   <!-- The popup dimension width -->
   <dimen name="popup_width">200dp</dimen>
 
index 4f0c547..db01cea 100644 (file)
@@ -22,6 +22,9 @@
 -->
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
+  <!-- The system directory -->
+  <string name="system_dir">/system</string>
+
   <!-- The size of the buffers use by the console (in bytes). Default: 0.5 Mb -->
   <integer name="buffer_size">512</integer>
 
index 8c66fed..6d60349 100644 (file)
   <!-- An operation can't not be cancelled -->
   <string name="msgs_read_only_filesystem">The filesystem is read-only. Try to mount the
     filesystem as read-write before do a write operation.</string>
+    <!-- An  association of an action failed -->
+  <string name="msgs_action_association_failed">The association of the action failed.</string>
 
   <!-- A message advise before close the application -->
   <string name="msgs_push_again_to_exit">Push again back button to exit.</string>
   <string name="actions_ask_remove_file">Are you sure you want to delete this file
     permanently?</string>
 
-
   <!-- Enter Name Dialog * Label -->
   <string name="input_name_dialog_label">Name:</string>
   <!-- Enter Name Dialog * Message * Empty name -->
   <!-- Enter Name Dialog * Message * Name exists -->
   <string name="input_name_dialog_message_name_exists">The namealready exists.</string>
 
+  <!-- Associations Dialog * Title -->
+  <string name="associations_dialog_title">Associations</string>
+  <!-- Associations Dialog * Remember the user action -->
+  <string name="associations_dialog_remember">Remember selection</string>
+  <!-- Associations Dialog * Open with Title -->
+  <string name="associations_dialog_openwith_title">Open with</string>
+    <!-- Associations Dialog * Open action -->
+  <string name="associations_dialog_openwith_action">Open</string>
 
   <!-- Inline Autocomplete Widget * Tab message nothing to complete -->
   <string name="inline_autocomplete_tab_nothing_to_complete_msg">Nothing to complete</string>
index 3115cc9..f538cfc 100644 (file)
@@ -25,7 +25,7 @@
      The required exitcode element must have the required attributes commandId and commandPath
      with the expected command for retrieve the exit code of the executed command
 -->
-<CommandList xmlns:androcommandId="http://schemas.android.com/apk/res/android">
+<CommandList xmlns="http://schemas.android.com/apk/res/com.cyanogenmod.explorer">
   <!-- Start code (append to commands; for retrieve the exit code) -->
   <startcode commandId="startcode" commandPath="/system/xbin/echo %1$s0%2$s ; " />
   <!-- Exit code (append to commands; for retrieve the exit code) -->
index fe81f8a..a08ee00 100644 (file)
@@ -22,7 +22,7 @@
         name:   (string) The name of the bookmark
         directory:   (string) The folder where to point to
 -->
-<Bookmarks xmlns:androcommandId="http://schemas.android.com/apk/res/android">
+<Bookmarks xmlns="http://schemas.android.com/apk/res/com.cyanogenmod.explorer">
   <bookmark name="@string/bookmarks_root_folder" directory="/" />
-  <bookmark name="@string/bookmarks_system_folder" directory="/system" />
+  <bookmark name="@string/bookmarks_system_folder" directory="@string/system_dir" />
 </Bookmarks>
\ No newline at end of file
index c02bde3..0c79be5 100644 (file)
@@ -21,6 +21,7 @@ import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.util.Log;
 
@@ -29,6 +30,7 @@ import com.cyanogenmod.explorer.console.ConsoleBuilder;
 import com.cyanogenmod.explorer.console.ConsoleHolder;
 import com.cyanogenmod.explorer.preferences.ExplorerSettings;
 import com.cyanogenmod.explorer.preferences.Preferences;
+import com.cyanogenmod.explorer.util.ExceptionUtil;
 import com.cyanogenmod.explorer.util.FileHelper;
 import com.cyanogenmod.explorer.util.MimeTypeHelper;
 
@@ -177,7 +179,7 @@ public final class ExplorerApplication extends Application {
      * @return Application The application singleton reference
      * @hide
      */
-    public static Application getInstance() {
+    public static ExplorerApplication getInstance() {
         return sApp;
     }
 
@@ -190,4 +192,27 @@ public final class ExplorerApplication extends Application {
         return sBackgroundConsole.getConsole();
     }
 
+
+
+    /**
+     * Method that check if the app is signed with the platform signature
+     *
+     * @param ctx The current context
+     * @return boolean If the app is signed with the platform signature
+     */
+    public static boolean isAppPlatformSignature(Context ctx) {
+        // TODO This need to be improved, checking if the app is really with the platform signature
+        try {
+            // For now only check that the app is installed in system directory
+            PackageManager pm = ctx.getPackageManager();
+            String appDir = pm.getApplicationInfo(ctx.getPackageName(), 0).sourceDir;
+            String systemDir = ctx.getString(R.string.system_dir);
+            return appDir.startsWith(systemDir);
+
+        } catch (Exception e) {
+            ExceptionUtil.translateException(ctx, e, true, false);
+        }
+        return false;
+    }
+
 }
index d0e1fc4..d005fe8 100644 (file)
@@ -287,11 +287,23 @@ public class BookmarksActivity extends Activity implements OnItemClickListener,
                     }
 
                     if (TAG_BOOKMARK.equals(element)) {
-                        CharSequence name =
-                                getString(parser.getAttributeResourceValue(
-                                        R.styleable.Bookmark_name, 0));
-                        CharSequence directory =
-                                parser.getAttributeValue(R.styleable.Bookmark_directory);
+                        CharSequence name = null;
+                        CharSequence directory = null;
+
+                        try {
+                            name =
+                                    getString(parser.getAttributeResourceValue(
+                                            R.styleable.Bookmark_name, 0));
+                        } catch (Exception e) {/**NON BLOCK**/}
+                        try {
+                            directory =
+                                    getString(parser.getAttributeResourceValue(
+                                            R.styleable.Bookmark_directory, 0));
+                        } catch (Exception e) {/**NON BLOCK**/}
+                        if (directory == null) {
+                            directory =
+                                    parser.getAttributeValue(R.styleable.Bookmark_directory);
+                        }
                         if (name != null && directory != null) {
                             bookmarks.add(
                                     new Bookmark(
diff --git a/src/com/cyanogenmod/explorer/adapters/AssociationsAdapter.java b/src/com/cyanogenmod/explorer/adapters/AssociationsAdapter.java
new file mode 100644 (file)
index 0000000..df99f05
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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.explorer.adapters;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.cyanogenmod.explorer.R;
+
+import java.util.List;
+
+/**
+ * An implementation of {@link ArrayAdapter} for display associations.
+ */
+public class AssociationsAdapter
+    extends ArrayAdapter<ResolveInfo> implements View.OnClickListener {
+
+    /**
+     * A class that conforms with the ViewHolder pattern to performance
+     * the list view rendering.
+     */
+    private static class ViewHolder {
+        /**
+         * @hide
+         */
+        public ViewHolder() {
+            super();
+        }
+        int mPosition;
+        ImageView mIvIcon;
+        TextView mTvName;
+    }
+
+    /**
+     * A class that holds the full data information.
+     */
+    private static class DataHolder {
+        /**
+         * @hide
+         */
+        public DataHolder() {
+            super();
+        }
+        Drawable mDwIcon;
+        String mName;
+    }
+
+
+
+    private DataHolder[] mData;
+    private final OnItemClickListener mOnItemClickListener;
+
+    //The resource item layout
+    private static final int RESOURCE_LAYOUT = R.layout.associations_item;
+
+    //The resource of the item icon
+    private static final int RESOURCE_ITEM_ICON = R.id.associations_item_icon;
+    //The resource of the item name
+    private static final int RESOURCE_ITEM_NAME = R.id.associations_item_text;
+
+    /**
+     * Constructor of <code>AssociationsAdapter</code>.
+     *
+     * @param context The current context
+     * @param intents The intents info
+     * @param onItemClickListener The listener for listen action clicks
+     */
+    public AssociationsAdapter(
+            Context context, List<ResolveInfo> intents, OnItemClickListener onItemClickListener) {
+        super(context, RESOURCE_ITEM_NAME, intents);
+        this.mOnItemClickListener = onItemClickListener;
+
+        //Do cache of the data for better performance
+        processData();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void notifyDataSetChanged() {
+        processData();
+        super.notifyDataSetChanged();
+    }
+
+    /**
+     * Method that dispose the elements of the adapter.
+     */
+    public void dispose() {
+        clear();
+        this.mData = null;
+    }
+
+    /**
+     * Method that process the data before use {@link #getView} method.
+     */
+    private void processData() {
+        this.mData = new DataHolder[getCount()];
+        int cc = getCount();
+        for (int i = 0; i < cc; i++) {
+            //Intent info
+            ResolveInfo intentInfo = getItem(i);
+
+            //Build the data holder
+            this.mData[i] = new AssociationsAdapter.DataHolder();
+            this.mData[i].mDwIcon = intentInfo.loadIcon(getContext().getPackageManager());
+            this.mData[i].mName =
+                    intentInfo.loadLabel(getContext().getPackageManager()).toString();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+
+        //Check to reuse view
+        View v = convertView;
+        if (v == null) {
+            //Create the view holder
+            LayoutInflater li =
+                    (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            v = li.inflate(RESOURCE_LAYOUT, parent, false);
+            ViewHolder viewHolder = new AssociationsAdapter.ViewHolder();
+            viewHolder.mPosition = position;
+            viewHolder.mIvIcon = (ImageView)v.findViewById(RESOURCE_ITEM_ICON);
+            viewHolder.mTvName = (TextView)v.findViewById(RESOURCE_ITEM_NAME);
+            v.setTag(viewHolder);
+        }
+
+        //Retrieve data holder
+        final DataHolder dataHolder = this.mData[position];
+
+        //Retrieve the view holder
+        ViewHolder viewHolder = (ViewHolder)v.getTag();
+
+        //Set the data
+        viewHolder.mPosition = position;
+        viewHolder.mIvIcon.setImageDrawable(dataHolder.mDwIcon);
+        viewHolder.mTvName.setText(dataHolder.mName);
+        v.setOnClickListener(this);
+
+        //Return the view
+        return v;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onClick(View v) {
+        ViewHolder viewHolder = (ViewHolder)v.getTag();
+        this.mOnItemClickListener.onItemClick(null, v, viewHolder.mPosition, v.getId());
+    }
+
+}
index 76d6840..7a2f7d7 100644 (file)
@@ -83,7 +83,7 @@ public class Query implements Serializable {
      */
     public Query fillSlots(List<String> queries) {
         int cc = queries.size();
-        for (int i = 0; i < queries.size(); i++) {
+        for (int i = 0; i < cc; i++) {
             if (i > this.mQUERIES.length) {
                 break;
             }
@@ -100,7 +100,7 @@ public class Query implements Serializable {
     public List<String> getQueries() {
         List<String> queries = new ArrayList<String>(getSlotsCount());
         int cc = this.mQUERIES.length;
-        for (int i = 0; i < this.mQUERIES.length; i++) {
+        for (int i = 0; i < cc; i++) {
             if (this.mQUERIES[i] != null && this.mQUERIES[i].length() > 0) {
                 queries.add(this.mQUERIES[i]);
             }
diff --git a/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java b/src/com/cyanogenmod/explorer/ui/dialogs/AssociationsDialog.java
new file mode 100644 (file)
index 0000000..ced108f
--- /dev/null
@@ -0,0 +1,432 @@
+/*
+ * 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.explorer.ui.dialogs;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+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.explorer.ExplorerApplication;
+import com.cyanogenmod.explorer.R;
+import com.cyanogenmod.explorer.adapters.AssociationsAdapter;
+import com.cyanogenmod.explorer.util.DialogHelper;
+import com.cyanogenmod.explorer.util.ExceptionUtil;
+
+import java.lang.reflect.Method;
+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$
+
+    final Context mContext;
+    private final List<ResolveInfo> mIntents;
+    private final ResolveInfo mPreferred;
+    /**
+     * @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
+     */
+    public AssociationsDialog(
+            Context context, int icon, String title, String action,
+            Intent requestIntent, List<ResolveInfo> intents, ResolveInfo preferred) {
+        super();
+
+        //Save the data
+        this.mContext = context;
+        this.mRequestIntent = requestIntent;
+        this.mIntents = intents;
+        this.mPreferred = preferred;
+        this.mLoaded = false;
+
+        //Initialize dialog
+        init(icon, title, action);
+    }
+
+    /**
+     * 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
+     */
+    private void init(int icon, String title, String action) {
+        boolean isPlatformSigned =
+                ExplorerApplication.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 ? View.VISIBLE : View.GONE);
+        this.mGrid = (GridView)v.findViewById(R.id.associations_gridview);
+        this.mGrid.setAdapter(new AssociationsAdapter(this.mContext, this.mIntents, this));
+
+        // Ensure a default title dialog
+        String dialogTitle = title;
+        if (dialogTitle == null) {
+            dialogTitle = this.mContext.getString(R.string.associations_dialog_title);
+        }
+
+        //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 = new Intent(AssociationsDialog.this.mRequestIntent);
+                        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));
+
+                        // 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);
+    }
+
+    /**
+     * Method that shows the dialog.
+     */
+    public void show() {
+        this.mDialog.show();
+
+        // 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);
+
+        // 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());
+
+                                // Not allow to revert remember status
+                                this.mRemember.setChecked(true);
+                                this.mRemember.setEnabled(false);
+                                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({"boxing"})
+    void onIntentSelected(ResolveInfo ri, Intent intent, boolean remember) {
+        if (remember) {
+            // 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.
+                    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;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Register preferred association is only allowed by platform signature
+            // CMExplorer 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 =
+                    ExplorerApplication.isAppPlatformSignature(this.mContext);
+            if (isPlatformSigned) {
+                if (filter != null && !isPreferredSelected()) {
+                    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);
+                            if (r.match > bestMatch) bestMatch = r.match;
+                        }
+
+                        // Use reflection to access the hidden replacePreferredActivity method
+                        // FIXME This need to be tested on a CM compilation. Now 
+                        // replacePreferredActivity is not working. Check also
+                        // the use of addPreferredActivity.
+                        PackageManager pm = this.mContext.getPackageManager();
+                        Method m = pm.getClass().getMethod(
+                                            "replacePreferredActivity", //$NON-NLS-1$
+                                            IntentFilter.class, int.class,
+                                            ComponentName[].class, ComponentName.class);
+                        m.invoke(pm, filter, bestMatch, set, intent.getComponent());
+
+                    } catch (Exception e) {
+                        // Capture the exception
+                        ExceptionUtil.translateException(this.mContext, e, true, false);
+                        Toast.makeText(
+                                this.mContext,
+                                R.string.msgs_action_association_failed,
+                                Toast.LENGTH_SHORT).show();
+                    }
+                }
+            }
+        }
+
+        if (intent != null) {
+            this.mContext.startActivity(intent);
+        }
+    }
+}
index ba509e3..a46cf46 100644 (file)
@@ -35,6 +35,7 @@ import com.cyanogenmod.explorer.model.Bookmark;
 import com.cyanogenmod.explorer.model.Bookmark.BOOKMARK_TYPE;
 import com.cyanogenmod.explorer.model.FileSystemObject;
 import com.cyanogenmod.explorer.preferences.Bookmarks;
+import com.cyanogenmod.explorer.ui.dialogs.AssociationsDialog;
 import com.cyanogenmod.explorer.ui.dialogs.FsoPropertiesDialog;
 import com.cyanogenmod.explorer.util.CommandHelper;
 import com.cyanogenmod.explorer.util.DialogHelper;
@@ -134,7 +135,7 @@ public final class ActionsPolicy {
      *
      * @param ctx The current context
      * @param fso The file system object
-     * @param choose If allow the user to select the app to open with
+     * @param choose If allow the user to select the application to open with
      */
     public static void openFileSystemObject(
             final Context ctx, final FileSystemObject fso, final boolean choose) {
@@ -161,37 +162,39 @@ public final class ActionsPolicy {
                     packageManager.
                         queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
 
+            // Retrieve the preferred activity that can handle the file
+            final ResolveInfo mPreferredInfo = packageManager.resolveActivity(intent, 0);
+
             // Now we have the list of activities that can handle the file. The next steps are:
             //
             // 1.- If choose, then show open with dialog
-            // 1.- If info size == 0. No default application, then show open with dialog
-            // 2.- If !choose, seek inside our database the default activity for the extension
+            // 2.- If info size == 0. No default application, then show open with dialog
+            // 3.- If !choose, seek inside our database the default activity for the extension
             //     and open the file with this application
-            // 3.- If no default activity saved, then use system default
+            // 4.- If no default activity saved, then use system default
 
-            if (choose || info.size() == 0) {
-                // Show open with dialog
-                return;
+            // No registered application
+            if (info.size() == 0) {
+                DialogHelper.createWarningDialog(ctx, R.string.msgs_not_registered_app);
             }
 
+            // Is a simple open and we have an application that can handle the file?
+            if (!choose && (mPreferredInfo.match != 0 || info.size() == 1)) {
+                ctx.startActivity(intent);
+                return;
+            }
 
-
-            // 2.- info size == 0
-
-
-
-//            if (info.size() == 0) {
-//                DialogHelper.createWarningDialog(ctx, R.string.msgs_not_registered_app);
-//            }
-//
-//            // Open with?
-//            if (choose) {
-//                String title = ctx.getString(R.string.actions_menu_open_with);
-//                ((Activity)ctx).startActivity(Intent.createChooser(intent,title));
-//
-//            } else {
-//                ((Activity)ctx).startActivity(intent);
-//            }
+            // Otherwise, we have to show the open with dialog
+            AssociationsDialog dialog =
+                    new AssociationsDialog(
+                            ctx,
+                            R.drawable.ic_holo_light_open,
+                            ctx.getString(R.string.associations_dialog_openwith_title),
+                            ctx.getString(R.string.associations_dialog_openwith_action),
+                            intent,
+                            info,
+                            mPreferredInfo);
+            dialog.show();
 
         } catch (Exception e) {
             ExceptionUtil.translateException(ctx, e);