1 /* 2 * Copyright 2019 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.car.apps.common.imaging; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.ImageDecoder; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.util.Log; 29 import android.util.LruCache; 30 31 import androidx.annotation.UiThread; 32 33 import com.android.car.apps.common.BitmapUtils; 34 import com.android.car.apps.common.CommonFlags; 35 import com.android.car.apps.common.R; 36 import com.android.car.apps.common.UriUtils; 37 import com.android.car.apps.common.util.CarAppsIOUtils; 38 39 import java.io.BufferedInputStream; 40 import java.io.ByteArrayOutputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.lang.ref.WeakReference; 44 import java.net.URL; 45 import java.nio.ByteBuffer; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.Map; 49 import java.util.concurrent.CancellationException; 50 import java.util.concurrent.Executor; 51 import java.util.concurrent.Executors; 52 import java.util.function.BiConsumer; 53 54 55 /** 56 * A singleton that fetches images and offers a simple memory cache. The requests and the replies 57 * all happen on the UI thread. 58 */ 59 public class LocalImageFetcher { 60 61 private static final String TAG = "LocalImageFetcher"; 62 private static final boolean L_WARN = Log.isLoggable(TAG, Log.WARN); 63 private static final boolean L_DEBUG = Log.isLoggable(TAG, Log.DEBUG); 64 65 private static final int KB = 1024; 66 private static final int MB = KB * KB; 67 68 /** Should not be reset to null once created. */ 69 private static LocalImageFetcher sInstance; 70 71 /** Returns the singleton. */ getInstance(Context context)72 public static LocalImageFetcher getInstance(Context context) { 73 if (sInstance == null) { 74 sInstance = new LocalImageFetcher(context); 75 } 76 return sInstance; 77 } 78 79 private final int mPoolSize; 80 81 private final LruCache<String, Executor> mThreadPools; 82 83 private final Map<ImageKey, HashSet<BiConsumer<ImageKey, Drawable>>> mConsumers = 84 new HashMap<>(20); 85 private final Map<ImageKey, ImageLoadingTask> mTasks = new HashMap<>(20); 86 87 private final LruCache<ImageKey, Drawable> mMemoryCache; 88 89 private final boolean mFlagRemoteImages; 90 91 @UiThread LocalImageFetcher(Context context)92 private LocalImageFetcher(Context context) { 93 Resources res = context.getResources(); 94 int maxPools = res.getInteger(R.integer.image_fetcher_thread_pools_max_count); 95 mPoolSize = res.getInteger(R.integer.image_fetcher_thread_pool_size); 96 mThreadPools = new LruCache<>(maxPools); 97 98 int cacheSizeMB = res.getInteger(R.integer.bitmap_memory_cache_max_size_mb); 99 int drawableDefaultWeightKB = res.getInteger(R.integer.drawable_default_weight_kb); 100 mMemoryCache = new LruCache<ImageKey, Drawable>(cacheSizeMB * MB) { 101 @Override 102 protected int sizeOf(ImageKey key, Drawable drawable) { 103 if (drawable instanceof BitmapDrawable) { 104 return ((BitmapDrawable) drawable).getBitmap().getAllocationByteCount(); 105 } else { 106 // For now 107 // TODO(b/139386940): consider a more accurate sizing / caching strategy. 108 return drawableDefaultWeightKB * KB; 109 } 110 } 111 }; 112 113 mFlagRemoteImages = CommonFlags.getInstance(context).shouldFlagImproperImageRefs(); 114 } 115 getThreadPool(String packageName)116 private Executor getThreadPool(String packageName) { 117 Executor result = mThreadPools.get(packageName); 118 if (result == null) { 119 result = Executors.newFixedThreadPool(mPoolSize); 120 mThreadPools.put(packageName, result); 121 } 122 return result; 123 } 124 125 /** Fetches an image. The resulting drawable may be null. */ 126 @UiThread getImage(Context context, ImageKey key, BiConsumer<ImageKey, Drawable> consumer)127 public void getImage(Context context, ImageKey key, BiConsumer<ImageKey, Drawable> consumer) { 128 Drawable cached = mMemoryCache.get(key); 129 if (cached != null) { 130 consumer.accept(key, cached); 131 return; 132 } 133 134 ImageLoadingTask task = mTasks.get(key); 135 136 HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.get(key); 137 if (consumers == null) { 138 consumers = new HashSet<>(3); 139 if (task != null && L_WARN) { 140 Log.w(TAG, "Expected no task here for " + key); 141 } 142 mConsumers.put(key, consumers); 143 } 144 consumers.add(consumer); 145 146 if (task == null) { 147 String packageName = UriUtils.getPackageName(context, key.mImageUri); 148 if (packageName != null) { 149 task = new ImageLoadingTask(context, key, mFlagRemoteImages); 150 mTasks.put(key, task); 151 task.executeOnExecutor(getThreadPool(packageName)); 152 if (L_DEBUG) { 153 Log.d(TAG, "Added task " + key.mImageUri); 154 } 155 } else { 156 Log.e(TAG, "No package for " + key.mImageUri); 157 } 158 } 159 } 160 161 /** Cancels a request made via {@link #getImage}. */ 162 @UiThread cancelRequest(ImageKey key, BiConsumer<ImageKey, Drawable> consumer)163 public void cancelRequest(ImageKey key, BiConsumer<ImageKey, Drawable> consumer) { 164 HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.get(key); 165 if (consumers != null) { 166 boolean removed = consumers.remove(consumer); 167 if (consumers.isEmpty()) { 168 // Nobody else wants this image, remove the set and cancel the task. 169 mConsumers.remove(key); 170 ImageLoadingTask task = mTasks.remove(key); 171 if (task != null) { 172 task.cancel(true); 173 if (L_DEBUG) { 174 Log.d(TAG, "Canceled task " + key.mImageUri); 175 } 176 } else if (L_WARN) { 177 Log.w(TAG, "cancelRequest missing task for: " + key); 178 } 179 } 180 181 if (!removed && L_WARN) { 182 Log.w(TAG, "cancelRequest missing consumer for: " + key); 183 } 184 } else if (L_WARN) { 185 Log.w(TAG, "cancelRequest has no consumers for: " + key); 186 } 187 } 188 189 190 @UiThread fulfilRequests(ImageLoadingTask task, Drawable drawable)191 private void fulfilRequests(ImageLoadingTask task, Drawable drawable) { 192 ImageKey key = task.mImageKey; 193 ImageLoadingTask pendingTask = mTasks.get(key); 194 if (pendingTask == task) { 195 if (drawable != null) { 196 mMemoryCache.put(key, drawable); 197 } 198 199 HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.remove(key); 200 mTasks.remove(key); 201 if (consumers != null) { 202 for (BiConsumer<ImageKey, Drawable> consumer : consumers) { 203 consumer.accept(key, drawable); 204 } 205 } 206 } else if (L_WARN) { 207 // This case would possible if a running task was canceled, a new one was restarted 208 // right away for the same key, and the canceled task still managed to call 209 // fulfilRequests (despite the !isCancelled check). 210 Log.w(TAG, "A new task already started for: " + task.mImageKey); 211 } 212 } 213 214 215 private static class ImageLoadingTask extends AsyncTask<Void, Void, Drawable> { 216 217 private final WeakReference<Context> mWeakContext; 218 private final ImageKey mImageKey; 219 private final boolean mFlagRemoteImages; 220 221 222 @UiThread ImageLoadingTask(Context context, ImageKey request, boolean flagRemoteImages)223 ImageLoadingTask(Context context, ImageKey request, boolean flagRemoteImages) { 224 mWeakContext = new WeakReference<>(context.getApplicationContext()); 225 mImageKey = request; 226 mFlagRemoteImages = flagRemoteImages; 227 } 228 229 /** Runs in the background. */ 230 private final ImageDecoder.OnHeaderDecodedListener mOnHeaderDecodedListener = 231 new ImageDecoder.OnHeaderDecodedListener() { 232 @Override 233 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 234 ImageDecoder.Source source) { 235 if (isCancelled()) throw new CancellationException(); 236 decoder.setAllocator(mAllocatorMode); 237 int maxW = mImageKey.mMaxImageSize.getWidth(); 238 int maxH = mImageKey.mMaxImageSize.getHeight(); 239 int imgW = info.getSize().getWidth(); 240 int imgH = info.getSize().getHeight(); 241 if (imgW > maxW || imgH > maxH) { 242 float scale = Math.min(maxW / (float) imgW, maxH / (float) imgH); 243 decoder.setTargetSize(Math.round(scale * imgW), Math.round(scale * imgH)); 244 } 245 } 246 }; 247 248 // ALLOCATOR_HARDWARE causes crashes on some emulators (in media center's queue). 249 private int mAllocatorMode = ImageDecoder.ALLOCATOR_SOFTWARE; 250 251 @Override doInBackground(Void... voids)252 protected Drawable doInBackground(Void... voids) { 253 try { 254 if (isCancelled()) return null; 255 Uri imageUri = mImageKey.mImageUri; 256 257 Context context = mWeakContext.get(); 258 if (context == null) return null; 259 260 if (UriUtils.isAndroidResourceUri(imageUri)) { 261 // ImageDecoder doesn't support all resources via the content provider... 262 return UriUtils.getDrawable(context, 263 UriUtils.getIconResource(context, imageUri)); 264 } else if (UriUtils.isContentUri(imageUri)) { 265 ContentResolver resolver = context.getContentResolver(); 266 ImageDecoder.Source src = ImageDecoder.createSource(resolver, imageUri); 267 return ImageDecoder.decodeDrawable(src, mOnHeaderDecodedListener); 268 } else if (mFlagRemoteImages) { 269 mAllocatorMode = ImageDecoder.ALLOCATOR_SOFTWARE; // Needed for canvas drawing. 270 URL url = new URL(imageUri.toString()); 271 272 try (InputStream is = new BufferedInputStream(url.openStream()); 273 ByteArrayOutputStream bytes = new ByteArrayOutputStream()) { 274 275 CarAppsIOUtils.copy(is, bytes); 276 ImageDecoder.Source src = 277 ImageDecoder.createSource(ByteBuffer.wrap(bytes.toByteArray())); 278 Bitmap decoded = ImageDecoder.decodeBitmap(src, mOnHeaderDecodedListener); 279 Bitmap tinted = BitmapUtils.createTintedBitmap(decoded, 280 context.getColor(R.color.improper_image_refs_tint_color)); 281 return new BitmapDrawable(context.getResources(), tinted); 282 } 283 } 284 } catch (IOException ioe) { 285 Log.e(TAG, "ImageLoadingTask#doInBackground: " + ioe); 286 } catch (CancellationException e) { 287 return null; 288 } 289 return null; 290 } 291 292 @UiThread 293 @Override onPostExecute(Drawable drawable)294 protected void onPostExecute(Drawable drawable) { 295 if (L_DEBUG) { 296 Log.d(TAG, "onPostExecute canceled: " + isCancelled() + " drawable: " + drawable 297 + " " + mImageKey.mImageUri); 298 } 299 if (!isCancelled()) { 300 if (sInstance != null) { 301 sInstance.fulfilRequests(this, drawable); 302 } else { 303 Log.e(TAG, "ImageLoadingTask#onPostExecute: LocalImageFetcher was reset !"); 304 } 305 } 306 } 307 } 308 } 309