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