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