1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.settings;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.content.Intent;
23 import android.content.Intent.ShortcutIconResource;
24 import android.content.pm.PackageManager;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.pm.ResolveInfo;
27 import android.content.res.Resources;
28 import android.graphics.Bitmap;
29 import android.graphics.Canvas;
30 import android.graphics.ColorFilter;
31 import android.graphics.Paint;
32 import android.graphics.PaintFlagsDrawFilter;
33 import android.graphics.PixelFormat;
34 import android.graphics.Rect;
35 import android.graphics.drawable.BitmapDrawable;
36 import android.graphics.drawable.Drawable;
37 import android.graphics.drawable.PaintDrawable;
38 import android.os.Bundle;
39 import android.os.Parcelable;
40 import android.util.DisplayMetrics;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.BaseAdapter;
45 import android.widget.TextView;
46 
47 import com.android.internal.app.AlertActivity;
48 import com.android.internal.app.AlertController;
49 
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.List;
53 
54 /**
55  * Displays a list of all activities matching the incoming
56  * {@link Intent#EXTRA_INTENT} query, along with any injected items.
57  */
58 public class ActivityPicker extends AlertActivity implements
59         DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
60 
61     /**
62      * Adapter of items that are displayed in this dialog.
63      */
64     private PickAdapter mAdapter;
65 
66     /**
67      * Base {@link Intent} used when building list.
68      */
69     private Intent mBaseIntent;
70 
71     @Override
onCreate(Bundle savedInstanceState)72     protected void onCreate(Bundle savedInstanceState) {
73         super.onCreate(savedInstanceState);
74 
75         final Intent intent = getIntent();
76 
77         // Read base intent from extras, otherwise assume default
78         Parcelable parcel = intent.getParcelableExtra(Intent.EXTRA_INTENT);
79         if (parcel instanceof Intent) {
80             mBaseIntent = (Intent) parcel;
81             mBaseIntent.setFlags(mBaseIntent.getFlags() & ~(Intent.FLAG_GRANT_READ_URI_PERMISSION
82                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
83                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
84                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION));
85         } else {
86             mBaseIntent = new Intent(Intent.ACTION_MAIN, null);
87             mBaseIntent.addCategory(Intent.CATEGORY_DEFAULT);
88         }
89 
90         // Create dialog parameters
91         AlertController.AlertParams params = mAlertParams;
92         params.mOnClickListener = this;
93         params.mOnCancelListener = this;
94 
95         // Use custom title if provided, otherwise default window title
96         if (intent.hasExtra(Intent.EXTRA_TITLE)) {
97             params.mTitle = intent.getStringExtra(Intent.EXTRA_TITLE);
98         } else {
99             params.mTitle = getTitle();
100         }
101 
102         // Build list adapter of pickable items
103         List<PickAdapter.Item> items = getItems();
104         mAdapter = new PickAdapter(this, items);
105         params.mAdapter = mAdapter;
106 
107         setupAlert();
108     }
109 
110     /**
111      * Handle clicking of dialog item by passing back
112      * {@link #getIntentForPosition(int)} in {@link #setResult(int, Intent)}.
113      */
onClick(DialogInterface dialog, int which)114     public void onClick(DialogInterface dialog, int which) {
115         Intent intent = getIntentForPosition(which);
116         setResult(Activity.RESULT_OK, intent);
117         finish();
118     }
119 
120     /**
121      * Handle canceled dialog by passing back {@link Activity#RESULT_CANCELED}.
122      */
onCancel(DialogInterface dialog)123     public void onCancel(DialogInterface dialog) {
124         setResult(Activity.RESULT_CANCELED);
125         finish();
126     }
127 
128     /**
129      * Build the specific {@link Intent} for a given list position. Convenience
130      * method that calls through to {@link PickAdapter.Item#getIntent(Intent)}.
131      */
getIntentForPosition(int position)132     protected Intent getIntentForPosition(int position) {
133         PickAdapter.Item item = (PickAdapter.Item) mAdapter.getItem(position);
134         return item.getIntent(mBaseIntent);
135     }
136 
137     /**
138      * Build and return list of items to be shown in dialog. Default
139      * implementation mixes activities matching {@link #mBaseIntent} from
140      * {@link #putIntentItems(Intent, List)} with any injected items from
141      * {@link Intent#EXTRA_SHORTCUT_NAME}. Override this method in subclasses to
142      * change the items shown.
143      */
getItems()144     protected List<PickAdapter.Item> getItems() {
145         PackageManager packageManager = getPackageManager();
146         List<PickAdapter.Item> items = new ArrayList<PickAdapter.Item>();
147 
148         // Add any injected pick items
149         final Intent intent = getIntent();
150         ArrayList<String> labels =
151             intent.getStringArrayListExtra(Intent.EXTRA_SHORTCUT_NAME);
152         ArrayList<ShortcutIconResource> icons =
153             intent.getParcelableArrayListExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
154 
155         if (labels != null && icons != null && labels.size() == icons.size()) {
156             for (int i = 0; i < labels.size(); i++) {
157                 String label = labels.get(i);
158                 Drawable icon = null;
159 
160                 try {
161                     // Try loading icon from requested package
162                     ShortcutIconResource iconResource = icons.get(i);
163                     Resources res = packageManager.getResourcesForApplication(
164                             iconResource.packageName);
165                     icon = res.getDrawable(res.getIdentifier(
166                             iconResource.resourceName, null, null), null);
167                 } catch (NameNotFoundException e) {
168                     // Ignore
169                 }
170 
171                 items.add(new PickAdapter.Item(this, label, icon));
172             }
173         }
174 
175         // Add any intent items if base was given
176         if (mBaseIntent != null) {
177             putIntentItems(mBaseIntent, items);
178         }
179 
180         return items;
181     }
182 
183     /**
184      * Fill the given list with any activities matching the base {@link Intent}.
185      */
putIntentItems(Intent baseIntent, List<PickAdapter.Item> items)186     protected void putIntentItems(Intent baseIntent, List<PickAdapter.Item> items) {
187         PackageManager packageManager = getPackageManager();
188         List<ResolveInfo> list = packageManager.queryIntentActivities(baseIntent,
189                 0 /* no flags */);
190         Collections.sort(list, new ResolveInfo.DisplayNameComparator(packageManager));
191 
192         final int listSize = list.size();
193         for (int i = 0; i < listSize; i++) {
194             ResolveInfo resolveInfo = list.get(i);
195             items.add(new PickAdapter.Item(this, packageManager, resolveInfo));
196         }
197     }
198 
199     /**
200      * Adapter which shows the set of activities that can be performed for a
201      * given {@link Intent}.
202      */
203     protected static class PickAdapter extends BaseAdapter {
204 
205         /**
206          * Item that appears in a {@link PickAdapter} list.
207          */
208         public static class Item implements AppWidgetLoader.LabelledItem {
209             protected static IconResizer sResizer;
210 
getResizer(Context context)211             protected IconResizer getResizer(Context context) {
212                 if (sResizer == null) {
213                     final Resources resources = context.getResources();
214                     int size = (int) resources.getDimension(android.R.dimen.app_icon_size);
215                     sResizer = new IconResizer(size, size, resources.getDisplayMetrics());
216                 }
217                 return sResizer;
218             }
219 
220             CharSequence label;
221             Drawable icon;
222             String packageName;
223             String className;
224             Bundle extras;
225 
226             /**
227              * Create a list item from given label and icon.
228              */
Item(Context context, CharSequence label, Drawable icon)229             Item(Context context, CharSequence label, Drawable icon) {
230                 this.label = label;
231                 this.icon = getResizer(context).createIconThumbnail(icon);
232             }
233 
234             /**
235              * Create a list item and fill it with details from the given
236              * {@link ResolveInfo} object.
237              */
Item(Context context, PackageManager pm, ResolveInfo resolveInfo)238             Item(Context context, PackageManager pm, ResolveInfo resolveInfo) {
239                 label = resolveInfo.loadLabel(pm);
240                 if (label == null && resolveInfo.activityInfo != null) {
241                     label = resolveInfo.activityInfo.name;
242                 }
243 
244                 icon = getResizer(context).createIconThumbnail(resolveInfo.loadIcon(pm));
245                 packageName = resolveInfo.activityInfo.applicationInfo.packageName;
246                 className = resolveInfo.activityInfo.name;
247             }
248 
249             /**
250              * Build the {@link Intent} described by this item. If this item
251              * can't create a valid {@link android.content.ComponentName}, it will return
252              * {@link Intent#ACTION_CREATE_SHORTCUT} filled with the item label.
253              */
getIntent(Intent baseIntent)254             Intent getIntent(Intent baseIntent) {
255                 Intent intent = new Intent(baseIntent);
256                 if (packageName != null && className != null) {
257                     // Valid package and class, so fill details as normal intent
258                     intent.setClassName(packageName, className);
259                     if (extras != null) {
260                         intent.putExtras(extras);
261                     }
262                 } else {
263                     // No valid package or class, so treat as shortcut with label
264                     intent.setAction(Intent.ACTION_CREATE_SHORTCUT);
265                     intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
266                 }
267                 return intent;
268             }
269 
getLabel()270             public CharSequence getLabel() {
271                 return label;
272             }
273         }
274 
275         private final LayoutInflater mInflater;
276         private final List<Item> mItems;
277 
278         /**
279          * Create an adapter for the given items.
280          */
PickAdapter(Context context, List<Item> items)281         public PickAdapter(Context context, List<Item> items) {
282             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
283             mItems = items;
284         }
285 
286         /**
287          * {@inheritDoc}
288          */
getCount()289         public int getCount() {
290             return mItems.size();
291         }
292 
293         /**
294          * {@inheritDoc}
295          */
getItem(int position)296         public Object getItem(int position) {
297             return mItems.get(position);
298         }
299 
300         /**
301          * {@inheritDoc}
302          */
getItemId(int position)303         public long getItemId(int position) {
304             return position;
305         }
306 
307         /**
308          * {@inheritDoc}
309          */
getView(int position, View convertView, ViewGroup parent)310         public View getView(int position, View convertView, ViewGroup parent) {
311             if (convertView == null) {
312                 convertView = mInflater.inflate(R.layout.pick_item, parent, false);
313             }
314 
315             Item item = (Item) getItem(position);
316             TextView textView = (TextView) convertView;
317             textView.setText(item.label);
318             textView.setCompoundDrawablesWithIntrinsicBounds(item.icon, null, null, null);
319 
320             return convertView;
321         }
322     }
323 
324     /**
325      * Utility class to resize icons to match default icon size. Code is mostly
326      * borrowed from Launcher.
327      */
328     private static class IconResizer {
329         private final int mIconWidth;
330         private final int mIconHeight;
331 
332         private final DisplayMetrics mMetrics;
333         private final Rect mOldBounds = new Rect();
334         private final Canvas mCanvas = new Canvas();
335 
IconResizer(int width, int height, DisplayMetrics metrics)336         public IconResizer(int width, int height, DisplayMetrics metrics) {
337             mCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
338                     Paint.FILTER_BITMAP_FLAG));
339 
340             mMetrics = metrics;
341             mIconWidth = width;
342             mIconHeight = height;
343         }
344 
345         /**
346          * Returns a Drawable representing the thumbnail of the specified Drawable.
347          * The size of the thumbnail is defined by the dimension
348          * android.R.dimen.launcher_application_icon_size.
349          *
350          * This method is not thread-safe and should be invoked on the UI thread only.
351          *
352          * @param icon The icon to get a thumbnail of.
353          *
354          * @return A thumbnail for the specified icon or the icon itself if the
355          *         thumbnail could not be created.
356          */
createIconThumbnail(Drawable icon)357         public Drawable createIconThumbnail(Drawable icon) {
358             int width = mIconWidth;
359             int height = mIconHeight;
360 
361             if (icon == null) {
362                 return new EmptyDrawable(width, height);
363             }
364 
365             try {
366                 if (icon instanceof PaintDrawable) {
367                     PaintDrawable painter = (PaintDrawable) icon;
368                     painter.setIntrinsicWidth(width);
369                     painter.setIntrinsicHeight(height);
370                 } else if (icon instanceof BitmapDrawable) {
371                     // Ensure the bitmap has a density.
372                     BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
373                     Bitmap bitmap = bitmapDrawable.getBitmap();
374                     if (bitmap.getDensity() == Bitmap.DENSITY_NONE) {
375                         bitmapDrawable.setTargetDensity(mMetrics);
376                     }
377                 }
378                 int iconWidth = icon.getIntrinsicWidth();
379                 int iconHeight = icon.getIntrinsicHeight();
380 
381                 if (iconWidth > 0 && iconHeight > 0) {
382                     if (width < iconWidth || height < iconHeight) {
383                         final float ratio = (float) iconWidth / iconHeight;
384 
385                         if (iconWidth > iconHeight) {
386                             height = (int) (width / ratio);
387                         } else if (iconHeight > iconWidth) {
388                             width = (int) (height * ratio);
389                         }
390 
391                         final Bitmap.Config c = icon.getOpacity() != PixelFormat.OPAQUE ?
392                                     Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
393                         final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
394                         final Canvas canvas = mCanvas;
395                         canvas.setBitmap(thumb);
396                         // Copy the old bounds to restore them later
397                         // If we were to do oldBounds = icon.getBounds(),
398                         // the call to setBounds() that follows would
399                         // change the same instance and we would lose the
400                         // old bounds
401                         mOldBounds.set(icon.getBounds());
402                         final int x = (mIconWidth - width) / 2;
403                         final int y = (mIconHeight - height) / 2;
404                         icon.setBounds(x, y, x + width, y + height);
405                         icon.draw(canvas);
406                         icon.setBounds(mOldBounds);
407                         //noinspection deprecation
408                         icon = new BitmapDrawable(thumb);
409                         ((BitmapDrawable) icon).setTargetDensity(mMetrics);
410                         canvas.setBitmap(null);
411                     } else if (iconWidth < width && iconHeight < height) {
412                         final Bitmap.Config c = Bitmap.Config.ARGB_8888;
413                         final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
414                         final Canvas canvas = mCanvas;
415                         canvas.setBitmap(thumb);
416                         mOldBounds.set(icon.getBounds());
417                         final int x = (width - iconWidth) / 2;
418                         final int y = (height - iconHeight) / 2;
419                         icon.setBounds(x, y, x + iconWidth, y + iconHeight);
420                         icon.draw(canvas);
421                         icon.setBounds(mOldBounds);
422                         //noinspection deprecation
423                         icon = new BitmapDrawable(thumb);
424                         ((BitmapDrawable) icon).setTargetDensity(mMetrics);
425                         canvas.setBitmap(null);
426                     }
427                 }
428 
429             } catch (Throwable t) {
430                 icon = new EmptyDrawable(width, height);
431             }
432 
433             return icon;
434         }
435     }
436 
437     private static class EmptyDrawable extends Drawable {
438         private final int mWidth;
439         private final int mHeight;
440 
EmptyDrawable(int width, int height)441         EmptyDrawable(int width, int height) {
442             mWidth = width;
443             mHeight = height;
444         }
445 
446         @Override
getIntrinsicWidth()447         public int getIntrinsicWidth() {
448             return mWidth;
449         }
450 
451         @Override
getIntrinsicHeight()452         public int getIntrinsicHeight() {
453             return mHeight;
454         }
455 
456         @Override
getMinimumWidth()457         public int getMinimumWidth() {
458             return mWidth;
459         }
460 
461         @Override
getMinimumHeight()462         public int getMinimumHeight() {
463             return mHeight;
464         }
465 
466         @Override
draw(Canvas canvas)467         public void draw(Canvas canvas) {
468         }
469 
470         @Override
setAlpha(int alpha)471         public void setAlpha(int alpha) {
472         }
473 
474         @Override
setColorFilter(ColorFilter cf)475         public void setColorFilter(ColorFilter cf) {
476         }
477 
478         @Override
getOpacity()479         public int getOpacity() {
480             return PixelFormat.TRANSLUCENT;
481         }
482     }
483 }
484