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