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 * <service android:name=".MyInstrumentClusterService" 79 * android:permission="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"> 80 * </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