1/*
2 * Copyright (C) 2014 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.tv.settings.widget;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.content.Context;
22import android.content.Intent.ShortcutIconResource;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.res.Resources;
25import android.content.res.Resources.NotFoundException;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.graphics.Canvas;
29import android.graphics.drawable.Drawable;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.util.Log;
33import android.util.TypedValue;
34import android.widget.ImageView;
35
36import com.android.tv.settings.util.AccountImageHelper;
37import com.android.tv.settings.util.ByteArrayPool;
38import com.android.tv.settings.util.CachedInputStream;
39import com.android.tv.settings.util.UriUtils;
40
41import java.io.BufferedInputStream;
42import java.io.IOException;
43import java.io.InputStream;
44import java.lang.ref.WeakReference;
45import java.net.URL;
46import java.net.URLConnection;
47
48/**
49 * AsyncTask which loads a bitmap.
50 * <p>
51 * The source of this can be another package (via a resource), a URI (content provider), or
52 * a file path.
53 *
54 * @see BitmapWorkerOptions
55 */
56public class BitmapWorkerTask extends AsyncTask<BitmapWorkerOptions, Void, Bitmap> {
57
58    private static final String TAG = "BitmapWorker";
59    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
60
61    private static final boolean DEBUG = false;
62
63    private static final int SOCKET_TIMEOUT = 10000;
64    private static final int READ_TIMEOUT = 10000;
65
66    private WeakReference<ImageView> mImageView;
67    // a flag for if the bitmap is scaled from original source
68    protected boolean mScaled;
69
70    public BitmapWorkerTask(ImageView imageView) {
71        mImageView = new WeakReference<ImageView>(imageView);
72        mScaled = false;
73    }
74
75    @Override
76    protected Bitmap doInBackground(BitmapWorkerOptions... params) {
77
78        return retrieveBitmap(params[0]);
79    }
80
81    protected Bitmap retrieveBitmap(BitmapWorkerOptions workerOptions) {
82        try {
83            if (workerOptions.getIconResource() != null) {
84                return getBitmapFromResource(workerOptions.getContext(),
85                        workerOptions.getIconResource(),
86                        workerOptions);
87            } else if (workerOptions.getResourceUri() != null) {
88                if (UriUtils.isAndroidResourceUri(workerOptions.getResourceUri())
89                        || UriUtils.isShortcutIconResourceUri(workerOptions.getResourceUri())) {
90                    // Make an icon resource from this.
91                    return getBitmapFromResource(workerOptions.getContext(),
92                            UriUtils.getIconResource(workerOptions.getResourceUri()),
93                            workerOptions);
94                } else if (UriUtils.isWebUri(workerOptions.getResourceUri())) {
95                        return getBitmapFromHttp(workerOptions);
96                } else if (UriUtils.isContentUri(workerOptions.getResourceUri())) {
97                    return getBitmapFromContent(workerOptions);
98                } else if (UriUtils.isAccountImageUri(workerOptions.getResourceUri())) {
99                    return getAccountImage(workerOptions);
100                } else {
101                    Log.e(TAG, "Error loading bitmap - unknown resource URI! "
102                            + workerOptions.getResourceUri());
103                }
104            } else {
105                Log.e(TAG, "Error loading bitmap - no source!");
106            }
107        } catch (IOException e) {
108            Log.e(TAG, "Error loading url " + workerOptions.getResourceUri(), e);
109            return null;
110        } catch (RuntimeException e) {
111            Log.e(TAG, "Critical Error loading url " + workerOptions.getResourceUri(), e);
112            return null;
113        }
114
115        return null;
116    }
117
118    @Override
119    protected void onPostExecute(Bitmap bitmap) {
120        if (mImageView != null) {
121            final ImageView imageView = mImageView.get();
122            if (imageView != null) {
123                imageView.setImageBitmap(bitmap);
124            }
125        }
126    }
127
128    private Bitmap getBitmapFromResource(Context context, ShortcutIconResource iconResource,
129            BitmapWorkerOptions outputOptions) throws IOException {
130        if (DEBUG) {
131            Log.d(TAG, "Loading " + iconResource.toString());
132        }
133        String packageName = iconResource.packageName;
134        String resourceName = iconResource.resourceName;
135        try {
136            Object drawable = loadDrawable(context, iconResource);
137            if (drawable instanceof InputStream) {
138                // Most of these are bitmaps, so resize properly.
139                return decodeBitmap((InputStream)drawable, outputOptions);
140            } else if (drawable instanceof Drawable){
141                return createIconBitmap((Drawable) drawable, outputOptions);
142            } else {
143                Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable);
144                return null;
145            }
146        } catch (NameNotFoundException e) {
147            Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound");
148            return null;
149        } catch (NotFoundException e) {
150            Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound");
151            return null;
152        }
153    }
154
155    public final boolean isScaled() {
156        return mScaled;
157    }
158
159    /**
160     * Scales the bitmap if either of the dimensions is out of range.
161     */
162    private Bitmap scaleBitmapIfNecessary(BitmapWorkerOptions outputOptions, Bitmap bitmap) {
163        if (bitmap == null) {
164            return null;
165        }
166
167        float heightScale = 1f;
168        {
169            if (bitmap.getHeight() > outputOptions.getHeight()) {
170                heightScale = (float) outputOptions.getHeight() / (float) bitmap.getHeight();
171            }
172        }
173
174        float widthScale = 1f;
175        {
176            if (bitmap.getWidth() > outputOptions.getWidth()) {
177                widthScale = (float) outputOptions.getWidth() / (float) bitmap.getWidth();
178            }
179        }
180        float scale = heightScale < widthScale ? heightScale : widthScale;
181        if (scale >= 1.0f) {
182            return bitmap;
183        } else {
184            int width = (int) (bitmap.getWidth() * scale);
185            int height = (int) (bitmap.getHeight() * scale);
186            if (DEBUG) {
187                Log.d(TAG, "Scaling bitmap " + ((outputOptions.getResourceUri() != null)
188                                        ? outputOptions.getResourceUri().toString()
189                                        : outputOptions.getIconResource().toString()) + " from "
190                                + bitmap.getWidth() + "x" + bitmap.getHeight() + " to " + width
191                                + "x" + height + " scale " + scale);
192            }
193            Bitmap newBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
194            mScaled = true;
195            return newBitmap;
196        }
197    }
198
199    private Bitmap decodeBitmap(InputStream in, BitmapWorkerOptions options)
200            throws IOException {
201        CachedInputStream bufferedStream = null;
202        BitmapFactory.Options bitmapOptions = null;
203        try {
204            bufferedStream = new CachedInputStream(in);
205            // Let the bufferedStream be able to mark unlimited bytes up to full stream length.
206            // The value that BitmapFactory uses (1024) is too small for detecting bounds
207            bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE);
208            bitmapOptions = new BitmapFactory.Options();
209            bitmapOptions.inJustDecodeBounds = true;
210            if (options.getBitmapConfig() != null) {
211                bitmapOptions.inPreferredConfig = options.getBitmapConfig();
212            }
213            bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk();
214            bufferedStream.mark(Integer.MAX_VALUE);
215            BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
216
217            float heightScale = 1f;
218            {
219                int height = options.getHeight();
220                if (height > 0) {
221                    heightScale = (float) bitmapOptions.outHeight / height;
222                }
223            }
224
225            float widthScale = 1f;
226            {
227                int width = options.getWidth();
228                if (width > 0) {
229                    widthScale = (float) bitmapOptions.outWidth / width;
230                }
231            }
232
233            float scale = heightScale > widthScale ? heightScale : widthScale;
234
235            if (DEBUG) {
236                Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x"
237                        + bitmapOptions.outHeight + ").  Max size: (" + options.getWidth() + "x"
238                        + options.getHeight() + ").  Chosen scale: " + scale + " -> "
239                        + (int) scale);
240            }
241
242            bitmapOptions.inJustDecodeBounds = false;
243            if (scale >= 2) {
244                bitmapOptions.inSampleSize = (int) scale;
245            }
246            // Reset buffer to original position and disable the overrideMarkLimit
247            bufferedStream.reset();
248            bufferedStream.setOverrideMarkLimit(0);
249            return scaleBitmapIfNecessary(options,
250                    BitmapFactory.decodeStream(bufferedStream, null,
251                            bitmapOptions));
252
253        } finally {
254            if (bitmapOptions != null) {
255                ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage);
256            }
257            if (bufferedStream != null) {
258                bufferedStream.close();
259            }
260        }
261    }
262
263    private Bitmap getBitmapFromHttp(BitmapWorkerOptions options) throws IOException {
264        URL url = new URL(options.getResourceUri().toString());
265        if (DEBUG) {
266            Log.d(TAG, "Loading " + url);
267        }
268        try {
269            URLConnection connection = url.openConnection();
270            connection.setConnectTimeout(SOCKET_TIMEOUT);
271            connection.setReadTimeout(READ_TIMEOUT);
272            InputStream in = new BufferedInputStream(connection.getInputStream());
273            return decodeBitmap(in, options);
274        } finally {
275            if (DEBUG) {
276                Log.d(TAG, "loading done "+url);
277            }
278        }
279    }
280
281    private Bitmap getBitmapFromContent(BitmapWorkerOptions options) throws IOException {
282        InputStream bitmapStream =
283                options.getContext().getContentResolver().openInputStream(options.getResourceUri());
284        if (bitmapStream != null) {
285            return decodeBitmap(bitmapStream, options);
286        } else {
287            Log.w(TAG, "Content provider returned a null InputStream when trying to " +
288                    "open resource.");
289            return null;
290        }
291    }
292
293    /**
294     * load drawable for non-bitmap resource or InputStream for bitmap resource without
295     * caching Bitmap in Resources.  So that caller can maintain a different caching
296     * storage with less memory used.
297     * @return  either {@link Drawable} for xml and ColorDrawable <br>
298     *          or {@link InputStream} for Bitmap resource
299     */
300    private static Object loadDrawable(Context context, ShortcutIconResource r)
301            throws NameNotFoundException {
302        Resources resources = context.getPackageManager()
303                .getResourcesForApplication(r.packageName);
304        if (resources == null) {
305            return null;
306        }
307        final int id = resources.getIdentifier(r.resourceName, null, null);
308        if (id == 0) {
309            Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of "
310                    + r.packageName);
311            return null;
312        }
313        TypedValue value = new TypedValue();
314        resources.getValue(id, value, true);
315        if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || (
316                value.type >= TypedValue.TYPE_FIRST_COLOR_INT
317                && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) {
318            return resources.getDrawable(id);
319        }
320        return resources.openRawResource(id, value);
321    }
322
323    private static Bitmap createIconBitmap(Drawable drawable, BitmapWorkerOptions workerOptions) {
324        // Some drawables have an intrinsic width and height of -1. In that case
325        // size it to our output.
326        int width = drawable.getIntrinsicWidth();
327        if (width == -1) {
328            width = workerOptions.getWidth();
329        }
330        int height = drawable.getIntrinsicHeight();
331        if (height == -1) {
332            height = workerOptions.getHeight();
333        }
334        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
335        Canvas canvas = new Canvas(bitmap);
336        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
337        drawable.draw(canvas);
338        return bitmap;
339    }
340
341    public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
342            throws NameNotFoundException {
343        Resources resources =
344                context.getPackageManager().getResourcesForApplication(iconResource.packageName);
345        int id = resources.getIdentifier(iconResource.resourceName, null, null);
346        if (id == 0) {
347            throw new NameNotFoundException();
348        }
349        return resources.getDrawable(id);
350    }
351
352    private Bitmap getAccountImage(BitmapWorkerOptions options) {
353        String accountName = UriUtils.getAccountName(options.getResourceUri());
354        Context context = options.getContext();
355
356        if (accountName != null && context != null) {
357            Account thisAccount = null;
358            for (Account account : AccountManager.get(context).
359                    getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
360                if (account.name.equals(accountName)) {
361                    thisAccount = account;
362                    break;
363                }
364            }
365            if (thisAccount != null) {
366                String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount);
367                if (picUriString != null) {
368                    BitmapWorkerOptions.Builder optionBuilder =
369                            new BitmapWorkerOptions.Builder(context)
370                            .width(options.getWidth())
371                                    .height(options.getHeight())
372                                    .cacheFlag(options.getCacheFlag())
373                                    .bitmapConfig(options.getBitmapConfig())
374                                    .resource(Uri.parse(picUriString));
375                    return BitmapDownloader.getInstance(context)
376                            .loadBitmapBlocking(optionBuilder.build());
377                }
378                return null;
379            }
380        }
381        return null;
382    }
383}
384