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