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