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