• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.renderer;
17 
18 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE;
20 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
21 
22 import android.annotation.CallSuper;
23 import android.annotation.MainThread;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.SystemApi;
27 import android.annotation.UserIdInt;
28 import android.app.ActivityManager;
29 import android.app.ActivityOptions;
30 import android.app.Service;
31 import android.car.Car;
32 import android.car.CarLibLog;
33 import android.car.annotation.AddedInOrBefore;
34 import android.car.builtin.util.Slogf;
35 import android.car.cluster.ClusterActivityState;
36 import android.car.navigation.CarNavigationInstrumentCluster;
37 import android.content.ActivityNotFoundException;
38 import android.content.ComponentName;
39 import android.content.Intent;
40 import android.content.pm.ActivityInfo;
41 import android.content.pm.PackageManager;
42 import android.content.pm.ProviderInfo;
43 import android.content.pm.ResolveInfo;
44 import android.graphics.Bitmap;
45 import android.graphics.BitmapFactory;
46 import android.net.Uri;
47 import android.os.Bundle;
48 import android.os.Handler;
49 import android.os.IBinder;
50 import android.os.Looper;
51 import android.os.ParcelFileDescriptor;
52 import android.os.RemoteException;
53 import android.os.UserHandle;
54 import android.util.Log;
55 import android.util.LruCache;
56 import android.view.KeyEvent;
57 
58 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
59 import com.android.internal.annotations.GuardedBy;
60 
61 import java.io.FileDescriptor;
62 import java.io.IOException;
63 import java.io.PrintWriter;
64 import java.util.Arrays;
65 import java.util.Collection;
66 import java.util.Collections;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Objects;
70 import java.util.Set;
71 import java.util.concurrent.CountDownLatch;
72 import java.util.concurrent.atomic.AtomicReference;
73 import java.util.function.Supplier;
74 import java.util.stream.Collectors;
75 
76 /**
77  * A service used for interaction between Car Service and Instrument Cluster. Car Service may
78  * provide internal navigation binder interface to Navigation App and all notifications will be
79  * eventually land in the {@link NavigationRenderer} returned by {@link #getNavigationRenderer()}.
80  *
81  * <p>To extend this class, you must declare the service in your manifest file with
82  * the {@code android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE} permission
83  * <pre>
84  * &lt;service android:name=".MyInstrumentClusterService"
85  *          android:permission="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE">
86  * &lt;/service></pre>
87  * <p>Also, you will need to register this service in the following configuration file:
88  * {@code packages/services/Car/service/res/values/config.xml}
89  *
90  * @hide
91  */
92 @SystemApi
93 public abstract class InstrumentClusterRenderingService extends Service {
94     /**
95      * Key to pass IInstrumentClusterHelper binder in onBind call {@link Intent} through extra
96      * {@link Bundle). Both extra bundle and binder itself use this key.
97      *
98      * @hide
99      */
100     @AddedInOrBefore(majorVersion = 33)
101     public static final String EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER =
102             "android.car.cluster.renderer.IInstrumentClusterHelper";
103 
104     private static final String TAG = CarLibLog.TAG_CLUSTER;
105     private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
106 
107     private static final String BITMAP_QUERY_WIDTH = "w";
108     private static final String BITMAP_QUERY_HEIGHT = "h";
109     private static final String BITMAP_QUERY_OFFLANESALPHA = "offLanesAlpha";
110 
111     private final Handler mUiHandler = new Handler(Looper.getMainLooper());
112 
113     private final Object mLock = new Object();
114     // Main thread only
115     private RendererBinder mRendererBinder;
116     private ActivityOptions mActivityOptions;
117     private ClusterActivityState mActivityState;
118     private ComponentName mNavigationComponent;
119     @GuardedBy("mLock")
120     private ContextOwner mNavContextOwner;
121 
122     @GuardedBy("mLock")
123     private IInstrumentClusterHelper mInstrumentClusterHelper;
124 
125     private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
126     private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
127             IMAGE_CACHE_SIZE_BYTES) {
128         @Override
129         protected int sizeOf(String key, Bitmap value) {
130             return value.getByteCount();
131         }
132     };
133 
134     private static class ContextOwner {
135         final int mUid;
136         final int mPid;
137         final Set<String> mPackageNames;
138         final Set<String> mAuthorities;
139 
ContextOwner(int uid, int pid, PackageManager packageManager)140         ContextOwner(int uid, int pid, PackageManager packageManager) {
141             mUid = uid;
142             mPid = pid;
143             String[] packageNames = uid != 0 ? packageManager.getPackagesForUid(uid)
144                     : null;
145             mPackageNames = packageNames != null
146                     ? Collections.unmodifiableSet(new HashSet<>(Arrays.asList(packageNames)))
147                     : Collections.emptySet();
148             mAuthorities = Collections.unmodifiableSet(mPackageNames.stream()
149                     .map(packageName -> getAuthoritiesForPackage(packageManager, packageName))
150                     .flatMap(Collection::stream)
151                     .collect(Collectors.toSet()));
152         }
153 
154         @Override
155         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
toString()156         public String toString() {
157             return "{uid: " + mUid + ", pid: " + mPid + ", packagenames: " + mPackageNames
158                     + ", authorities: " + mAuthorities + "}";
159         }
160 
getAuthoritiesForPackage(PackageManager packageManager, String packageName)161         private List<String> getAuthoritiesForPackage(PackageManager packageManager,
162                 String packageName) {
163             try {
164                 ProviderInfo[] providers = packageManager.getPackageInfo(packageName,
165                         PackageManager.GET_PROVIDERS | PackageManager.MATCH_ANY_USER).providers;
166                 if (providers == null) {
167                     return Collections.emptyList();
168                 }
169                 return Arrays.stream(providers)
170                         .map(provider -> provider.authority)
171                         .collect(Collectors.toList());
172             } catch (PackageManager.NameNotFoundException e) {
173                 Slogf.w(TAG, "Package name not found while retrieving content provider "
174                         + "authorities: %s" , packageName);
175                 return Collections.emptyList();
176             }
177         }
178     }
179 
180     @Override
181     @CallSuper
182     @AddedInOrBefore(majorVersion = 33)
onBind(Intent intent)183     public IBinder onBind(Intent intent) {
184         if (DBG) {
185             Slogf.d(TAG, "onBind, intent: %s", intent);
186         }
187 
188         Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
189         IBinder binder = null;
190         if (bundle != null) {
191             binder = bundle.getBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
192         }
193         if (binder == null) {
194             Slogf.wtf(TAG, "IInstrumentClusterHelper not passed through binder");
195         } else {
196             synchronized (mLock) {
197                 mInstrumentClusterHelper = IInstrumentClusterHelper.Stub.asInterface(binder);
198             }
199         }
200         if (mRendererBinder == null) {
201             mRendererBinder = new RendererBinder(getNavigationRenderer());
202         }
203 
204         return mRendererBinder;
205     }
206 
207     /**
208      * Returns {@link NavigationRenderer} or null if it's not supported. This renderer will be
209      * shared with the navigation context owner (application holding navigation focus).
210      */
211     @MainThread
212     @Nullable
213     @AddedInOrBefore(majorVersion = 33)
getNavigationRenderer()214     public abstract NavigationRenderer getNavigationRenderer();
215 
216     /**
217      * Called when key event that was addressed to instrument cluster display has been received.
218      */
219     @MainThread
220     @AddedInOrBefore(majorVersion = 33)
onKeyEvent(@onNull KeyEvent keyEvent)221     public void onKeyEvent(@NonNull KeyEvent keyEvent) {
222     }
223 
224     /**
225      * Called when a navigation application becomes a context owner (receives navigation focus) and
226      * its {@link Car#CATEGORY_NAVIGATION} activity is launched.
227      */
228     @MainThread
229     @AddedInOrBefore(majorVersion = 33)
onNavigationComponentLaunched()230     public void onNavigationComponentLaunched() {
231     }
232 
233     /**
234      * Called when the current context owner (application holding navigation focus) releases the
235      * focus and its {@link Car#CAR_CATEGORY_NAVIGATION} activity is ready to be replaced by a
236      * system default.
237      */
238     @MainThread
239     @AddedInOrBefore(majorVersion = 33)
onNavigationComponentReleased()240     public void onNavigationComponentReleased() {
241     }
242 
243     @Nullable
getClusterHelper()244     private IInstrumentClusterHelper getClusterHelper() {
245         synchronized (mLock) {
246             if (mInstrumentClusterHelper == null) {
247                 Slogf.w("mInstrumentClusterHelper still null, should wait until onBind",
248                         new RuntimeException());
249             }
250             return mInstrumentClusterHelper;
251         }
252     }
253 
254     /**
255      * Start Activity in fixed mode.
256      *
257      * <p>Activity launched in this way will stay visible across crash, package updatge
258      * or other Activity launch. So this should be carefully used for case like apps running
259      * in instrument cluster.</p>
260      *
261      * <p> Only one Activity can stay in this mode for a display and launching other Activity
262      * with this call means old one get out of the mode. Alternatively
263      * {@link #stopFixedActivityMode(int)} can be called to get the top activitgy out of this
264      * mode.</p>
265      *
266      * @param intentParam Should include specific {@code ComponentName}.
267      * @param options Should include target display.
268      * @param userId Target user id
269      * @return {@code true} if succeeded. {@code false} may mean the target component is not ready
270      *         or available. Note that failure can happen during early boot-up stage even if the
271      *         target Activity is in normal state and client should retry when it fails. Once it is
272      *         successfully launched, car service will guarantee that it is running across crash or
273      *         other events.
274      */
275     @AddedInOrBefore(majorVersion = 33)
startFixedActivityModeForDisplayAndUser(@onNull Intent intentParam, @NonNull ActivityOptions options, @UserIdInt int userId)276     public boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intentParam,
277             @NonNull ActivityOptions options, @UserIdInt int userId) {
278         Intent intent = intentParam;
279 
280         IInstrumentClusterHelper helper = getClusterHelper();
281         if (helper == null) {
282             return false;
283         }
284         if (mActivityState != null
285                 && intent.getBundleExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE) == null) {
286             intent = new Intent(intent).putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
287                     mActivityState.toBundle());
288         }
289         try {
290             return helper.startFixedActivityModeForDisplayAndUser(intent, options.toBundle(),
291                     userId);
292         } catch (RemoteException e) {
293             Slogf.w("Remote exception from car service", e);
294             // Probably car service will restart and rebind. So do nothing.
295         }
296         return false;
297     }
298 
299 
300     /**
301      * Stop fixed mode for top Activity in the display. Crashing or launching other Activity
302      * will not re-launch the top Activity any more.
303      */
304     @AddedInOrBefore(majorVersion = 33)
stopFixedActivityMode(int displayId)305     public void stopFixedActivityMode(int displayId) {
306         IInstrumentClusterHelper helper = getClusterHelper();
307         if (helper == null) {
308             return;
309         }
310         try {
311             helper.stopFixedActivityMode(displayId);
312         } catch (RemoteException e) {
313             Slogf.w("Remote exception from car service, displayId:" + displayId, e);
314             // Probably car service will restart and rebind. So do nothing.
315         }
316     }
317 
318     /**
319      * Updates the cluster navigation activity by checking which activity to show (an activity of
320      * the {@link #mNavContextOwner}). If not yet launched, it will do so.
321      */
updateNavigationActivity()322     private void updateNavigationActivity() {
323         ContextOwner contextOwner = getNavigationContextOwner();
324 
325         if (DBG) {
326             Slogf.d(TAG, "updateNavigationActivity (mActivityOptions: %s, "
327                             + "mActivityState: %s, mNavContextOwnerUid: %s)", mActivityOptions,
328                     mActivityState, contextOwner);
329         }
330 
331         if (contextOwner == null || contextOwner.mUid == 0 || mActivityOptions == null
332                 || mActivityState == null || !mActivityState.isVisible()) {
333             // We are not yet ready to display an activity on the cluster
334             if (mNavigationComponent != null) {
335                 mNavigationComponent = null;
336                 onNavigationComponentReleased();
337             }
338             return;
339         }
340 
341         ComponentName component = getNavigationComponentByOwner(contextOwner);
342         if (Objects.equals(mNavigationComponent, component)) {
343             // We have already launched this component.
344             if (DBG) {
345                 Slogf.d(TAG, "Already launched component: %s", component);
346             }
347             return;
348         }
349 
350         if (component == null) {
351             if (DBG) {
352                 Slogf.d(TAG, "No component found for owner: %s", contextOwner);
353             }
354             return;
355         }
356 
357         if (!startNavigationActivity(component)) {
358             if (DBG) {
359                 Slogf.d(TAG, "Unable to launch component: %s", component);
360             }
361             return;
362         }
363 
364         mNavigationComponent = component;
365         onNavigationComponentLaunched();
366     }
367 
368     /**
369      * Returns a component with category {@link Car#CAR_CATEGORY_NAVIGATION} from the same package
370      * as the given navigation context owner.
371      */
372     @Nullable
getNavigationComponentByOwner(ContextOwner contextOwner)373     private ComponentName getNavigationComponentByOwner(ContextOwner contextOwner) {
374         for (String packageName : contextOwner.mPackageNames) {
375             ComponentName component = getComponentFromPackage(packageName);
376             if (component != null) {
377                 if (DBG) {
378                     Slogf.d(TAG, "Found component: %s", component);
379                 }
380                 return component;
381             }
382         }
383         return null;
384     }
385 
getNavigationContextOwner()386     private ContextOwner getNavigationContextOwner() {
387         synchronized (mLock) {
388             return mNavContextOwner;
389         }
390     }
391 
392     /**
393      * Returns the cluster activity from the application given by its package name.
394      *
395      * @return the {@link ComponentName} of the cluster activity, or null if the given application
396      * doesn't have a cluster activity.
397      *
398      * @hide
399      */
400     @Nullable
401     @AddedInOrBefore(majorVersion = 33)
getComponentFromPackage(@onNull String packageName)402     public ComponentName getComponentFromPackage(@NonNull String packageName) {
403         PackageManager packageManager = getPackageManager();
404 
405         // Check package permission.
406         if (packageManager.checkPermission(Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER, packageName)
407                 != PackageManager.PERMISSION_GRANTED) {
408             Slogf.i(TAG, "Package '%s' doesn't have permission %s", packageName,
409                     Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER);
410             return null;
411         }
412 
413         Intent intent = new Intent(Intent.ACTION_MAIN)
414                 .addCategory(Car.CAR_CATEGORY_NAVIGATION)
415                 .setPackage(packageName);
416         List<ResolveInfo> resolveList = packageManager.queryIntentActivitiesAsUser(
417                 intent, PackageManager.GET_RESOLVED_FILTER,
418                 UserHandle.of(ActivityManager.getCurrentUser()));
419         if (resolveList == null || resolveList.isEmpty()
420                 || resolveList.get(0).activityInfo == null) {
421             Slogf.i(TAG, "Failed to resolve an intent: %s", intent);
422             return null;
423         }
424 
425         // In case of multiple matching activities in the same package, we pick the first one.
426         ActivityInfo info = resolveList.get(0).activityInfo;
427         return new ComponentName(info.packageName, info.name);
428     }
429 
430     /**
431      * Starts an activity on the cluster using the given component.
432      *
433      * @return false if the activity couldn't be started.
434      */
435     @AddedInOrBefore(majorVersion = 33)
startNavigationActivity(@onNull ComponentName component)436     protected boolean startNavigationActivity(@NonNull ComponentName component) {
437         // Create an explicit intent.
438         Intent intent = new Intent();
439         intent.setComponent(component);
440         intent.putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE, mActivityState.toBundle());
441         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
442         try {
443             startFixedActivityModeForDisplayAndUser(intent, mActivityOptions,
444                     ActivityManager.getCurrentUser());
445             Slogf.i(TAG, "Activity launched: %s (options: %s, displayId: %d)",
446                     mActivityOptions, intent, mActivityOptions.getLaunchDisplayId());
447         } catch (ActivityNotFoundException e) {
448             Slogf.w(TAG, "Unable to find activity for intent: " + intent);
449             return false;
450         } catch (RuntimeException e) {
451             // Catch all other possible exception to prevent service disruption by misbehaving
452             // applications.
453             Slogf.e(TAG, "Error trying to launch intent: " + intent + ". Ignored", e);
454             return false;
455         }
456         return true;
457     }
458 
459     /**
460      * @hide
461      * @deprecated Use {@link #setClusterActivityLaunchOptions(ActivityOptions)} instead.
462      */
463     @Deprecated
464     @ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE)
465     @AddedInOrBefore(majorVersion = 33)
setClusterActivityLaunchOptions(String category, ActivityOptions activityOptions)466     public void setClusterActivityLaunchOptions(String category, ActivityOptions activityOptions) {
467         setClusterActivityLaunchOptions(activityOptions);
468     }
469 
470     /**
471      * Sets configuration for activities that should be launched directly in the instrument
472      * cluster.
473      *
474      * @param activityOptions contains information of how to start cluster activity (on what display
475      *                        or activity stack).
476      */
477     @AddedInOrBefore(majorVersion = 33)
setClusterActivityLaunchOptions(@onNull ActivityOptions activityOptions)478     public void setClusterActivityLaunchOptions(@NonNull ActivityOptions activityOptions) {
479         mActivityOptions = activityOptions;
480         updateNavigationActivity();
481     }
482 
483     /**
484      * @hide
485      * @deprecated Use {@link #setClusterActivityState(ClusterActivityState)} instead.
486      */
487     @Deprecated
488     @ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE)
489     @AddedInOrBefore(majorVersion = 33)
setClusterActivityState(String category, Bundle state)490     public void setClusterActivityState(String category, Bundle state) {
491         setClusterActivityState(ClusterActivityState.fromBundle(state));
492     }
493 
494     /**
495      * Set activity state (such as unobscured bounds).
496      *
497      * @param state pass information about activity state, see
498      *              {@link android.car.cluster.ClusterActivityState}
499      */
500     @AddedInOrBefore(majorVersion = 33)
setClusterActivityState(@onNull ClusterActivityState state)501     public void setClusterActivityState(@NonNull ClusterActivityState state) {
502         mActivityState = state;
503         updateNavigationActivity();
504     }
505 
506     @CallSuper
507     @Override
508     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
509     @AddedInOrBefore(majorVersion = 33)
dump(FileDescriptor fd, PrintWriter writer, String[] args)510     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
511         synchronized (mLock) {
512             writer.println("**" + getClass().getSimpleName() + "**");
513             writer.println("renderer binder: " + mRendererBinder);
514             if (mRendererBinder != null) {
515                 writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
516             }
517             writer.println("navigation focus owner: " + getNavigationContextOwner());
518             writer.println("activity options: " + mActivityOptions);
519             writer.println("activity state: " + mActivityState);
520             writer.println("current nav component: " + mNavigationComponent);
521             writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
522             writer.println("mInstrumentClusterHelper" + mInstrumentClusterHelper);
523         }
524     }
525 
526     private class RendererBinder extends IInstrumentCluster.Stub {
527         private final NavigationRenderer mNavigationRenderer;
528 
RendererBinder(NavigationRenderer navigationRenderer)529         RendererBinder(NavigationRenderer navigationRenderer) {
530             mNavigationRenderer = navigationRenderer;
531         }
532 
533         @Override
getNavigationService()534         public IInstrumentClusterNavigation getNavigationService() throws RemoteException {
535             return new NavigationBinder(mNavigationRenderer);
536         }
537 
538         @Override
setNavigationContextOwner(int uid, int pid)539         public void setNavigationContextOwner(int uid, int pid) throws RemoteException {
540             if (DBG) {
541                 Slogf.d(TAG, "Updating navigation ownership to uid: %d, pid: %d", uid, pid);
542             }
543             synchronized (mLock) {
544                 mNavContextOwner = new ContextOwner(uid, pid, getPackageManager());
545             }
546             mUiHandler.post(InstrumentClusterRenderingService.this::updateNavigationActivity);
547         }
548 
549         @Override
onKeyEvent(KeyEvent keyEvent)550         public void onKeyEvent(KeyEvent keyEvent) throws RemoteException {
551             mUiHandler.post(() -> InstrumentClusterRenderingService.this.onKeyEvent(keyEvent));
552         }
553     }
554 
555     private class NavigationBinder extends IInstrumentClusterNavigation.Stub {
556         private final NavigationRenderer mNavigationRenderer;
557 
NavigationBinder(NavigationRenderer navigationRenderer)558         NavigationBinder(NavigationRenderer navigationRenderer) {
559             mNavigationRenderer = navigationRenderer;
560         }
561 
562         @Override
563         @SuppressWarnings("deprecation")
onNavigationStateChanged(@ullable Bundle bundle)564         public void onNavigationStateChanged(@Nullable Bundle bundle) throws RemoteException {
565             assertClusterManagerPermission();
566             mUiHandler.post(() -> {
567                 if (mNavigationRenderer != null) {
568                     mNavigationRenderer.onNavigationStateChanged(bundle);
569                 }
570             });
571         }
572 
573         @Override
getInstrumentClusterInfo()574         public CarNavigationInstrumentCluster getInstrumentClusterInfo() throws RemoteException {
575             assertClusterManagerPermission();
576             return runAndWaitResult(() -> mNavigationRenderer.getNavigationProperties());
577         }
578     }
579 
assertClusterManagerPermission()580     private void assertClusterManagerPermission() {
581         if (checkCallingOrSelfPermission(Car.PERMISSION_CAR_NAVIGATION_MANAGER)
582                 != PackageManager.PERMISSION_GRANTED) {
583             throw new SecurityException("requires " + Car.PERMISSION_CAR_NAVIGATION_MANAGER);
584         }
585     }
586 
runAndWaitResult(final Supplier<E> supplier)587     private <E> E runAndWaitResult(final Supplier<E> supplier) {
588         final CountDownLatch latch = new CountDownLatch(1);
589         final AtomicReference<E> result = new AtomicReference<>();
590 
591         mUiHandler.post(() -> {
592             result.set(supplier.get());
593             latch.countDown();
594         });
595 
596         try {
597             latch.await();
598         } catch (InterruptedException e) {
599             throw new RuntimeException(e);
600         }
601         return result.get();
602     }
603 
604     /**
605      * Fetches a bitmap from the navigation context owner (application holding navigation focus).
606      * It returns null if:
607      * <ul>
608      * <li>there is no navigation context owner
609      * <li>or if the {@link Uri} is invalid
610      * <li>or if it references a process other than the current navigation context owner
611      * </ul>
612      * This is a costly operation. Returned bitmaps should be cached and fetching should be done on
613      * a secondary thread.
614      *
615      * @param uri The URI of the bitmap
616      *
617      * @throws IllegalArgumentException if {@code uri} does not have width and height query params.
618      *
619      * @deprecated Replaced by {@link #getBitmap(Uri, int, int)}.
620      */
621     @Deprecated
622     @Nullable
623     @ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE)
624     @AddedInOrBefore(majorVersion = 33)
getBitmap(Uri uri)625     public Bitmap getBitmap(Uri uri) {
626         try {
627             if (uri.getQueryParameter(BITMAP_QUERY_WIDTH).isEmpty() || uri.getQueryParameter(
628                     BITMAP_QUERY_HEIGHT).isEmpty()) {
629                 throw new IllegalArgumentException(
630                     "Uri must have '" + BITMAP_QUERY_WIDTH + "' and '" + BITMAP_QUERY_HEIGHT
631                             + "' query parameters");
632             }
633 
634             ContextOwner contextOwner = getNavigationContextOwner();
635             if (contextOwner == null) {
636                 Slogf.e(TAG, "No context owner available while fetching: " + uri);
637                 return null;
638             }
639 
640             String host = uri.getHost();
641             if (!contextOwner.mAuthorities.contains(host)) {
642                 Slogf.e(TAG, "Uri points to an authority not handled by the current context owner: "
643                         + uri + " (valid authorities: " + contextOwner.mAuthorities + ")");
644                 return null;
645             }
646 
647             // Add user to URI to make the request to the right instance of content provider
648             // (see ContentProvider#getUserIdFromAuthority()).
649             int userId = UserHandle.getUserHandleForUid(contextOwner.mUid).getIdentifier();
650             Uri filteredUid = uri.buildUpon().encodedAuthority(userId + "@" + host).build();
651 
652             // Fetch the bitmap
653             if (DBG) {
654                 Slogf.d(TAG, "Requesting bitmap: %s", uri);
655             }
656             try (ParcelFileDescriptor fileDesc = getContentResolver()
657                     .openFileDescriptor(filteredUid, "r")) {
658                 if (fileDesc != null) {
659                     Bitmap bitmap = BitmapFactory.decodeFileDescriptor(
660                             fileDesc.getFileDescriptor());
661                     return bitmap;
662                 } else {
663                     Slogf.e(TAG, "Failed to create pipe for uri string: %s", uri);
664                 }
665             }
666         } catch (IOException e) {
667             Slogf.e(TAG, "Unable to fetch uri: " + uri, e);
668         }
669         return null;
670     }
671 
672     /**
673      * See {@link #getBitmap(Uri, int, int, float)}
674      */
675     @Nullable
676     @AddedInOrBefore(majorVersion = 33)
getBitmap(@onNull Uri uri, int width, int height)677     public Bitmap getBitmap(@NonNull Uri uri, int width, int height) {
678         return getBitmap(uri, width, height, 1f);
679     }
680 
681     /**
682      * Fetches a bitmap from the navigation context owner (application holding navigation focus)
683      * of the given width and height and off lane opacity. The fetched bitmaps are cached.
684      * It returns null if:
685      * <ul>
686      * <li>there is no navigation context owner
687      * <li>or if the {@link Uri} is invalid
688      * <li>or if it references a process other than the current navigation context owner
689      * </ul>
690      * This is a costly operation. Returned bitmaps should be fetched on a secondary thread.
691      *
692      * @param bitmapUri     The URI of the bitmap
693      * @param width         Requested width
694      * @param height        Requested height
695      * @param offLanesAlpha Opacity value of the off-lane images. Only used for lane guidance images
696      * @throws IllegalArgumentException if width, height <= 0, or 0 > offLanesAlpha > 1
697      */
698     @Nullable
699     @AddedInOrBefore(majorVersion = 33)
getBitmap(@onNull Uri bitmapUri, int width, int height, float offLanesAlpha)700     public Bitmap getBitmap(@NonNull Uri bitmapUri, int width, int height, float offLanesAlpha) {
701         Uri uri = bitmapUri;
702 
703         if (width <= 0 || height <= 0) {
704             throw new IllegalArgumentException("Width and height must be > 0");
705         }
706         if (offLanesAlpha < 0 || offLanesAlpha > 1) {
707             throw new IllegalArgumentException("offLanesAlpha must be between [0, 1]");
708         }
709 
710         try {
711             ContextOwner contextOwner = getNavigationContextOwner();
712             if (contextOwner == null) {
713                 Slogf.e(TAG, "No context owner available while fetching: %s", uri);
714                 return null;
715             }
716 
717             uri = uri.buildUpon()
718                     .appendQueryParameter(BITMAP_QUERY_WIDTH, String.valueOf(width))
719                     .appendQueryParameter(BITMAP_QUERY_HEIGHT, String.valueOf(height))
720                     .appendQueryParameter(BITMAP_QUERY_OFFLANESALPHA, String.valueOf(offLanesAlpha))
721                     .build();
722 
723             String host = uri.getHost();
724 
725             if (!contextOwner.mAuthorities.contains(host)) {
726                 Slogf.e(TAG, "Uri points to an authority not handled by the current context owner: "
727                         + "%s (valid authorities: %s)", uri, contextOwner.mAuthorities);
728                 return null;
729             }
730 
731             // Add user to URI to make the request to the right instance of content provider
732             // (see ContentProvider#getUserIdFromAuthority()).
733             int userId = UserHandle.getUserHandleForUid(contextOwner.mUid).getIdentifier();
734             Uri filteredUid = uri.buildUpon().encodedAuthority(userId + "@" + host).build();
735 
736             Bitmap bitmap = mCache.get(uri.toString());
737             if (bitmap == null) {
738                 // Fetch the bitmap
739                 if (DBG) {
740                     Slogf.d(TAG, "Requesting bitmap: %s", uri);
741                 }
742                 try (ParcelFileDescriptor fileDesc = getContentResolver()
743                         .openFileDescriptor(filteredUid, "r")) {
744                     if (fileDesc != null) {
745                         bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
746                     } else {
747                         Slogf.e(TAG, "Failed to create pipe for uri string: %s", uri);
748                     }
749                 }
750                 if (bitmap.getWidth() != width || bitmap.getHeight() != height) {
751                     bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
752                 }
753                 mCache.put(uri.toString(), bitmap);
754             }
755             return bitmap;
756         } catch (IOException e) {
757             Slogf.e(TAG, "Unable to fetch uri: " + uri, e);
758         }
759         return null;
760     }
761 }
762