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