1 /* 2 * Copyright (C) 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 package android.car.cluster; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.car.cluster.navigation.NavigationState.ImageReference; 21 import android.graphics.Bitmap; 22 import android.graphics.Point; 23 import android.net.Uri; 24 import android.util.Log; 25 26 import java.util.List; 27 import java.util.Map; 28 import java.util.concurrent.CompletableFuture; 29 import java.util.stream.Collectors; 30 31 /** 32 * Class for retrieving bitmap images from a ContentProvider 33 * 34 * @hide 35 */ 36 public class ImageResolver { 37 private static final String TAG = "Cluster.ImageResolver"; 38 private final BitmapFetcher mFetcher; 39 40 /** 41 * Interface used for fetching bitmaps from a content resolver 42 */ 43 public interface BitmapFetcher { 44 /** 45 * Returns a {@link Bitmap} given a request Uri and dimensions 46 */ getBitmap(@onNull Uri uri, int width, int height)47 Bitmap getBitmap(@NonNull Uri uri, int width, int height); 48 49 /** 50 * Returns a {@link Bitmap} given a request Uri, dimensions, and offLanesAlpha value 51 */ getBitmap(@onNull Uri uri, int width, int height, float offLanesAlpha)52 Bitmap getBitmap(@NonNull Uri uri, int width, int height, float offLanesAlpha); 53 } 54 55 /** 56 * Creates a resolver that delegate the image retrieval to the given fetcher. 57 */ ImageResolver(BitmapFetcher fetcher)58 public ImageResolver(BitmapFetcher fetcher) { 59 mFetcher = fetcher; 60 } 61 62 /** 63 * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}. 64 * This image would fit inside the provided size. Either width, height or both should be greater 65 * than 0. 66 * 67 * @param width required width, or 0 if width is flexible based on height. 68 * @param height required height, or 0 if height is flexible based on width. 69 */ 70 @NonNull getBitmap(@onNull ImageReference img, int width, int height)71 public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height) { 72 return getBitmap(img, width, height, 1f); 73 } 74 75 /** 76 * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}. 77 * This image would fit inside the provided size. Either width, height or both should be greater 78 * than 0. 79 * 80 * @param width required width, or 0 if width is flexible based on height. 81 * @param height required height, or 0 if height is flexible based on width. 82 * @param offLanesAlpha opacity value for off lane guidance images. Only applies to lane 83 * guidance images. 0 (transparent) <= offLanesAlpha <= 1 (opaque). 84 */ 85 @NonNull getBitmap(@onNull ImageReference img, int width, int height, float offLanesAlpha)86 public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height, 87 float offLanesAlpha) { 88 if (Log.isLoggable(TAG, Log.DEBUG)) { 89 Log.d(TAG, String.format("Requesting image %s (width: %d, height: %d)", 90 img.getContentUri(), width, height)); 91 } 92 93 return CompletableFuture.supplyAsync(() -> { 94 // Adjust the size to fit in the requested box. 95 Point adjusted = getAdjustedSize(img.getAspectRatio(), width, height); 96 if (adjusted == null) { 97 Log.e(TAG, "The provided image has no aspect ratio: " + img.getContentUri()); 98 return null; 99 } 100 101 Uri uri = Uri.parse(img.getContentUri()); 102 Bitmap bitmap = null; 103 try { 104 bitmap = mFetcher.getBitmap(uri, adjusted.x, adjusted.y, offLanesAlpha); 105 } catch (IllegalArgumentException e) { 106 Log.e(TAG, e.getMessage()); 107 } 108 if (bitmap == null) { 109 if (Log.isLoggable(TAG, Log.DEBUG)) { 110 Log.d(TAG, "Unable to fetch image: " + uri); 111 } 112 return null; 113 } 114 115 if (Log.isLoggable(TAG, Log.DEBUG)) { 116 Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)", 117 img.getContentUri(), width, height)); 118 } 119 return bitmap; 120 }); 121 } 122 123 /** 124 * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The 125 * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to 126 * its bitmap. If any image fails to be fetched, the whole future completes exceptionally. 127 * 128 * @param width required width, or 0 if width is flexible based on height. 129 * @param height required height, or 0 if height is flexible based on width. 130 */ 131 @NonNull getBitmaps( @onNull List<ImageReference> imgs, int width, int height)132 public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps( 133 @NonNull List<ImageReference> imgs, int width, int height) { 134 return getBitmaps(imgs, width, height, 1f); 135 } 136 137 /** 138 * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The 139 * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to 140 * its bitmap. If any image fails to be fetched, the whole future completes exceptionally. 141 * 142 * @param width required width, or 0 if width is flexible based on height. 143 * @param height required height, or 0 if height is flexible based on width. 144 * @param offLanesAlpha opacity value for off lane guidance images. Only applies to lane 145 * guidance images. 0 (transparent) <= offLanesAlpha <= 1 (opaque). 146 */ 147 @NonNull getBitmaps( @onNull List<ImageReference> imgs, int width, int height, float offLanesAlpha)148 public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps( 149 @NonNull List<ImageReference> imgs, int width, int height, float offLanesAlpha) { 150 CompletableFuture<Map<ImageReference, Bitmap>> future = new CompletableFuture<>(); 151 152 Map<ImageReference, CompletableFuture<Bitmap>> bitmapFutures = imgs.stream().collect( 153 Collectors.toMap( 154 img -> img, 155 img -> getBitmap(img, width, height, offLanesAlpha))); 156 157 CompletableFuture.allOf(bitmapFutures.values().toArray(new CompletableFuture[0])) 158 .thenAccept(v -> { 159 Map<ImageReference, Bitmap> bitmaps = bitmapFutures.entrySet().stream() 160 .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry 161 .getValue().join())); 162 future.complete(bitmaps); 163 }) 164 .exceptionally(ex -> { 165 future.completeExceptionally(ex); 166 return null; 167 }); 168 169 return future; 170 } 171 172 /** 173 * Returns an image size that exactly fits inside a requested box, maintaining an original size 174 * aspect ratio. 175 * 176 * @param imageRatio original aspect ratio (must be > 0) 177 * @param requestedWidth required width, or 0 if width is flexible based on height. 178 * @param requestedHeight required height, or 0 if height is flexible based on width. 179 */ 180 @Nullable getAdjustedSize(double imageRatio, int requestedWidth, int requestedHeight)181 public Point getAdjustedSize(double imageRatio, int requestedWidth, 182 int requestedHeight) { 183 if (imageRatio <= 0) { 184 return null; 185 } else if (requestedWidth == 0 && requestedHeight == 0) { 186 throw new IllegalArgumentException("At least one of width or height must be != 0"); 187 } 188 // If width is flexible or if both width and height are set and the original image is wider 189 // than the space provided, then scale the width. 190 float requiredRatio = requestedHeight > 0 ? ((float) requestedWidth) / requestedHeight : 0; 191 Point res = new Point(requestedWidth, requestedHeight); 192 if (requestedWidth == 0 || (requestedHeight != 0 && imageRatio < requiredRatio)) { 193 res.x = (int) (imageRatio * requestedHeight); 194 } else { 195 res.y = (int) (requestedWidth / imageRatio); 196 } 197 return res; 198 } 199 } 200