Merge "Fixing multi-user image fetching and implementing async fetch"
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 185c57f..3459e1f 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -169,6 +169,7 @@
public abstract class InstrumentClusterRenderingService extends android.app.Service {
ctor public InstrumentClusterRenderingService();
+ method @Nullable public android.graphics.Bitmap getBitmap(android.net.Uri);
method @MainThread @Nullable public abstract android.car.cluster.renderer.NavigationRenderer getNavigationRenderer();
method @CallSuper public android.os.IBinder onBind(android.content.Intent);
method @MainThread public void onKeyEvent(@NonNull android.view.KeyEvent);
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 4a9e5f5..f00d6ff 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -32,11 +32,16 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
@@ -47,14 +52,19 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
/**
- * A service that used for interaction between Car Service and Instrument Cluster. Car Service may
+ * A service used for interaction between Car Service and Instrument Cluster. Car Service may
* provide internal navigation binder interface to Navigation App and all notifications will be
* eventually land in the {@link NavigationRenderer} returned by {@link #getNavigationRenderer()}.
*
@@ -85,15 +95,45 @@
private static class ContextOwner {
final int mUid;
final int mPid;
+ final Set<String> mPackageNames;
+ final Set<String> mAuthorities;
- ContextOwner(int uid, int pid) {
+ ContextOwner(int uid, int pid, PackageManager packageManager) {
mUid = uid;
mPid = pid;
+ String[] packageNames = uid != 0 ? packageManager.getPackagesForUid(uid)
+ : null;
+ mPackageNames = packageNames != null
+ ? Collections.unmodifiableSet(new HashSet<>(Arrays.asList(packageNames)))
+ : Collections.emptySet();
+ mAuthorities = Collections.unmodifiableSet(mPackageNames.stream()
+ .map(packageName -> getAuthoritiesForPackage(packageManager, packageName))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet()));
}
@Override
public String toString() {
- return "{uid: " + mUid + ", pid: " + mPid + "}";
+ return "{uid: " + mUid + ", pid: " + mPid + ", packagenames: " + mPackageNames
+ + ", authorities: " + mAuthorities + "}";
+ }
+
+ private List<String> getAuthoritiesForPackage(PackageManager packageManager,
+ String packageName) {
+ try {
+ ProviderInfo[] providers = packageManager.getPackageInfo(packageName,
+ PackageManager.GET_PROVIDERS).providers;
+ if (providers == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.stream(providers)
+ .map(provider -> provider.authority)
+ .collect(Collectors.toList());
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Package name not found while retrieving content provider authorities: "
+ + packageName);
+ return Collections.emptyList();
+ }
}
}
@@ -199,7 +239,7 @@
*/
@Nullable
private ComponentName getNavigationComponentByOwner(ContextOwner contextOwner) {
- for (String packageName : getPackageNamesForUid(contextOwner)) {
+ for (String packageName : contextOwner.mPackageNames) {
ComponentName component = getComponentFromPackage(packageName);
if (component != null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -211,14 +251,6 @@
return null;
}
- private String[] getPackageNamesForUid(ContextOwner contextOwner) {
- if (contextOwner == null || contextOwner.mUid == 0 || contextOwner.mPid == 0) {
- return new String[0];
- }
- String[] packageNames = getPackageManager().getPackagesForUid(contextOwner.mUid);
- return packageNames != null ? packageNames : new String[0];
- }
-
private ContextOwner getNavigationContextOwner() {
synchronized (mLock) {
return mNavContextOwner;
@@ -338,8 +370,7 @@
writer.println("activity options: " + mActivityOptions);
writer.println("activity state: " + mActivityState);
writer.println("current nav component: " + mNavigationComponent);
- writer.println("current nav packages: " + Arrays.toString(getPackageNamesForUid(
- getNavigationContextOwner())));
+ writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
}
private class RendererBinder extends IInstrumentCluster.Stub {
@@ -356,8 +387,11 @@
@Override
public void setNavigationContextOwner(int uid, int pid) throws RemoteException {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Updating navigation ownership to uid: " + uid + ", pid: " + pid);
+ }
synchronized (mLock) {
- mNavContextOwner = new ContextOwner(uid, pid);
+ mNavContextOwner = new ContextOwner(uid, pid, getPackageManager());
}
mUiHandler.post(InstrumentClusterRenderingService.this::updateNavigationActivity);
}
@@ -419,4 +453,57 @@
}
return result.get();
}
+
+ /**
+ * Fetches a bitmap from the navigation context owner (application holding navigation focus).
+ * It returns null if:
+ * <ul>
+ * <li>there is no navigation context owner
+ * <li>or if the {@link Uri} is invalid
+ * <li>or if it references a process other than the current navigation context owner
+ * </ul>
+ * This is a costly operation. Returned bitmaps should be cached and fetching should be done on
+ * a secondary thread.
+ */
+ @Nullable
+ public Bitmap getBitmap(Uri uri) {
+ try {
+ ContextOwner contextOwner = getNavigationContextOwner();
+ if (contextOwner == null) {
+ Log.e(TAG, "No context owner available while fetching: " + uri);
+ return null;
+ }
+
+ String host = uri.getHost();
+
+ if (!contextOwner.mAuthorities.contains(host)) {
+ Log.e(TAG, "Uri points to an authority not handled by the current context owner: "
+ + uri + " (valid authorities: " + contextOwner.mAuthorities + ")");
+ return null;
+ }
+
+ // Add user to URI to make the request to the right instance of content provider
+ // (see ContentProvider#getUserIdFromAuthority()).
+ int userId = UserHandle.getUserId(contextOwner.mUid);
+ Uri filteredUid = uri.buildUpon().encodedAuthority(userId + "@" + host).build();
+
+ // Fetch the bitmap
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Requesting bitmap: " + uri);
+ }
+ ParcelFileDescriptor fileDesc = getContentResolver()
+ .openFileDescriptor(filteredUid, "r");
+ if (fileDesc != null) {
+ Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
+ fileDesc.close();
+ return bitmap;
+ } else {
+ Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+ }
+ } catch (Throwable e) {
+ Log.e(TAG, "Unable to fetch uri: " + uri, e);
+ }
+
+ return null;
+ }
}
diff --git a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
index 4681a8b..644e17e 100644
--- a/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
+++ b/car-lib/src/android/car/cluster/renderer/NavigationRenderer.java
@@ -18,7 +18,6 @@
import android.annotation.SystemApi;
import android.annotation.UiThread;
import android.car.navigation.CarNavigationInstrumentCluster;
-import android.graphics.Bitmap;
import android.os.Bundle;
/**
@@ -32,10 +31,10 @@
/**
* Returns properties of instrument cluster for navigation.
*/
- abstract public CarNavigationInstrumentCluster getNavigationProperties();
+ public abstract CarNavigationInstrumentCluster getNavigationProperties();
/**
* Called when an event is fired to change the navigation state.
*/
- abstract public void onEvent(int eventType, Bundle bundle);
+ public abstract void onEvent(int eventType, Bundle bundle);
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index 84d67b9..e3ee05a 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -27,12 +27,9 @@
import android.content.Intent;
import android.graphics.Rect;
import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Binder;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
@@ -47,37 +44,41 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.function.Consumer;
/**
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
* virtual display that is transmitted to an external screen.
*/
-public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService {
+public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService implements
+ ImageResolver.BitmapFetcher {
private static final String TAG = "Cluster.SampleService";
private static final int NO_DISPLAY = -1;
+ static final int NAV_STATE_EVENT_ID = 1;
static final String LOCAL_BINDING_ACTION = "local";
static final String NAV_STATE_BUNDLE_KEY = "navstate";
- static final int NAV_STATE_EVENT_ID = 1;
- static final int MSG_SET_ACTIVITY_LAUNCH_OPTIONS = 1;
- static final int MSG_ON_NAVIGATION_STATE_CHANGED = 2;
- static final int MSG_ON_KEY_EVENT = 3;
- static final int MSG_REGISTER_CLIENT = 4;
- static final int MSG_UNREGISTER_CLIENT = 5;
- static final String MSG_KEY_CATEGORY = "category";
- static final String MSG_KEY_ACTIVITY_DISPLAY_ID = "activity_display_id";
- static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
- static final String MSG_KEY_KEY_EVENT = "key_event";
- private List<Messenger> mClients = new ArrayList<>();
+ private List<ServiceClient> mClients = new ArrayList<>();
private ClusterDisplayProvider mDisplayProvider;
private int mDisplayId = NO_DISPLAY;
- private final IBinder mLocalBinder = new Messenger(new MessageHandler(this)).getBinder();
+ private final IBinder mLocalBinder = new LocalBinder();
+ private final ImageResolver mImageResolver = new ImageResolver(this);
+
+ public interface ServiceClient {
+ void onKeyEvent(KeyEvent keyEvent);
+ void onNavigationStateChange(NavigationState navState);
+ }
+
+ public class LocalBinder extends Binder {
+ ClusterRenderingServiceImpl getService() {
+ return ClusterRenderingServiceImpl.this;
+ }
+ }
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
@@ -98,43 +99,33 @@
}
};
- private static class MessageHandler extends Handler {
- private final WeakReference<ClusterRenderingServiceImpl> mService;
-
- MessageHandler(ClusterRenderingServiceImpl service) {
- mService = new WeakReference<>(service);
+ public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
+ ActivityOptions options = displayId != Display.INVALID_DISPLAY
+ ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
+ : null;
+ setClusterActivityLaunchOptions(options);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
+ options, options.getLaunchDisplayId()));
}
-
- @Override
- public void handleMessage(Message msg) {
- Log.d(TAG, "handleMessage: " + msg.what);
- switch (msg.what) {
- case MSG_SET_ACTIVITY_LAUNCH_OPTIONS: {
- int displayId = msg.getData().getInt(MSG_KEY_ACTIVITY_DISPLAY_ID);
- Bundle state = msg.getData().getBundle(MSG_KEY_ACTIVITY_STATE);
- String category = msg.getData().getString(MSG_KEY_CATEGORY);
- ActivityOptions options = displayId != Display.INVALID_DISPLAY
- ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
- : null;
- mService.get().setClusterActivityLaunchOptions(category, options);
- Log.d(TAG, String.format("activity options set: %s = %s (displayeId: %d)",
- category, options, options.getLaunchDisplayId()));
- mService.get().setClusterActivityState(category, state);
- Log.d(TAG, String.format("activity state set: %s = %s", category, state));
- break;
- }
- case MSG_REGISTER_CLIENT:
- mService.get().mClients.add(msg.replyTo);
- break;
- case MSG_UNREGISTER_CLIENT:
- mService.get().mClients.remove(msg.replyTo);
- break;
- default:
- super.handleMessage(msg);
- }
+ setClusterActivityState(state);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("activity state set: %s", state));
}
}
+ public void registerClient(ServiceClient client) {
+ mClients.add(client);
+ }
+
+ public void unregisterClient(ServiceClient client) {
+ mClients.remove(client);
+ }
+
+ public ImageResolver getImageResolver() {
+ return mImageResolver;
+ }
+
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind, intent: " + intent);
@@ -161,30 +152,20 @@
@Override
public void onKeyEvent(KeyEvent keyEvent) {
- Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
- Bundle data = new Bundle();
- data.putParcelable(MSG_KEY_KEY_EVENT, keyEvent);
- broadcastClientMessage(MSG_ON_KEY_EVENT, data);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
+ }
+ broadcastClientEvent(client -> client.onKeyEvent(keyEvent));
}
/**
- * Broadcasts a message to all the registered service clients
+ * Broadcasts an event to all the registered service clients
*
- * @param what event identifier
- * @param data event data
+ * @param event event to broadcast
*/
- private void broadcastClientMessage(int what, Bundle data) {
- Log.d(TAG, "broadcast message " + what + " to " + mClients.size() + " clients");
- for (int i = mClients.size() - 1; i >= 0; i--) {
- Messenger client = mClients.get(i);
- try {
- Message msg = Message.obtain(null, what);
- msg.setData(data);
- client.send(msg);
- } catch (RemoteException ex) {
- Log.e(TAG, "Client " + i + " is dead", ex);
- mClients.remove(i);
- }
+ private void broadcastClientEvent(Consumer<ServiceClient> event) {
+ for (ServiceClient client : mClients) {
+ event.accept(client);
}
}
@@ -210,7 +191,7 @@
bundleSummary.append(navState.toString());
// Update clients
- broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
+ broadcastClientEvent(client -> client.onNavigationStateChange(navState));
} else {
for (String key : bundle.keySet()) {
bundleSummary.append(key);
@@ -222,9 +203,8 @@
Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
} catch (Exception e) {
Log.e(TAG, "Error parsing event data (" + eventType + ", " + bundle + ")", e);
- bundle.putParcelable(NAV_STATE_BUNDLE_KEY, new NavigationState.Builder().build()
- .toParcelable());
- broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
+ NavigationState navState = new NavigationState.Builder().build();
+ broadcastClientEvent(client -> client.onNavigationStateChange(navState));
}
}
};
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
index e0d0d12..ae86850 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/CueView.java
@@ -18,19 +18,34 @@
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
+import android.os.Handler;
import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import android.util.AttributeSet;
+import android.util.Log;
import android.widget.TextView;
+import androidx.car.cluster.navigation.ImageReference;
import androidx.car.cluster.navigation.RichText;
import androidx.car.cluster.navigation.RichTextElement;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
/**
* View component that displays the Cue information on the instrument cluster display
*/
public class CueView extends TextView {
+ private static final String TAG = "Cluster.CueView";
+
private String mImageSpanText;
+ private CompletableFuture<?> mFuture;
+ private Handler mHandler = new Handler();
+ private RichText mContent;
public CueView(Context context) {
super(context);
@@ -45,20 +60,45 @@
mImageSpanText = context.getString(R.string.span_image);
}
- public void setRichText(RichText richText) {
+ public void setRichText(RichText richText, ImageResolver imageResolver) {
if (richText == null) {
setText(null);
return;
}
+ if (mFuture != null && !Objects.equals(richText, mContent)) {
+ mFuture.cancel(true);
+ }
+
+ List<ImageReference> imageReferences = richText.getElements().stream()
+ .filter(element -> element.getImage() != null)
+ .map(element -> element.getImage())
+ .collect(Collectors.toList());
+ mFuture = imageResolver
+ .getBitmaps(imageReferences, 0, getLineHeight())
+ .thenAccept(bitmaps -> {
+ mHandler.post(() -> update(richText, bitmaps));
+ mFuture = null;
+ })
+ .exceptionally(ex -> {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unable to fetch images for cue: " + richText);
+ }
+ mHandler.post(() -> update(richText, Collections.emptyMap()));
+ return null;
+ });
+ mContent = richText;
+ }
+
+ private void update(RichText richText, Map<ImageReference, Bitmap> bitmaps) {
SpannableStringBuilder builder = new SpannableStringBuilder();
+
for (RichTextElement element : richText.getElements()) {
if (builder.length() > 0) {
builder.append(" ");
}
if (element.getImage() != null) {
- Bitmap bitmap = ImageResolver.getInstance().getBitmapConstrained(getContext(),
- element.getImage(), 0, getLineHeight());
+ Bitmap bitmap = bitmaps.get(element.getImage());
if (bitmap != null) {
String imageText = element.getText().isEmpty() ? mImageSpanText :
element.getText();
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
index f306143..5e03b9b 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ImageResolver.java
@@ -15,95 +15,126 @@
*/
package android.car.cluster.sample;
-import android.content.ContentResolver;
-import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.net.Uri;
-import android.os.ParcelFileDescriptor;
import android.util.Log;
+import android.util.LruCache;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.cluster.navigation.ImageReference;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
/**
* Class for retrieving bitmap images from a ContentProvider
*/
public class ImageResolver {
private static final String TAG = "Cluster.ImageResolver";
+ private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
- private static ImageResolver sImageResolver = new ImageResolver();
+ private final BitmapFetcher mFetcher;
+ private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
+ IMAGE_CACHE_SIZE_BYTES) {
+ @Override
+ protected int sizeOf(String key, Bitmap value) {
+ return value.getByteCount();
+ }
+ };
- private ImageResolver() {}
-
- public static ImageResolver getInstance() {
- return sImageResolver;
+ public interface BitmapFetcher {
+ Bitmap getBitmap(Uri uri);
}
/**
- * Returns a bitmap from an URI string from a content provider
- *
- * @param context View context
+ * Creates a resolver that delegate the image retrieval to the given fetcher.
*/
- @Nullable
- public Bitmap getBitmap(Context context, Uri uri) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Requesting: " + uri);
- }
- try {
- ContentResolver contentResolver = context.getContentResolver();
- ParcelFileDescriptor fileDesc = contentResolver.openFileDescriptor(uri, "r");
- if (fileDesc != null) {
- Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
- fileDesc.close();
- return bitmap;
- } else {
- Log.e(TAG, "Null pointer: Failed to create pipe for uri string: " + uri);
- }
- } catch (FileNotFoundException e) {
- Log.e(TAG, "File not found for uri string: " + uri, e);
- } catch (IOException e) {
- Log.e(TAG, "File descriptor could not close: ", e);
- }
-
- return null;
+ public ImageResolver(BitmapFetcher fetcher) {
+ mFetcher = fetcher;
}
/**
- * Returns a bitmap from a Car Instrument Cluster {@link ImageReference} that would fit inside
- * the provided size. Either width, height or both should be greater than 0.
+ * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}.
+ * This image would fit inside the provided size. Either width, height or both should be greater
+ * than 0.
*
- * @param context View context
* @param width required width, or 0 if width is flexible based on height.
* @param height required height, or 0 if height is flexible based on width.
*/
- @Nullable
- public Bitmap getBitmapConstrained(Context context, ImageReference img, int width,
- int height) {
+ @NonNull
+ public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("Requesting image %s (width: %d, height: %d)",
img.getRawContentUri(), width, height));
}
- // Adjust the size to fit in the requested box.
- Point adjusted = getAdjustedSize(img.getOriginalWidth(), img.getOriginalHeight(), width,
- height);
- if (adjusted == null) {
- Log.e(TAG, "The provided image has no original size: " + img.getRawContentUri());
- return null;
- }
- Bitmap bitmap = getBitmap(context, img.getContentUri(adjusted.x, adjusted.y));
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)",
- img.getRawContentUri(), width, height));
- }
- return bitmap != null ? Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true)
- : null;
+ return CompletableFuture.supplyAsync(() -> {
+ // Adjust the size to fit in the requested box.
+ Point adjusted = getAdjustedSize(img.getOriginalWidth(), img.getOriginalHeight(), width,
+ height);
+ if (adjusted == null) {
+ Log.e(TAG, "The provided image has no original size: " + img.getRawContentUri());
+ return null;
+ }
+ Uri uri = img.getContentUri(adjusted.x, adjusted.y);
+ Bitmap bitmap = mCache.get(uri.toString());
+ if (bitmap == null) {
+ bitmap = mFetcher.getBitmap(uri);
+ if (bitmap == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unable to fetch image: " + uri);
+ }
+ return null;
+ }
+ if (bitmap.getWidth() != adjusted.x || bitmap.getHeight() != adjusted.y) {
+ bitmap = Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true);
+ }
+ mCache.put(uri.toString(), bitmap);
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)",
+ img.getRawContentUri(), width, height));
+ }
+ return bitmap != null ? Bitmap.createScaledBitmap(bitmap, adjusted.x, adjusted.y, true)
+ : null;
+ });
+ }
+
+ /**
+ * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The
+ * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to
+ * its bitmap. If any image fails to be fetched, the whole future completes exceptionally.
+ *
+ * @param width required width, or 0 if width is flexible based on height.
+ * @param height required height, or 0 if height is flexible based on width.
+ */
+ @NonNull
+ public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps(
+ @NonNull List<ImageReference> imgs, int width, int height) {
+ CompletableFuture<Map<ImageReference, Bitmap>> future = new CompletableFuture<>();
+
+ Map<ImageReference, CompletableFuture<Bitmap>> bitmapFutures = imgs.stream().collect(
+ Collectors.toMap(
+ img -> img,
+ img -> getBitmap(img, width, height)));
+
+ CompletableFuture.allOf(bitmapFutures.values().toArray(new CompletableFuture[0]))
+ .thenAccept(v -> {
+ Map<ImageReference, Bitmap> bitmaps = bitmapFutures.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry
+ .getValue().join()));
+ future.complete(bitmaps);
+ })
+ .exceptionally(ex -> {
+ future.completeExceptionally(ex);
+ return null;
+ });
+
+ return future;
}
/**
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 3181b1e..13a1399 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -16,17 +16,6 @@
package android.car.cluster.sample;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.LOCAL_BINDING_ACTION;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
- .MSG_ON_NAVIGATION_STATE_CHANGED;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_REGISTER_CLIENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
- .MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
import static android.content.Intent.ACTION_USER_SWITCHED;
import static android.content.Intent.ACTION_USER_UNLOCKED;
@@ -48,9 +37,6 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
@@ -69,7 +55,6 @@
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
-import androidx.versionedparcelable.ParcelUtils;
import androidx.viewpager.widget.ViewPager;
import java.lang.ref.WeakReference;
@@ -95,7 +80,8 @@
* This is necessary because the navigation app runs under a normal user, and different users will
* see different instances of the same application, with their own personalized data.
*/
-public class MainClusterActivity extends FragmentActivity {
+public class MainClusterActivity extends FragmentActivity implements
+ ClusterRenderingServiceImpl.ServiceClient {
private static final String TAG = "Cluster.MainActivity";
private static final NavigationState NULL_NAV_STATE = new NavigationState.Builder().build();
@@ -110,8 +96,7 @@
private Map<Sensors.Gear, View> mGearsToIcon = new HashMap<>();
private InputMethodManager mInputMethodManager;
- private Messenger mService;
- private Messenger mServiceCallbacks = new Messenger(new MessageHandler(this));
+ private ClusterRenderingServiceImpl mService;
private VirtualDisplay mPendingVirtualDisplay = null;
private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
@@ -142,7 +127,7 @@
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
- mPager.setCurrentItem(mButtonToFacet.get(v).order);
+ mPager.setCurrentItem(mButtonToFacet.get(v).mOrder);
}
}
};
@@ -151,8 +136,9 @@
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
- mService = new Messenger(service);
- sendServiceMessage(MSG_REGISTER_CLIENT, null, mServiceCallbacks);
+ mService = ((ClusterRenderingServiceImpl.LocalBinder) service).getService();
+ mService.registerClient(MainClusterActivity.this);
+ mNavStateController.setImageResolver(mService.getImageResolver());
if (mPendingVirtualDisplay != null) {
// If haven't reported the virtual display yet, do so on service connect.
reportNavDisplay(mPendingVirtualDisplay);
@@ -164,44 +150,11 @@
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "onServiceDisconnected, name: " + name);
mService = null;
+ mNavStateController.setImageResolver(null);
onNavigationStateChange(NULL_NAV_STATE);
}
};
- private static class MessageHandler extends Handler {
- private final WeakReference<MainClusterActivity> mActivity;
-
- MessageHandler(MainClusterActivity activity) {
- mActivity = new WeakReference<>(activity);
- }
-
- @Override
- public void handleMessage(Message msg) {
- Bundle data = msg.getData();
- switch (msg.what) {
- case MSG_ON_KEY_EVENT:
- KeyEvent event = data.getParcelable(MSG_KEY_KEY_EVENT);
- if (event != null) {
- mActivity.get().onKeyEvent(event);
- }
- break;
- case MSG_ON_NAVIGATION_STATE_CHANGED:
- if (data == null) {
- mActivity.get().onNavigationStateChange(null);
- } else {
- data.setClassLoader(ParcelUtils.class.getClassLoader());
- NavigationState navState = NavigationState
- .fromParcelable(data.getParcelable(
- ClusterRenderingServiceImpl.NAV_STATE_BUNDLE_KEY));
- mActivity.get().onNavigationStateChange(navState);
- }
- break;
- default:
- super.handleMessage(msg);
- }
- }
- }
-
private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
if (displayId != mNavigationDisplayId) {
return;
@@ -259,7 +212,7 @@
mPager = findViewById(R.id.pager);
mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
- mOrderToFacet.get(0).button.requestFocus();
+ mOrderToFacet.get(0).mButton.requestFocus();
mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
@@ -300,13 +253,14 @@
mUserReceiver.unregister(this);
mActivityMonitor.stop();
if (mService != null) {
- sendServiceMessage(MSG_UNREGISTER_CLIENT, null, mServiceCallbacks);
+ mService.unregisterClient(this);
mService = null;
}
unbindService(mClusterRenderingServiceConnection);
}
- private void onKeyEvent(KeyEvent event) {
+ @Override
+ public void onKeyEvent(KeyEvent event) {
Log.i(TAG, "onKeyEvent, event: " + event);
// This is a hack. We use SOURCE_CLASS_POINTER here because this type of input is associated
@@ -316,7 +270,8 @@
mInputMethodManager.dispatchKeyEventFromInputMethod(getCurrentFocus(), event);
}
- private void onNavigationStateChange(NavigationState state) {
+ @Override
+ public void onNavigationStateChange(NavigationState state) {
Log.d(TAG, "onNavigationStateChange: " + state);
if (mNavStateController != null) {
mNavStateController.update(state);
@@ -338,32 +293,9 @@
}
private void reportNavDisplay(VirtualDisplay virtualDisplay) {
- Bundle data = new Bundle();
- data.putString(MSG_KEY_CATEGORY, Car.CAR_CATEGORY_NAVIGATION);
- data.putInt(MSG_KEY_ACTIVITY_DISPLAY_ID, virtualDisplay.mDisplayId);
- data.putBundle(MSG_KEY_ACTIVITY_STATE, ClusterActivityState
+ mService.setActivityLaunchOptions(virtualDisplay.mDisplayId, ClusterActivityState
.create(virtualDisplay.mDisplayId != Display.INVALID_DISPLAY,
- virtualDisplay.mUnobscuredBounds)
- .toBundle());
- sendServiceMessage(MSG_SET_ACTIVITY_LAUNCH_OPTIONS, data, null);
- }
-
- /**
- * Sends a message to the {@link ClusterRenderingServiceImpl}, which runs on a different
- * process.
- * @param what action to perform
- * @param data action data
- * @param replyTo {@link Messenger} where to reply back
- */
- private void sendServiceMessage(int what, Bundle data, Messenger replyTo) {
- try {
- Message message = Message.obtain(null, what);
- message.setData(data);
- message.replyTo = replyTo;
- mService.send(message);
- } catch (RemoteException ex) {
- Log.e(TAG, "Unable to deliver message " + what + ". Service died");
- }
+ virtualDisplay.mUnobscuredBounds));
}
public class ClusterPageAdapter extends FragmentPagerAdapter {
@@ -383,21 +315,21 @@
}
private <T> void registerFacet(Facet<T> facet) {
- mOrderToFacet.append(facet.order, facet);
- mButtonToFacet.put(facet.button, facet);
+ mOrderToFacet.append(facet.mOrder, facet);
+ mButtonToFacet.put(facet.mButton, facet);
- facet.button.setOnFocusChangeListener(mFacetButtonFocusListener);
+ facet.mButton.setOnFocusChangeListener(mFacetButtonFocusListener);
}
private static class Facet<T> {
- Button button;
- Class<T> clazz;
- int order;
+ Button mButton;
+ Class<T> mClazz;
+ int mOrder;
Facet(Button button, int order, Class<T> clazz) {
- this.button = button;
- this.order = order;
- this.clazz = clazz;
+ this.mButton = button;
+ this.mOrder = order;
+ this.mClazz = clazz;
}
private Fragment mFragment;
@@ -405,8 +337,9 @@
Fragment getOrCreateFragment() {
if (mFragment == null) {
try {
- mFragment = (Fragment) clazz.getConstructors()[0].newInstance();
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ mFragment = (Fragment) mClazz.getConstructors()[0].newInstance();
+ } catch (InstantiationException | IllegalAccessException
+ | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index 0d07962..3ba31d7 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -46,6 +46,7 @@
private TextView mEta;
private CueView mCue;
private Context mContext;
+ private ImageResolver mImageResolver;
/**
* Creates a controller to coordinate updates to the views displaying navigation state
@@ -63,6 +64,10 @@
mContext = container.getContext();
}
+ public void setImageResolver(@Nullable ImageResolver imageResolver) {
+ mImageResolver = imageResolver;
+ }
+
/**
* Updates views to reflect the provided navigation state
*/
@@ -80,7 +85,7 @@
mEta.setTextColor(getTrafficColor(traffic));
mManeuver.setImageDrawable(getManeuverIcon(step != null ? step.getManeuver() : null));
mDistance.setText(formatDistance(step != null ? step.getDistance() : null));
- mCue.setRichText(step != null ? step.getCue() : null);
+ mCue.setRichText(step != null ? step.getCue() : null, mImageResolver);
if (step != null && step.getLanes().size() > 0) {
mLane.setLanes(step.getLanes());
diff --git a/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java b/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
index 746c0df..0d1cece 100644
--- a/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
+++ b/tests/DirectRenderingClusterSample/tests/robotests/src/android/car/cluster/sample/ImageResolverTest.java
@@ -32,7 +32,7 @@
@Before
public void setup() {
- mImageResolver = ImageResolver.getInstance();
+ mImageResolver = new ImageResolver((uri) -> null);
}
@Test