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