1/*
2 * Copyright (C) 2012 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
17package com.android.settings;
18
19import android.app.Activity;
20import android.app.ActivityManager;
21import android.app.LauncherActivity.IconResizer;
22import android.appwidget.AppWidgetHost;
23import android.appwidget.AppWidgetManager;
24import android.appwidget.AppWidgetProviderInfo;
25import android.content.ActivityNotFoundException;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageManager;
30import android.content.res.Resources;
31import android.graphics.Bitmap;
32import android.graphics.Bitmap.Config;
33import android.graphics.Canvas;
34import android.graphics.Paint;
35import android.graphics.Rect;
36import android.graphics.drawable.Drawable;
37import android.os.AsyncTask;
38import android.os.Bundle;
39import android.os.IBinder;
40import android.os.RemoteException;
41import android.os.ServiceManager;
42import android.os.UserHandle;
43import android.util.DisplayMetrics;
44import android.util.Log;
45import android.view.IWindowManager;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.view.ViewGroup;
49import android.widget.AdapterView;
50import android.widget.BaseAdapter;
51import android.widget.GridView;
52import android.widget.ImageView;
53import android.widget.TextView;
54import android.widget.Toast;
55
56import com.android.internal.widget.LockPatternUtils;
57
58import java.lang.ref.WeakReference;
59import java.util.List;
60
61/**
62 * Displays a list of {@link AppWidgetProviderInfo} widgets, along with any
63 * injected special widgets specified through
64 * {@link AppWidgetManager#EXTRA_CUSTOM_INFO} and
65 * {@link AppWidgetManager#EXTRA_CUSTOM_EXTRAS}.
66 * <p>
67 * When an installed {@link AppWidgetProviderInfo} is selected, this activity
68 * will bind it to the given {@link AppWidgetManager#EXTRA_APPWIDGET_ID},
69 * otherwise it will return the requested extras.
70 */
71public class KeyguardAppWidgetPickActivity extends Activity
72    implements GridView.OnItemClickListener,
73        AppWidgetLoader.ItemConstructor<KeyguardAppWidgetPickActivity.Item> {
74    private static final String TAG = "KeyguardAppWidgetPickActivity";
75    private static final int REQUEST_PICK_APPWIDGET = 126;
76    private static final int REQUEST_CREATE_APPWIDGET = 127;
77
78    private AppWidgetLoader<Item> mAppWidgetLoader;
79    private List<Item> mItems;
80    private GridView mGridView;
81    private AppWidgetAdapter mAppWidgetAdapter;
82    private AppWidgetManager mAppWidgetManager;
83    private int mAppWidgetId;
84    // Might make it possible to make this be false in future
85    private boolean mAddingToKeyguard = true;
86    private Intent mResultData;
87    private LockPatternUtils mLockPatternUtils;
88    private Bundle mExtraConfigureOptions;
89
90    @Override
91    protected void onCreate(Bundle savedInstanceState) {
92        setContentView(R.layout.keyguard_appwidget_picker_layout);
93        super.onCreate(savedInstanceState);
94
95        // Set default return data
96        setResultData(RESULT_CANCELED, null);
97
98        // Read the appWidgetId passed our direction, otherwise bail if not found
99        final Intent intent = getIntent();
100        if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
101            mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
102                    AppWidgetManager.INVALID_APPWIDGET_ID);
103        } else {
104            finish();
105        }
106        mExtraConfigureOptions = intent.getBundleExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
107
108        mGridView = (GridView) findViewById(R.id.widget_list);
109        DisplayMetrics dm = new DisplayMetrics();
110        getWindowManager().getDefaultDisplay().getMetrics(dm);
111        int maxGridWidth = getResources().getDimensionPixelSize(
112                R.dimen.keyguard_appwidget_picker_max_width);
113
114        if (maxGridWidth < dm.widthPixels) {
115            mGridView.getLayoutParams().width = maxGridWidth;
116        }
117        mAppWidgetManager = AppWidgetManager.getInstance(this);
118        mAppWidgetLoader = new AppWidgetLoader<Item>(this, mAppWidgetManager, this);
119        mItems = mAppWidgetLoader.getItems(getIntent());
120        mAppWidgetAdapter = new AppWidgetAdapter(this, mItems);
121        mGridView.setAdapter(mAppWidgetAdapter);
122        mGridView.setOnItemClickListener(this);
123
124        mLockPatternUtils = new LockPatternUtils(this); // TEMP-- we want to delete this
125    }
126
127    /**
128     * Convenience method for setting the result code and intent. This method
129     * correctly injects the {@link AppWidgetManager#EXTRA_APPWIDGET_ID} that
130     * most hosts expect returned.
131     */
132    void setResultData(int code, Intent intent) {
133        Intent result = intent != null ? intent : new Intent();
134        result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
135        mResultData = result;
136        setResult(code, result);
137    }
138
139    /**
140     * Item that appears in the AppWidget picker grid.
141     */
142    public static class Item implements AppWidgetLoader.LabelledItem {
143        protected static IconResizer sResizer;
144
145
146        CharSequence label;
147        int appWidgetPreviewId;
148        int iconId;
149        String packageName;
150        String className;
151        Bundle extras;
152        private WidgetPreviewLoader mWidgetPreviewLoader;
153        private Context mContext;
154
155        /**
156         * Create a list item from given label and icon.
157         */
158        Item(Context context, CharSequence label) {
159            this.label = label;
160            mContext = context;
161        }
162
163        void loadWidgetPreview(ImageView v) {
164            mWidgetPreviewLoader = new WidgetPreviewLoader(mContext, v);
165            mWidgetPreviewLoader.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
166        }
167
168        void cancelLoadingWidgetPreview() {
169            if (mWidgetPreviewLoader != null) {
170                mWidgetPreviewLoader.cancel(false);
171                mWidgetPreviewLoader = null;
172            }
173        }
174
175        /**
176         * Build the {@link Intent} described by this item. If this item
177         * can't create a valid {@link android.content.ComponentName}, it will return
178         * {@link Intent#ACTION_CREATE_SHORTCUT} filled with the item label.
179         */
180        Intent getIntent() {
181            Intent intent = new Intent();
182            if (packageName != null && className != null) {
183                // Valid package and class, so fill details as normal intent
184                intent.setClassName(packageName, className);
185                if (extras != null) {
186                    intent.putExtras(extras);
187                }
188            } else {
189                // No valid package or class, so treat as shortcut with label
190                intent.setAction(Intent.ACTION_CREATE_SHORTCUT);
191                intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
192            }
193            return intent;
194        }
195
196        public CharSequence getLabel() {
197            return label;
198        }
199
200        class WidgetPreviewLoader extends AsyncTask<Void, Bitmap, Void> {
201            private Resources mResources;
202            private PackageManager mPackageManager;
203            private int mIconDpi;
204            private ImageView mView;
205            public WidgetPreviewLoader(Context context, ImageView v) {
206                super();
207                mResources = context.getResources();
208                mPackageManager = context.getPackageManager();
209                ActivityManager activityManager =
210                        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
211                mIconDpi = activityManager.getLauncherLargeIconDensity();
212                mView = v;
213            }
214            public Void doInBackground(Void... params) {
215                if (!isCancelled()) {
216                    int appWidgetPreviewWidth =
217                            mResources.getDimensionPixelSize(R.dimen.appwidget_preview_width);
218                    int appWidgetPreviewHeight =
219                            mResources.getDimensionPixelSize(R.dimen.appwidget_preview_height);
220                    Bitmap b = getWidgetPreview(new ComponentName(packageName, className),
221                            appWidgetPreviewId, iconId,
222                            appWidgetPreviewWidth, appWidgetPreviewHeight);
223                    publishProgress(b);
224                }
225                return null;
226            }
227            public void onProgressUpdate(Bitmap... values) {
228                if (!isCancelled()) {
229                    Bitmap b = values[0];
230                    mView.setImageBitmap(b);
231                }
232            }
233            abstract class WeakReferenceThreadLocal<T> {
234                private ThreadLocal<WeakReference<T>> mThreadLocal;
235                public WeakReferenceThreadLocal() {
236                    mThreadLocal = new ThreadLocal<WeakReference<T>>();
237                }
238
239                abstract T initialValue();
240
241                public void set(T t) {
242                    mThreadLocal.set(new WeakReference<T>(t));
243                }
244
245                public T get() {
246                    WeakReference<T> reference = mThreadLocal.get();
247                    T obj;
248                    if (reference == null) {
249                        obj = initialValue();
250                        mThreadLocal.set(new WeakReference<T>(obj));
251                        return obj;
252                    } else {
253                        obj = reference.get();
254                        if (obj == null) {
255                            obj = initialValue();
256                            mThreadLocal.set(new WeakReference<T>(obj));
257                        }
258                        return obj;
259                    }
260                }
261            }
262
263            class CanvasCache extends WeakReferenceThreadLocal<Canvas> {
264                @Override
265                protected Canvas initialValue() {
266                    return new Canvas();
267                }
268            }
269
270            class PaintCache extends WeakReferenceThreadLocal<Paint> {
271                @Override
272                protected Paint initialValue() {
273                    return null;
274                }
275            }
276
277            class BitmapCache extends WeakReferenceThreadLocal<Bitmap> {
278                @Override
279                protected Bitmap initialValue() {
280                    return null;
281                }
282            }
283
284            class RectCache extends WeakReferenceThreadLocal<Rect> {
285                @Override
286                protected Rect initialValue() {
287                    return new Rect();
288                }
289            }
290
291            // Used for drawing widget previews
292            CanvasCache sCachedAppWidgetPreviewCanvas = new CanvasCache();
293            RectCache sCachedAppWidgetPreviewSrcRect = new RectCache();
294            RectCache sCachedAppWidgetPreviewDestRect = new RectCache();
295            PaintCache sCachedAppWidgetPreviewPaint = new PaintCache();
296
297            private Bitmap getWidgetPreview(ComponentName provider, int previewImage,
298                    int iconId, int maxWidth, int maxHeight) {
299                // Load the preview image if possible
300                String packageName = provider.getPackageName();
301                if (maxWidth < 0) maxWidth = Integer.MAX_VALUE;
302                if (maxHeight < 0) maxHeight = Integer.MAX_VALUE;
303
304
305                int appIconSize = mResources.getDimensionPixelSize(R.dimen.app_icon_size);
306
307                Drawable drawable = null;
308                if (previewImage != 0) {
309                    drawable = mPackageManager.getDrawable(packageName, previewImage, null);
310                    if (drawable == null) {
311                        Log.w(TAG, "Can't load widget preview drawable 0x" +
312                                Integer.toHexString(previewImage) + " for provider: " + provider);
313                    }
314                }
315
316                int bitmapWidth;
317                int bitmapHeight;
318                Bitmap defaultPreview = null;
319                boolean widgetPreviewExists = (drawable != null);
320                if (widgetPreviewExists) {
321                    bitmapWidth = drawable.getIntrinsicWidth();
322                    bitmapHeight = drawable.getIntrinsicHeight();
323                } else {
324                    // Generate a preview image if we couldn't load one
325                    bitmapWidth = appIconSize;
326                    bitmapHeight = appIconSize;
327                    defaultPreview = Bitmap.createBitmap(bitmapWidth, bitmapHeight,
328                            Config.ARGB_8888);
329
330                    try {
331                        Drawable icon = null;
332                        if (iconId > 0)
333                            icon = getFullResIcon(packageName, iconId);
334                        if (icon != null) {
335                            renderDrawableToBitmap(icon, defaultPreview, 0,
336                                    0, appIconSize, appIconSize);
337                        }
338                    } catch (Resources.NotFoundException e) {
339                    }
340                }
341
342                // Scale to fit width only - let the widget preview be clipped in the
343                // vertical dimension
344                float scale = 1f;
345                if (bitmapWidth > maxWidth) {
346                    scale = maxWidth / (float) bitmapWidth;
347                }
348                int finalPreviewWidth = (int) (scale * bitmapWidth);
349                int finalPreviewHeight = (int) (scale * bitmapHeight);
350
351                bitmapWidth = finalPreviewWidth;
352                bitmapHeight = Math.min(finalPreviewHeight, maxHeight);
353
354                Bitmap preview = Bitmap.createBitmap(bitmapWidth, bitmapHeight,
355                        Config.ARGB_8888);
356
357                // Draw the scaled preview into the final bitmap
358                if (widgetPreviewExists) {
359                    renderDrawableToBitmap(drawable, preview, 0, 0, finalPreviewWidth,
360                            finalPreviewHeight);
361                } else {
362                    final Canvas c = sCachedAppWidgetPreviewCanvas.get();
363                    final Rect src = sCachedAppWidgetPreviewSrcRect.get();
364                    final Rect dest = sCachedAppWidgetPreviewDestRect.get();
365                    c.setBitmap(preview);
366                    src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight());
367                    dest.set(0, 0, finalPreviewWidth, finalPreviewHeight);
368
369                    Paint p = sCachedAppWidgetPreviewPaint.get();
370                    if (p == null) {
371                        p = new Paint();
372                        p.setFilterBitmap(true);
373                        sCachedAppWidgetPreviewPaint.set(p);
374                    }
375                    c.drawBitmap(defaultPreview, src, dest, p);
376                    c.setBitmap(null);
377                }
378                return preview;
379            }
380            public Drawable getFullResDefaultActivityIcon() {
381                return getFullResIcon(Resources.getSystem(),
382                        android.R.mipmap.sym_def_app_icon);
383            }
384
385            public Drawable getFullResIcon(Resources resources, int iconId) {
386                Drawable d;
387                try {
388                    d = resources.getDrawableForDensity(iconId, mIconDpi);
389                } catch (Resources.NotFoundException e) {
390                    d = null;
391                }
392
393                return (d != null) ? d : getFullResDefaultActivityIcon();
394            }
395
396            public Drawable getFullResIcon(String packageName, int iconId) {
397                Resources resources;
398                try {
399                    resources = mPackageManager.getResourcesForApplication(packageName);
400                } catch (PackageManager.NameNotFoundException e) {
401                    resources = null;
402                }
403                if (resources != null) {
404                    if (iconId != 0) {
405                        return getFullResIcon(resources, iconId);
406                    }
407                }
408                return getFullResDefaultActivityIcon();
409            }
410
411            private void renderDrawableToBitmap(Drawable d, Bitmap bitmap, int x, int y, int w, int h) {
412                renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f);
413            }
414
415            private void renderDrawableToBitmap(Drawable d, Bitmap bitmap, int x, int y, int w, int h,
416                    float scale) {
417                if (bitmap != null) {
418                    Canvas c = new Canvas(bitmap);
419                    c.scale(scale, scale);
420                    Rect oldBounds = d.copyBounds();
421                    d.setBounds(x, y, x + w, y + h);
422                    d.draw(c);
423                    d.setBounds(oldBounds); // Restore the bounds
424                    c.setBitmap(null);
425                }
426            }
427        }
428    }
429
430    @Override
431    public Item createItem(Context context, AppWidgetProviderInfo info, Bundle extras) {
432        CharSequence label = info.label;
433
434        Item item = new Item(context, label);
435        item.appWidgetPreviewId = info.previewImage;
436        item.iconId = info.icon;
437        item.packageName = info.provider.getPackageName();
438        item.className = info.provider.getClassName();
439        item.extras = extras;
440        return item;
441    }
442
443    protected static class AppWidgetAdapter extends BaseAdapter {
444        private final LayoutInflater mInflater;
445        private final List<Item> mItems;
446
447        /**
448         * Create an adapter for the given items.
449         */
450        public AppWidgetAdapter(Context context, List<Item> items) {
451            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
452            mItems = items;
453        }
454
455        /**
456         * {@inheritDoc}
457         */
458        public int getCount() {
459            return mItems.size();
460        }
461
462        /**
463         * {@inheritDoc}
464         */
465        public Object getItem(int position) {
466            return mItems.get(position);
467        }
468
469        /**
470         * {@inheritDoc}
471         */
472        public long getItemId(int position) {
473            return position;
474        }
475
476        /**
477         * {@inheritDoc}
478         */
479        public View getView(int position, View convertView, ViewGroup parent) {
480            if (convertView == null) {
481                convertView = mInflater.inflate(R.layout.keyguard_appwidget_item, parent, false);
482            }
483
484            Item item = (Item) getItem(position);
485            TextView textView = (TextView) convertView.findViewById(R.id.label);
486            textView.setText(item.label);
487            ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
488            iconView.setImageDrawable(null);
489            item.loadWidgetPreview(iconView);
490            return convertView;
491        }
492
493        public void cancelAllWidgetPreviewLoaders() {
494            for (int i = 0; i < mItems.size(); i++) {
495                mItems.get(i).cancelLoadingWidgetPreview();
496            }
497        }
498    }
499
500    /**
501     * {@inheritDoc}
502     */
503    @Override
504    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
505        Item item = mItems.get(position);
506        Intent intent = item.getIntent();
507
508        int result;
509        if (item.extras != null) {
510            // If these extras are present it's because this entry is custom.
511            // Don't try to bind it, just pass it back to the app.
512            result = RESULT_OK;
513            setResultData(result, intent);
514        } else {
515            try {
516                if (mAddingToKeyguard && mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
517                    // Found in KeyguardHostView.java
518                    final int KEYGUARD_HOST_ID = 0x4B455947;
519                    int userId = ActivityManager.getCurrentUser();
520                    mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForSystem(KEYGUARD_HOST_ID,
521                            userId);
522                }
523                mAppWidgetManager.bindAppWidgetId(
524                        mAppWidgetId, intent.getComponent(), mExtraConfigureOptions);
525                result = RESULT_OK;
526            } catch (IllegalArgumentException e) {
527                // This is thrown if they're already bound, or otherwise somehow
528                // bogus.  Set the result to canceled, and exit.  The app *should*
529                // clean up at this point.  We could pass the error along, but
530                // it's not clear that that's useful -- the widget will simply not
531                // appear.
532                result = RESULT_CANCELED;
533            }
534            setResultData(result, null);
535        }
536        if (mAddingToKeyguard) {
537            onActivityResult(REQUEST_PICK_APPWIDGET, result, mResultData);
538        } else {
539            finish();
540        }
541    }
542
543    protected void onDestroy() {
544        if (mAppWidgetAdapter != null) {
545            mAppWidgetAdapter.cancelAllWidgetPreviewLoaders();
546        }
547        super.onDestroy();
548    }
549
550    @Override
551    public void onActivityResult(int requestCode, int resultCode, Intent data) {
552        super.onActivityResult(requestCode, resultCode, data);
553        if (requestCode == REQUEST_PICK_APPWIDGET || requestCode == REQUEST_CREATE_APPWIDGET) {
554            int appWidgetId;
555            if  (data == null) {
556                appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID ;
557            } else {
558                appWidgetId = data.getIntExtra(
559                        AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
560            }
561            if (requestCode == REQUEST_PICK_APPWIDGET && resultCode == Activity.RESULT_OK) {
562                AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
563
564                AppWidgetProviderInfo appWidget = null;
565                appWidget = appWidgetManager.getAppWidgetInfo(appWidgetId);
566
567                if (appWidget.configure != null) {
568                    // Launch over to configure widget, if needed
569                    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
570                    intent.setComponent(appWidget.configure);
571                    intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
572                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
573
574                    startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET);
575                } else {
576                    // Otherwise just add it
577                    onActivityResult(REQUEST_CREATE_APPWIDGET, Activity.RESULT_OK, data);
578                }
579            } else if (requestCode == REQUEST_CREATE_APPWIDGET && resultCode == Activity.RESULT_OK) {
580                mLockPatternUtils.addAppWidget(appWidgetId, 0);
581                finishDelayedAndShowLockScreen(appWidgetId);
582            } else {
583                if (mAddingToKeyguard &&
584                        mAppWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
585                    int userId = ActivityManager.getCurrentUser();
586                    AppWidgetHost.deleteAppWidgetIdForSystem(mAppWidgetId, userId);
587                }
588                finishDelayedAndShowLockScreen(AppWidgetManager.INVALID_APPWIDGET_ID);
589            }
590        }
591    }
592
593    private void finishDelayedAndShowLockScreen(int appWidgetId) {
594        IBinder b = ServiceManager.getService(Context.WINDOW_SERVICE);
595        IWindowManager iWm = IWindowManager.Stub.asInterface(b);
596        Bundle opts = null;
597        if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
598            opts = new Bundle();
599            opts.putInt(LockPatternUtils.KEYGUARD_SHOW_APPWIDGET, appWidgetId);
600        }
601        try {
602            iWm.lockNow(opts);
603        } catch (RemoteException e) {
604        }
605
606        // Change background to all black
607        ViewGroup root = (ViewGroup) findViewById(R.id.layout_root);
608        root.setBackgroundColor(0xFF000000);
609        // Hide all children
610        final int childCount = root.getChildCount();
611        for (int i = 0; i < childCount; i++) {
612            root.getChildAt(i).setVisibility(View.INVISIBLE);
613        }
614        mGridView.postDelayed(new Runnable() {
615            public void run() {
616                finish();
617            }
618        }, 500);
619    }
620
621    void startActivityForResultSafely(Intent intent, int requestCode) {
622        try {
623            startActivityForResult(intent, requestCode);
624        } catch (ActivityNotFoundException e) {
625            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
626        } catch (SecurityException e) {
627            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
628            Log.e(TAG, "Settings does not have the permission to launch " + intent, e);
629        }
630    }
631}
632