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