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