1 /*
2  * Copyright 2018 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 
17 package com.android.pump.util;
18 
19 import android.graphics.Bitmap;
20 import android.graphics.BitmapFactory;
21 import android.net.Uri;
22 
23 import androidx.annotation.AnyThread;
24 import androidx.annotation.NonNull;
25 import androidx.annotation.Nullable;
26 import androidx.collection.ArrayMap;
27 import androidx.collection.ArraySet;
28 
29 import com.android.pump.concurrent.Executors;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.AbstractMap.SimpleEntry;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.concurrent.Executor;
39 
40 @AnyThread
41 public class ImageLoader {
42     private static final String TAG = Clog.tag(ImageLoader.class);
43 
44     private final BitmapCache mBitmapCache = new BitmapCache();
45     private final OrientationCache mOrientationCache = new OrientationCache();
46     private final Executor mExecutor;
47     private final Set<Map.Entry<Executor, Callback>> mCallbacks = new ArraySet<>();
48     private final Map<Uri, List<Map.Entry<Executor, Callback>>> mLoadCallbacks = new ArrayMap<>();
49 
50     @FunctionalInterface
51     public interface Callback {
onImageLoaded(@onNull Uri uri, @Nullable Bitmap bitmap)52         void onImageLoaded(@NonNull Uri uri, @Nullable Bitmap bitmap);
53     }
54 
ImageLoader(@onNull Executor executor)55     public ImageLoader(@NonNull Executor executor) {
56         mExecutor = executor;
57     }
58 
addCallback(@onNull Callback callback)59     public void addCallback(@NonNull Callback callback) {
60         addCallback(callback, Executors.uiThreadExecutor());
61     }
62 
addCallback(@onNull Callback callback, @NonNull Executor executor)63     public void addCallback(@NonNull Callback callback, @NonNull Executor executor) {
64         synchronized (this) { // TODO(b/123708613) other lock
65             if (!mCallbacks.add(new SimpleEntry<>(executor, callback))) {
66                 throw new IllegalArgumentException("Callback " + callback + " already added");
67             }
68         }
69     }
70 
removeCallback(@onNull Callback callback)71     public void removeCallback(@NonNull Callback callback) {
72         removeCallback(callback, Executors.uiThreadExecutor());
73     }
74 
removeCallback(@onNull Callback callback, @NonNull Executor executor)75     public void removeCallback(@NonNull Callback callback, @NonNull Executor executor) {
76         synchronized (this) { // TODO(b/123708613) other lock
77             if (!mCallbacks.remove(new SimpleEntry<>(executor, callback))) {
78                 throw new IllegalArgumentException("Callback " + callback + " not found");
79             }
80         }
81     }
82 
loadImage(@onNull Uri uri, @NonNull Callback callback)83     public void loadImage(@NonNull Uri uri, @NonNull Callback callback) {
84         loadImage(uri, callback, Executors.uiThreadExecutor());
85     }
86 
loadImage(@onNull Uri uri, @NonNull Callback callback, @NonNull Executor executor)87     public void loadImage(@NonNull Uri uri, @NonNull Callback callback,
88             @NonNull Executor executor) {
89         Bitmap bitmap;
90         Runnable loader = null;
91         synchronized (this) { // TODO(b/123708613) other lock
92             bitmap = mBitmapCache.get(uri);
93             if (bitmap == null) {
94                 List<Map.Entry<Executor, Callback>> callbacks = mLoadCallbacks.get(uri);
95                 if (callbacks == null) {
96                     callbacks = new LinkedList<>();
97                     mLoadCallbacks.put(uri, callbacks);
98                     loader = new ImageLoaderTask(uri);
99                 }
100                 callbacks.add(new SimpleEntry<>(executor, callback));
101             }
102         }
103         if (bitmap != null) {
104             executor.execute(() -> callback.onImageLoaded(uri, bitmap));
105         } else if (loader != null) {
106             mExecutor.execute(loader);
107         }
108     }
109 
getOrientation(@onNull Uri uri)110     public @Orientation int getOrientation(@NonNull Uri uri) {
111         return mOrientationCache.get(uri);
112     }
113 
114     private class ImageLoaderTask implements Runnable {
115         private final Uri mUri;
116 
ImageLoaderTask(@onNull Uri uri)117         private ImageLoaderTask(@NonNull Uri uri) {
118             mUri = uri;
119         }
120 
121         @Override
run()122         public void run() {
123             try {
124                 byte[] data;
125                 if (Scheme.isFile(mUri)) {
126                     data = IoUtils.readFromFile(new File(mUri.getPath()));
127                 } else if (Scheme.isHttp(mUri) || Scheme.isHttps(mUri)) {
128                     data = Http.get(mUri.toString());
129                 } else {
130                     throw new IllegalArgumentException("Unknown scheme '" + mUri.getScheme() + "'");
131                 }
132                 Bitmap bitmap = decodeBitmapFromByteArray(data);
133                 Set<Map.Entry<Executor, Callback>> callbacks;
134                 List<Map.Entry<Executor, Callback>> loadCallbacks;
135                 synchronized (ImageLoader.this) { // TODO(b/123708613) proper lock
136                     if (bitmap != null) {
137                         mBitmapCache.put(mUri, bitmap);
138                         mOrientationCache.put(mUri, bitmap);
139                     }
140                     callbacks = new ArraySet<>(mCallbacks);
141                     loadCallbacks = mLoadCallbacks.remove(mUri);
142                 }
143                 for (Map.Entry<Executor, Callback> callback : callbacks) {
144                     callback.getKey().execute(() ->
145                             callback.getValue().onImageLoaded(mUri, bitmap));
146                 }
147                 for (Map.Entry<Executor, Callback> callback : loadCallbacks) {
148                     callback.getKey().execute(() ->
149                             callback.getValue().onImageLoaded(mUri, bitmap));
150                 }
151             } catch (IOException | OutOfMemoryError e) {
152                 Clog.e(TAG, "Failed to load image " + mUri, e);
153                 // TODO(b/123708676) remove from mLoadCallbacks
154             }
155         }
156 
decodeBitmapFromByteArray(@onNull byte[] data)157         private @Nullable Bitmap decodeBitmapFromByteArray(@NonNull byte[] data) {
158             BitmapFactory.Options options = new BitmapFactory.Options();
159 
160             options.inJustDecodeBounds = true;
161             BitmapFactory.decodeByteArray(data, 0, data.length, options);
162 
163             options.inJustDecodeBounds = false;
164             options.inSampleSize = 1; // TODO(b/123708796) add scaling
165             return BitmapFactory.decodeByteArray(data, 0, data.length, options);
166         }
167     }
168 }
169