1 /* 2 * Copyright (C) 2020 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 17 package com.android.providers.media; 18 19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; 20 import static android.provider.MediaStore.Files.FileColumns.TRANSCODE_COMPLETE; 21 import static android.provider.MediaStore.Files.FileColumns.TRANSCODE_EMPTY; 22 import static android.provider.MediaStore.MATCH_EXCLUDE; 23 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING; 24 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED; 25 26 import static com.android.providers.media.MediaProvider.VolumeNotFoundException; 27 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA; 28 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN; 29 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_CLIENT_TIMEOUT; 30 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SERVICE_ERROR; 31 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SESSION_CANCELED; 32 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__TRANSCODE_RESULT__FAIL; 33 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__TRANSCODE_RESULT__SUCCESS; 34 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED; 35 import static com.android.providers.media.util.SyntheticPathUtils.createSparseFile; 36 37 import android.annotation.IntRange; 38 import android.annotation.LongDef; 39 import android.app.ActivityManager; 40 import android.app.ActivityManager.OnUidImportanceListener; 41 import android.app.NotificationChannel; 42 import android.app.NotificationManager; 43 import android.app.compat.CompatChanges; 44 import android.compat.annotation.ChangeId; 45 import android.compat.annotation.Disabled; 46 import android.content.ContentResolver; 47 import android.content.ContentValues; 48 import android.content.Context; 49 import android.content.pm.ApplicationInfo; 50 import android.content.pm.InstallSourceInfo; 51 import android.content.pm.PackageManager; 52 import android.content.pm.PackageManager.Property; 53 import android.content.res.XmlResourceParser; 54 import android.database.Cursor; 55 import android.media.ApplicationMediaCapabilities; 56 import android.media.MediaCodec; 57 import android.media.MediaFeature; 58 import android.media.MediaFormat; 59 import android.media.MediaTranscodingManager; 60 import android.media.MediaTranscodingManager.TranscodingRequest.VideoFormatResolver; 61 import android.media.MediaTranscodingManager.TranscodingSession; 62 import android.media.MediaTranscodingManager.VideoTranscodingRequest; 63 import android.net.Uri; 64 import android.os.Build; 65 import android.os.Bundle; 66 import android.os.Environment; 67 import android.os.Handler; 68 import android.os.ParcelFileDescriptor; 69 import android.os.Process; 70 import android.os.SystemClock; 71 import android.os.SystemProperties; 72 import android.os.UserHandle; 73 import android.os.storage.StorageManager; 74 import android.os.storage.StorageVolume; 75 import android.provider.MediaStore; 76 import android.provider.MediaStore.Files.FileColumns; 77 import android.provider.MediaStore.MediaColumns; 78 import android.provider.MediaStore.Video.VideoColumns; 79 import android.text.TextUtils; 80 import android.util.ArrayMap; 81 import android.util.ArraySet; 82 import android.util.Log; 83 import android.util.Pair; 84 import android.util.SparseArray; 85 import android.widget.Toast; 86 87 import androidx.annotation.GuardedBy; 88 import androidx.annotation.NonNull; 89 import androidx.annotation.Nullable; 90 import androidx.annotation.RequiresApi; 91 import androidx.core.app.NotificationCompat; 92 import androidx.core.app.NotificationManagerCompat; 93 94 import com.android.internal.annotations.VisibleForTesting; 95 import com.android.modules.utils.BackgroundThread; 96 import com.android.modules.utils.build.SdkLevel; 97 import com.android.providers.media.util.FileUtils; 98 import com.android.providers.media.util.ForegroundThread; 99 import com.android.providers.media.util.SQLiteQueryBuilder; 100 import com.android.providers.media.util.StringUtils; 101 102 import java.io.BufferedReader; 103 import java.io.File; 104 import java.io.IOException; 105 import java.io.InputStream; 106 import java.io.InputStreamReader; 107 import java.io.PrintWriter; 108 import java.lang.annotation.Retention; 109 import java.lang.annotation.RetentionPolicy; 110 import java.time.LocalDateTime; 111 import java.time.format.DateTimeFormatter; 112 import java.time.temporal.ChronoUnit; 113 import java.util.ArrayList; 114 import java.util.LinkedHashMap; 115 import java.util.List; 116 import java.util.Locale; 117 import java.util.Map; 118 import java.util.Optional; 119 import java.util.Set; 120 import java.util.UUID; 121 import java.util.concurrent.CountDownLatch; 122 import java.util.concurrent.ExecutionException; 123 import java.util.concurrent.ExecutorService; 124 import java.util.concurrent.Executors; 125 import java.util.concurrent.Future; 126 import java.util.concurrent.TimeoutException; 127 import java.util.concurrent.TimeUnit; 128 import java.util.regex.Matcher; 129 import java.util.regex.Pattern; 130 131 @RequiresApi(Build.VERSION_CODES.S) 132 public class TranscodeHelperImpl implements TranscodeHelper { 133 private static final String TAG = "TranscodeHelper"; 134 private static final boolean DEBUG = SystemProperties.getBoolean("persist.sys.fuse.log", false); 135 private static final float MAX_APP_NAME_SIZE_PX = 500f; 136 137 // Notice the pairing of the keys.When you change a DEVICE_CONFIG key, then please also change 138 // the corresponding SYS_PROP key too; and vice-versa. 139 // Keeping the whole strings separate for the ease of text search. 140 private static final String TRANSCODE_ENABLED_SYS_PROP_KEY = 141 "persist.sys.fuse.transcode_enabled"; 142 private static final String TRANSCODE_DEFAULT_SYS_PROP_KEY = 143 "persist.sys.fuse.transcode_default"; 144 private static final String TRANSCODE_USER_CONTROL_SYS_PROP_KEY = 145 "persist.sys.fuse.transcode_user_control"; 146 147 private static final int MY_UID = android.os.Process.myUid(); 148 149 // Whether the device has HDR plugin for transcoding HDR to SDR video. 150 private Future<Boolean> mHasHdrPlugin; 151 152 /** 153 * Force enable an app to support the HEVC media capability 154 * 155 * Apps should declare their supported media capabilities in their manifest but this flag can be 156 * used to force an app into supporting HEVC, hence avoiding transcoding while accessing media 157 * encoded in HEVC. 158 * 159 * Setting this flag will override any OS level defaults for apps. It is disabled by default, 160 * meaning that the OS defaults would take precedence. 161 * 162 * Setting this flag and {@code FORCE_DISABLE_HEVC_SUPPORT} is an undefined 163 * state and will result in the OS ignoring both flags. 164 */ 165 @ChangeId 166 @Disabled 167 private static final long FORCE_ENABLE_HEVC_SUPPORT = 174228127L; 168 169 /** 170 * Force disable an app from supporting the HEVC media capability 171 * 172 * Apps should declare their supported media capabilities in their manifest but this flag can be 173 * used to force an app into not supporting HEVC, hence forcing transcoding while accessing 174 * media encoded in HEVC. 175 * 176 * Setting this flag will override any OS level defaults for apps. It is disabled by default, 177 * meaning that the OS defaults would take precedence. 178 * 179 * Setting this flag and {@code FORCE_ENABLE_HEVC_SUPPORT} is an undefined state 180 * and will result in the OS ignoring both flags. 181 */ 182 @ChangeId 183 @Disabled 184 private static final long FORCE_DISABLE_HEVC_SUPPORT = 174227820L; 185 186 @VisibleForTesting 187 static final int FLAG_HEVC = 1 << 0; 188 @VisibleForTesting 189 static final int FLAG_SLOW_MOTION = 1 << 1; 190 private static final int FLAG_HDR_10 = 1 << 2; 191 private static final int FLAG_HDR_10_PLUS = 1 << 3; 192 private static final int FLAG_HDR_HLG = 1 << 4; 193 private static final int FLAG_HDR_DOLBY_VISION = 1 << 5; 194 private static final int MEDIA_FORMAT_FLAG_MASK = FLAG_HEVC | FLAG_SLOW_MOTION 195 | FLAG_HDR_10 | FLAG_HDR_10_PLUS | FLAG_HDR_HLG | FLAG_HDR_DOLBY_VISION; 196 197 @LongDef({ 198 FLAG_HEVC, 199 FLAG_SLOW_MOTION, 200 FLAG_HDR_10, 201 FLAG_HDR_10_PLUS, 202 FLAG_HDR_HLG, 203 FLAG_HDR_DOLBY_VISION 204 }) 205 @Retention(RetentionPolicy.SOURCE) 206 public @interface ApplicationMediaCapabilitiesFlags { 207 } 208 209 /** Coefficient to 'guess' how long a transcoding session might take */ 210 private static final double TRANSCODING_TIMEOUT_COEFFICIENT = 10; 211 /** Coefficient to 'guess' how large a transcoded file might be */ 212 private static final double TRANSCODING_SIZE_COEFFICIENT = 2; 213 214 /** 215 * Copied from MediaProvider.java 216 * TODO(b/170465810): Remove this when getQueryBuilder code is refactored. 217 */ 218 private static final int TYPE_QUERY = 0; 219 private static final int TYPE_UPDATE = 2; 220 221 private static final int MAX_FINISHED_TRANSCODING_SESSION_STORE_COUNT = 16; 222 private static final String DIRECTORY_CAMERA = "Camera"; 223 224 private static final boolean IS_TRANSCODING_SUPPORTED = SdkLevel.isAtLeastS(); 225 226 private final Object mLock = new Object(); 227 private final Context mContext; 228 private final MediaProvider mMediaProvider; 229 private final ConfigStore mConfigStore; 230 private final PackageManager mPackageManager; 231 private final StorageManager mStorageManager; 232 private final ActivityManager mActivityManager; 233 private final File mTranscodeDirectory; 234 private final List<String> mSupportedRelativePaths; 235 @GuardedBy("mLock") 236 private UUID mTranscodeVolumeUuid; 237 238 @GuardedBy("mLock") 239 private final Map<String, StorageTranscodingSession> mStorageTranscodingSessions = 240 new ArrayMap<>(); 241 242 // These are for dumping purpose only. 243 // We keep these separately because the probability of getting cancelled and error'ed sessions 244 // is pretty low, and we are limiting the count of what we keep. So, we don't wanna miss out 245 // on dumping the cancelled and error'ed sessions. 246 @GuardedBy("mLock") 247 private final Map<StorageTranscodingSession, Boolean> mSuccessfulTranscodeSessions = 248 createFinishedTranscodingSessionMap(); 249 @GuardedBy("mLock") 250 private final Map<StorageTranscodingSession, Boolean> mCancelledTranscodeSessions = 251 createFinishedTranscodingSessionMap(); 252 @GuardedBy("mLock") 253 private final Map<StorageTranscodingSession, Boolean> mErroredTranscodeSessions = 254 createFinishedTranscodingSessionMap(); 255 256 private final TranscodeUiNotifier mTranscodingUiNotifier; 257 private final TranscodeDenialController mTranscodeDenialController; 258 private final SessionTiming mSessionTiming; 259 @GuardedBy("mLock") 260 private final Map<String, Integer> mAppCompatMediaCapabilities = new ArrayMap<>(); 261 @GuardedBy("mLock") 262 private boolean mIsTranscodeEnabled; 263 264 private static final String[] TRANSCODE_CACHE_INFO_PROJECTION = 265 {FileColumns._ID, FileColumns._TRANSCODE_STATUS}; 266 private static final String TRANSCODE_WHERE_CLAUSE = 267 FileColumns.DATA + "=?" + " and mime_type not like 'null'"; 268 TranscodeHelperImpl(@onNull Context context, @NonNull MediaProvider mediaProvider, @NonNull ConfigStore configStore)269 public TranscodeHelperImpl(@NonNull Context context, @NonNull MediaProvider mediaProvider, 270 @NonNull ConfigStore configStore) { 271 mContext = context; 272 mPackageManager = context.getPackageManager(); 273 mStorageManager = context.getSystemService(StorageManager.class); 274 mActivityManager = context.getSystemService(ActivityManager.class); 275 mMediaProvider = mediaProvider; 276 mConfigStore = configStore; 277 mTranscodeDirectory = new File("/storage/emulated/" + UserHandle.myUserId(), 278 DIRECTORY_TRANSCODE); 279 mTranscodeDirectory.mkdirs(); 280 mSessionTiming = new SessionTiming(); 281 mTranscodingUiNotifier = new TranscodeUiNotifier(context, mSessionTiming); 282 mIsTranscodeEnabled = isTranscodeEnabled(); 283 mTranscodeDenialController = new TranscodeDenialController(mActivityManager, 284 mTranscodingUiNotifier, mConfigStore.getTranscodeMaxDurationMs()); 285 mSupportedRelativePaths = verifySupportedRelativePaths(StringUtils.getStringArrayConfig( 286 mContext, R.array.config_supported_transcoding_relative_paths)); 287 ExecutorService executor = Executors.newSingleThreadExecutor(); 288 mHasHdrPlugin = executor.submit(() -> { return hasHDRPlugin(); }); 289 executor.shutdown(); 290 291 parseTranscodeCompatManifest(); 292 // The storage namespace is a boot namespace so we actually don't expect this to be changed 293 // after boot, but it is useful for tests 294 configStore.addOnChangeListener( 295 BackgroundThread.getExecutor(), this::parseTranscodeCompatManifest); 296 } 297 hasHDRPlugin()298 private boolean hasHDRPlugin() { 299 MediaCodec decoder = null; 300 boolean hasPlugin = false; 301 try { 302 decoder = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC); 303 // We could query the HDR plugin with any resolution. But as normal HDR video is at 304 // least 1080P(1920x1080), so we create a 1080P video format to query. 305 MediaFormat decoderFormat = MediaFormat.createVideoFormat( 306 MediaFormat.MIMETYPE_VIDEO_HEVC, 1920, 1080); 307 decoderFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, 308 MediaFormat.COLOR_TRANSFER_SDR_VIDEO); 309 decoder.configure(decoderFormat, null, null, 0); 310 MediaFormat inputFormat = decoder.getInputFormat(); 311 if (inputFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST) 312 == MediaFormat.COLOR_TRANSFER_SDR_VIDEO) { 313 hasPlugin = true; 314 } 315 } catch (Exception ioe) { 316 hasPlugin = false; 317 } finally { 318 if (decoder != null) { 319 try { 320 decoder.release(); 321 } catch (Exception e) { 322 Log.w(TAG, "Unable to stop decoder", e); 323 } 324 } 325 } 326 Log.i(TAG, "Device HDR Plugin is available: " + hasPlugin); 327 return hasPlugin; 328 } 329 getHasHdrPlugin()330 private boolean getHasHdrPlugin() { 331 try { 332 return mHasHdrPlugin.get(1, TimeUnit.SECONDS); 333 } catch (InterruptedException | ExecutionException | TimeoutException e) { 334 Log.w(TAG, "Unable to get HDR plugin status", e); 335 return false; 336 } 337 } 338 339 /** 340 * Regex that matches path of transcode file. The regex only 341 * matches emulated volume, for files in other volumes we don't 342 * seamlessly transcode. 343 */ 344 private static final Pattern PATTERN_TRANSCODE_PATH = Pattern.compile( 345 "(?i)^/storage/emulated/(?:[0-9]+)/\\.transforms/transcode/(?:\\d+)$"); 346 private static final String DIRECTORY_TRANSCODE = ".transforms/transcode"; 347 /** 348 * @return true if the file path matches transcode file path. 349 */ isTranscodeFile(@onNull String path)350 private static boolean isTranscodeFile(@NonNull String path) { 351 final Matcher matcher = PATTERN_TRANSCODE_PATH.matcher(path); 352 return matcher.matches(); 353 } 354 freeCache(long bytes)355 public void freeCache(long bytes) { 356 File[] files = mTranscodeDirectory.listFiles(); 357 for (File file : files) { 358 if (bytes <= 0) { 359 return; 360 } 361 if (file.exists() && file.isFile()) { 362 long size = file.length(); 363 boolean deleted = file.delete(); 364 if (deleted) { 365 bytes -= size; 366 } 367 } 368 } 369 } 370 getTranscodeVolumeUuid()371 private UUID getTranscodeVolumeUuid() { 372 synchronized (mLock) { 373 if (mTranscodeVolumeUuid != null) { 374 return mTranscodeVolumeUuid; 375 } 376 } 377 378 StorageVolume vol = mStorageManager.getStorageVolume(mTranscodeDirectory); 379 if (vol != null) { 380 synchronized (mLock) { 381 mTranscodeVolumeUuid = vol.getStorageUuid(); 382 return mTranscodeVolumeUuid; 383 } 384 } else { 385 Log.w(TAG, "Failed to get storage volume UUID for: " + mTranscodeDirectory); 386 return null; 387 } 388 } 389 390 /** 391 * @return transcode file's path for given {@code rowId} 392 */ 393 @NonNull getTranscodePath(long rowId)394 private String getTranscodePath(long rowId) { 395 return new File(mTranscodeDirectory, String.valueOf(rowId)).getAbsolutePath(); 396 } 397 onAnrDelayStarted(String packageName, int uid, int tid, int reason)398 public void onAnrDelayStarted(String packageName, int uid, int tid, int reason) { 399 if (!isTranscodeEnabled()) { 400 return; 401 } 402 403 if (uid == MY_UID) { 404 Log.w(TAG, "Skipping ANR delay handling for MediaProvider"); 405 return; 406 } 407 408 logVerbose("Checking transcode status during ANR of " + packageName); 409 410 Set<StorageTranscodingSession> sessions = new ArraySet<>(); 411 synchronized (mLock) { 412 sessions.addAll(mStorageTranscodingSessions.values()); 413 } 414 415 for (StorageTranscodingSession session: sessions) { 416 if (session.isUidBlocked(uid)) { 417 session.setAnr(); 418 Log.i(TAG, "Package: " + packageName + " with uid: " + uid 419 + " and tid: " + tid + " is blocked on transcoding: " + session); 420 // TODO(b/170973510): Show UI 421 } 422 } 423 } 424 425 // TODO(b/170974147): This should probably use a cache so we don't need to ask the 426 // package manager every time for the package name or installer name getMetricsSafeNameForUid(int uid)427 private String getMetricsSafeNameForUid(int uid) { 428 String name = mPackageManager.getNameForUid(uid); 429 if (name == null) { 430 Log.w(TAG, "null package name received from getNameForUid for uid " + uid 431 + ", logging uid instead."); 432 return Integer.toString(uid); 433 } else if (name.isEmpty()) { 434 Log.w(TAG, "empty package name received from getNameForUid for uid " + uid 435 + ", logging uid instead"); 436 return ":empty_package_name:" + uid; 437 } else { 438 try { 439 InstallSourceInfo installInfo = mPackageManager.getInstallSourceInfo(name); 440 ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(name, 0); 441 if (installInfo.getInstallingPackageName() == null 442 && ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0)) { 443 // For privacy reasons, we don't log metrics for side-loaded packages that 444 // are not system packages 445 return ":installer_adb:" + uid; 446 } 447 return name; 448 } catch (PackageManager.NameNotFoundException e) { 449 Log.w(TAG, "Unable to check installer for uid: " + uid, e); 450 return ":name_not_found:" + uid; 451 } 452 } 453 } 454 reportTranscodingResult(int uid, boolean success, int errorCode, int failureReason, long transcodingDurationMs, int transcodingReason, String src, String dst, boolean hasAnr)455 private void reportTranscodingResult(int uid, boolean success, int errorCode, int failureReason, 456 long transcodingDurationMs, 457 int transcodingReason, String src, String dst, boolean hasAnr) { 458 BackgroundThread.getExecutor().execute(() -> { 459 try (Cursor c = queryFileForTranscode(src, 460 new String[]{MediaColumns.DURATION, MediaColumns.CAPTURE_FRAMERATE, 461 MediaColumns.WIDTH, MediaColumns.HEIGHT})) { 462 if (c != null && c.moveToNext()) { 463 MediaProviderStatsLog.write( 464 TRANSCODING_DATA, 465 getMetricsSafeNameForUid(uid), 466 MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_TRANSCODE, 467 success ? new File(dst).length() : -1, 468 success ? TRANSCODING_DATA__TRANSCODE_RESULT__SUCCESS : 469 TRANSCODING_DATA__TRANSCODE_RESULT__FAIL, 470 transcodingDurationMs, 471 c.getLong(0) /* video_duration */, 472 c.getLong(1) /* capture_framerate */, 473 transcodingReason, 474 c.getLong(2) /* width */, 475 c.getLong(3) /* height */, 476 hasAnr, 477 failureReason, 478 errorCode); 479 } 480 } 481 }); 482 } 483 transcode(String src, String dst, int uid, int reason)484 public boolean transcode(String src, String dst, int uid, int reason) { 485 // This can only happen when we are in a version that supports transcoding. 486 // So, no need to check for the SDK version here. 487 488 StorageTranscodingSession storageSession = null; 489 TranscodingSession transcodingSession = null; 490 CountDownLatch latch = null; 491 long startTime = SystemClock.elapsedRealtime(); 492 boolean result = false; 493 int errorCode = TranscodingSession.ERROR_SERVICE_DIED; 494 int failureReason = TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SERVICE_ERROR; 495 496 try { 497 synchronized (mLock) { 498 storageSession = mStorageTranscodingSessions.get(src); 499 if (storageSession == null) { 500 latch = new CountDownLatch(1); 501 try { 502 transcodingSession = enqueueTranscodingSession(src, dst, uid, latch); 503 if (transcodingSession == null) { 504 Log.e(TAG, "Failed to enqueue request due to Service unavailable"); 505 throw new IllegalStateException("Failed to enqueue request"); 506 } 507 } catch (UnsupportedOperationException | IOException e) { 508 throw new IllegalStateException(e); 509 } 510 storageSession = new StorageTranscodingSession(transcodingSession, latch, 511 src, dst); 512 mStorageTranscodingSessions.put(src, storageSession); 513 } else { 514 latch = storageSession.latch; 515 transcodingSession = storageSession.session; 516 if (latch == null || transcodingSession == null) { 517 throw new IllegalStateException("Uninitialised TranscodingSession for uid: " 518 + uid + ". Path: " + src); 519 } 520 } 521 storageSession.addBlockedUid(uid); 522 } 523 524 failureReason = waitTranscodingResult(uid, src, transcodingSession, latch); 525 errorCode = transcodingSession.getErrorCode(); 526 result = failureReason == TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN; 527 528 if (result) { 529 updateTranscodeStatus(src, TRANSCODE_COMPLETE); 530 } else { 531 logEvent("Transcoding failed for " + src + ". session: ", transcodingSession); 532 // Attempt to workaround potential media transcoding deadlock 533 // Cancelling a deadlocked session seems to unblock the transcoder 534 transcodingSession.cancel(); 535 } 536 } finally { 537 if (storageSession == null) { 538 Log.w(TAG, "Failed to create a StorageTranscodingSession"); 539 // We were unable to even queue the request. Which means the media service is 540 // in a very bad state 541 reportTranscodingResult(uid, result, errorCode, failureReason, 542 SystemClock.elapsedRealtime() - startTime, reason, 543 src, dst, false /* hasAnr */); 544 return false; 545 } 546 547 storageSession.notifyFinished(failureReason, errorCode); 548 if (errorCode == TranscodingSession.ERROR_DROPPED_BY_SERVICE) { 549 // If the transcoding service drops a request for a uid the uid will be denied 550 // transcoding access until the next boot, notify the denial controller which may 551 // also show a denial UI 552 mTranscodeDenialController.onTranscodingDropped(uid); 553 } 554 reportTranscodingResult(uid, result, errorCode, failureReason, 555 SystemClock.elapsedRealtime() - startTime, reason, 556 src, dst, storageSession.hasAnr()); 557 } 558 return result; 559 } 560 561 /** 562 * Returns IO path for a {@code path} and {@code uid} 563 * 564 * IO path is the actual path to be used on the lower fs for IO via FUSE. For some file 565 * transforms, this path might be different from the path the app is requesting IO on. 566 * 567 * @param path file path to get an IO path for 568 * @param uid app requesting IO 569 * 570 */ prepareIoPath(String path, int uid)571 public String prepareIoPath(String path, int uid) { 572 // This can only happen when we are in a version that supports transcoding. 573 // So, no need to check for the SDK version here. 574 575 Pair<Long, Integer> cacheInfo = getTranscodeCacheInfoFromDB(path); 576 final long rowId = cacheInfo.first; 577 if (rowId == -1) { 578 // No database row found, The file is pending/trashed or not added to database yet. 579 // Assuming that no transcoding needed. 580 return path; 581 } 582 583 int transcodeStatus = cacheInfo.second; 584 final String transcodePath = getTranscodePath(rowId); 585 final File transcodeFile = new File(transcodePath); 586 587 if (transcodeFile.exists()) { 588 return transcodePath; 589 } 590 591 if (transcodeStatus == TRANSCODE_COMPLETE) { 592 // The transcode file doesn't exist but db row is marked as TRANSCODE_COMPLETE, 593 // update db row to TRANSCODE_EMPTY so that cache state remains valid. 594 updateTranscodeStatus(path, TRANSCODE_EMPTY); 595 } 596 597 final long maxFileSize = (long) (new File(path).length() * 2); 598 if (createSparseFile(transcodeFile, maxFileSize)) { 599 return transcodePath; 600 } 601 602 return ""; 603 } 604 getMediaCapabilitiesUid(int uid, Bundle bundle)605 private static int getMediaCapabilitiesUid(int uid, Bundle bundle) { 606 if (bundle == null || !bundle.containsKey(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID)) { 607 return uid; 608 } 609 610 int mediaCapabilitiesUid = bundle.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID); 611 if (mediaCapabilitiesUid >= Process.FIRST_APPLICATION_UID) { 612 logVerbose( 613 "Media capabilities uid " + mediaCapabilitiesUid + ", passed for uid " + uid); 614 return mediaCapabilitiesUid; 615 } 616 Log.w(TAG, "Ignoring invalid media capabilities uid " + mediaCapabilitiesUid 617 + " for uid: " + uid); 618 return uid; 619 } 620 621 // TODO(b/173491972): Generalize to consider other file/app media capabilities beyond hevc 622 /** 623 * @return 0 or >0 representing whether we should transcode or not. 624 * 0 means we should not transcode, otherwise we should transcode and the value is the 625 * reason that will be logged to statsd as a transcode reason. Possible values are: 626 * <ul> 627 * <li>MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__SYSTEM_DEFAULT=1 628 * <li>MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__SYSTEM_CONFIG=2 629 * <li>MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_MANIFEST=3 630 * <li>MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_COMPAT=4 631 * <li>MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_EXTRA=5 632 * </ul> 633 * 634 */ shouldTranscode(String path, int uid, Bundle bundle)635 public int shouldTranscode(String path, int uid, Bundle bundle) { 636 boolean isTranscodeEnabled = isTranscodeEnabled(); 637 updateConfigs(isTranscodeEnabled); 638 639 if (!isTranscodeEnabled) { 640 logVerbose("Transcode not enabled"); 641 return 0; 642 } 643 644 uid = getMediaCapabilitiesUid(uid, bundle); 645 logVerbose("Checking shouldTranscode for: " + path + ". Uid: " + uid); 646 647 if (!supportsTranscode(path) || uid < Process.FIRST_APPLICATION_UID || uid == MY_UID) { 648 logVerbose("Transcode not supported"); 649 // Never transcode in any of these conditions 650 // 1. Path doesn't support transcode 651 // 2. Uid is from native process on device 652 // 3. Uid is ourselves, which can happen when we are opening a file via FUSE for 653 // redaction on behalf of another app via ContentResolver 654 return 0; 655 } 656 657 // Transcode only if file needs transcoding 658 Pair<Integer, Long> result = getFileFlagsAndDurationMs(path); 659 int fileFlags = result.first; 660 long durationMs = result.second; 661 662 if (fileFlags == 0) { 663 // Nothing to transcode 664 logVerbose("File is not HEVC"); 665 return 0; 666 } 667 668 int accessReason = doesAppNeedTranscoding(uid, bundle, fileFlags, durationMs); 669 if (accessReason != 0 && mTranscodeDenialController.checkFileAccess(uid, durationMs)) { 670 logVerbose("Transcoding denied"); 671 return 0; 672 } 673 return accessReason; 674 } 675 676 @VisibleForTesting doesAppNeedTranscoding(int uid, Bundle bundle, int fileFlags, long durationMs)677 int doesAppNeedTranscoding(int uid, Bundle bundle, int fileFlags, long durationMs) { 678 // Check explicit Bundle provided 679 if (bundle != null) { 680 if (bundle.getBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, false)) { 681 logVerbose("Original format requested"); 682 return 0; 683 } 684 685 ApplicationMediaCapabilities capabilities = 686 bundle.getParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES); 687 if (capabilities != null) { 688 Pair<Integer, Integer> flags = capabilitiesToMediaFormatFlags(capabilities); 689 Optional<Boolean> appExtraResult = checkAppMediaSupport(flags.first, flags.second, 690 fileFlags, "app_extra"); 691 if (appExtraResult.isPresent()) { 692 if (appExtraResult.get()) { 693 return MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_EXTRA; 694 } 695 return 0; 696 } 697 // Bundle didn't have enough information to make decision, continue 698 } 699 } 700 701 // Check app compat support 702 Optional<Boolean> appCompatResult = checkAppCompatSupport(uid, fileFlags); 703 if (appCompatResult.isPresent()) { 704 if (appCompatResult.get()) { 705 return MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_COMPAT; 706 } 707 return 0; 708 } 709 // App compat didn't have enough information to make decision, continue 710 711 // If we are here then the file supports HEVC, so we only check if the package is in the 712 // mAppCompatCapabilities. If it's there, we will respect that value. 713 LocalCallingIdentity identity = mMediaProvider.getCachedCallingIdentityForTranscoding(uid); 714 final String[] callingPackages = identity.getSharedPackageNamesArray(); 715 716 // Check app manifest support 717 for (String callingPackage : callingPackages) { 718 Optional<Boolean> appManifestResult = checkManifestSupport(callingPackage, identity, 719 fileFlags); 720 if (appManifestResult.isPresent()) { 721 if (appManifestResult.get()) { 722 return MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__APP_MANIFEST; 723 } 724 return 0; 725 } 726 // App manifest didn't have enough information to make decision, continue 727 728 // TODO(b/169327180): We should also check app's targetSDK version to verify if app 729 // still qualifies to be on these lists. 730 // Check config compat manifest 731 synchronized (mLock) { 732 if (mAppCompatMediaCapabilities.containsKey(callingPackage)) { 733 int configCompatFlags = mAppCompatMediaCapabilities.get(callingPackage); 734 int supportedFlags = configCompatFlags; 735 int unsupportedFlags = ~configCompatFlags & MEDIA_FORMAT_FLAG_MASK; 736 737 Optional<Boolean> systemConfigResult = checkAppMediaSupport(supportedFlags, 738 unsupportedFlags, fileFlags, "system_config"); 739 if (systemConfigResult.isPresent()) { 740 if (systemConfigResult.get()) { 741 return MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__SYSTEM_CONFIG; 742 } 743 return 0; 744 } 745 // Should never get here because the supported & unsupported flags should span 746 // the entire universe of file flags 747 } 748 } 749 } 750 751 // TODO: Need to add transcode_default as flags 752 if (shouldTranscodeDefault()) { 753 logVerbose("Default behavior should transcode"); 754 return MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_REASON__SYSTEM_DEFAULT; 755 } else { 756 logVerbose("Default behavior should not transcode"); 757 return 0; 758 } 759 } 760 761 /** 762 * Checks if transcode is required for the given app media capabilities and file media formats 763 * 764 * @param appSupportedMediaFormatFlags bit mask of media capabilites explicitly supported by an 765 * app, e.g 001 indicating HEVC support 766 * @param appUnsupportedMediaFormatFlags bit mask of media capabilites explicitly not supported 767 * by an app, e.g 10 indicating HDR_10 is not supportted 768 * @param fileMediaFormatFlags bit mask of media capabilites contained in a file e.g 101 769 * indicating HEVC and HDR_10 media file 770 * 771 * @return {@code Optional} containing {@code boolean}. {@code true} means transcode is 772 * required, {@code false} means transcode is not required and {@code empty} means a decision 773 * could not be made. 774 */ checkAppMediaSupport(int appSupportedMediaFormatFlags, int appUnsupportedMediaFormatFlags, int fileMediaFormatFlags, String type)775 private Optional<Boolean> checkAppMediaSupport(int appSupportedMediaFormatFlags, 776 int appUnsupportedMediaFormatFlags, int fileMediaFormatFlags, String type) { 777 if ((appSupportedMediaFormatFlags & appUnsupportedMediaFormatFlags) != 0) { 778 Log.w(TAG, "Ignoring app media capabilities for type: [" + type 779 + "]. Supported and unsupported capapbilities are not mutually exclusive"); 780 return Optional.empty(); 781 } 782 783 // As an example: 784 // 1. appSupportedMediaFormatFlags=001 # App supports HEVC 785 // 2. appUnsupportedMediaFormatFlags=100 # App does not support HDR_10 786 // 3. fileSupportedMediaFormatFlags=101 # File contains HEVC and HDR_10 787 788 // File contains HDR_10 but app explicitly doesn't support it 789 int fileMediaFormatsUnsupportedByApp = 790 fileMediaFormatFlags & appUnsupportedMediaFormatFlags; 791 if (fileMediaFormatsUnsupportedByApp != 0) { 792 // If *any* file media formats are unsupported by the app we need to transcode 793 logVerbose("App media capability check for type: [" + type + "]" + ". transcode=true"); 794 return Optional.of(true); 795 } 796 797 // fileMediaFormatsSupportedByApp=001 # File contains HEVC but app explicitly supports HEVC 798 int fileMediaFormatsSupportedByApp = appSupportedMediaFormatFlags & fileMediaFormatFlags; 799 // fileMediaFormatsNotSupportedByApp=100 # File contains HDR_10 but app doesn't support it 800 int fileMediaFormatsNotSupportedByApp = 801 fileMediaFormatsSupportedByApp ^ fileMediaFormatFlags; 802 if (fileMediaFormatsNotSupportedByApp == 0) { 803 logVerbose("App media capability check for type: [" + type + "]" + ". transcode=false"); 804 // If *all* file media formats are supported by the app, we don't need to transcode 805 return Optional.of(false); 806 } 807 808 // If there are some file media formats that are neither supported nor unsupported by the 809 // app we can't make a decision yet 810 return Optional.empty(); 811 } 812 getFileFlagsAndDurationMs(String path)813 private Pair<Integer, Long> getFileFlagsAndDurationMs(String path) { 814 final String[] projection = new String[] { 815 FileColumns._VIDEO_CODEC_TYPE, 816 VideoColumns.COLOR_STANDARD, 817 VideoColumns.COLOR_TRANSFER, 818 MediaColumns.DURATION 819 }; 820 821 try (Cursor cursor = queryFileForTranscode(path, projection)) { 822 if (cursor == null || !cursor.moveToNext()) { 823 logVerbose("Couldn't find database row"); 824 return Pair.create(0, 0L); 825 } 826 827 int result = 0; 828 boolean isHdr10Plus = isHdr10Plus(cursor.getInt(1), cursor.getInt(2)); 829 // If the video is a HDR video and the device does not have HDR plugin, we will return 830 // the original file regardless whether the app supports HEVC due to not all the devices 831 // support transcoding 10bit HEVC to 8bit AVC. This check needs to be removed when 832 // devices add support for it. 833 boolean isTranscodeUnsupported = isHdr10Plus && !getHasHdrPlugin(); 834 if (isTranscodeUnsupported) { 835 return Pair.create(0, 0L); 836 } 837 838 if (isHevc(cursor.getString(0))) { 839 result |= FLAG_HEVC; 840 } 841 // Set the HDR flag if the device has HDR plugin. If HDR plugin is not available, 842 // we will make the transcode decision based on whether the app supports HEVC or not. 843 if (isHdr10Plus) { 844 result |= FLAG_HDR_10_PLUS; 845 } 846 return Pair.create(result, cursor.getLong(3)); 847 } 848 } 849 isHevc(String mimeType)850 private static boolean isHevc(String mimeType) { 851 return MediaFormat.MIMETYPE_VIDEO_HEVC.equalsIgnoreCase(mimeType); 852 } 853 isHdr10Plus(int colorStandard, int colorTransfer)854 private static boolean isHdr10Plus(int colorStandard, int colorTransfer) { 855 return (colorStandard == MediaFormat.COLOR_STANDARD_BT2020) && 856 (colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 857 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG); 858 } 859 isModernFormat(String mimeType, int colorStandard, int colorTransfer)860 private static boolean isModernFormat(String mimeType, int colorStandard, int colorTransfer) { 861 return isHevc(mimeType) || isHdr10Plus(colorStandard, colorTransfer); 862 } 863 supportsTranscode(String path)864 public boolean supportsTranscode(String path) { 865 final File file = new File(path); 866 final String name = file.getName(); 867 final String relativePath = FileUtils.extractRelativePath(path); 868 869 if (isTranscodeFile(path) || !name.toLowerCase(Locale.ROOT).endsWith(".mp4") 870 || !path.startsWith("/storage/emulated/")) { 871 return false; 872 } 873 874 for (String supportedRelativePath : mSupportedRelativePaths) { 875 if (supportedRelativePath.equalsIgnoreCase(relativePath)) { 876 return true; 877 } 878 } 879 880 return false; 881 } 882 verifySupportedRelativePaths(List<String> relativePaths)883 private static List<String> verifySupportedRelativePaths(List<String> relativePaths) { 884 final List<String> verifiedPaths = new ArrayList<>(); 885 final String lowerCaseDcimDir = Environment.DIRECTORY_DCIM.toLowerCase(Locale.ROOT) + "/"; 886 887 for (String path : relativePaths) { 888 if (path.toLowerCase(Locale.ROOT).startsWith(lowerCaseDcimDir) && path.endsWith("/")) { 889 verifiedPaths.add(path); 890 } else { 891 Log.w(TAG, "Transcoding relative path must be a descendant of DCIM/ and end with" 892 + " '/'. Ignoring: " + path); 893 } 894 } 895 896 return verifiedPaths; 897 } 898 checkAppCompatSupport(int uid, int fileFlags)899 private Optional<Boolean> checkAppCompatSupport(int uid, int fileFlags) { 900 int supportedFlags = 0; 901 int unsupportedFlags = 0; 902 boolean hevcSupportEnabled = CompatChanges.isChangeEnabled(FORCE_ENABLE_HEVC_SUPPORT, uid); 903 boolean hevcSupportDisabled = CompatChanges.isChangeEnabled(FORCE_DISABLE_HEVC_SUPPORT, 904 uid); 905 if (hevcSupportEnabled) { 906 supportedFlags = FLAG_HEVC; 907 logVerbose("App compat hevc support enabled"); 908 } 909 910 if (hevcSupportDisabled) { 911 unsupportedFlags = FLAG_HEVC; 912 logVerbose("App compat hevc support disabled"); 913 } 914 return checkAppMediaSupport(supportedFlags, unsupportedFlags, fileFlags, "app_compat"); 915 } 916 917 /** 918 * @return {@code true} if HEVC is explicitly supported by the manifest of {@code packageName}, 919 * {@code false} otherwise. 920 */ checkManifestSupport(String packageName, LocalCallingIdentity identity, int fileFlags)921 private Optional<Boolean> checkManifestSupport(String packageName, 922 LocalCallingIdentity identity, int fileFlags) { 923 // TODO(b/169327180): 924 // 1. Support beyond HEVC 925 // 2. Shared package names policy: 926 // If appA and appB share the same uid. And appA supports HEVC but appB doesn't. 927 // Should we assume entire uid supports or doesn't? 928 // For now, we assume uid supports, but this might change in future 929 int supportedFlags = identity.getApplicationMediaCapabilitiesSupportedFlags(); 930 int unsupportedFlags = identity.getApplicationMediaCapabilitiesUnsupportedFlags(); 931 if (supportedFlags != -1 && unsupportedFlags != -1) { 932 return checkAppMediaSupport(supportedFlags, unsupportedFlags, fileFlags, 933 "cached_app_manifest"); 934 } 935 936 try { 937 Property mediaCapProperty = mPackageManager.getProperty( 938 PackageManager.PROPERTY_MEDIA_CAPABILITIES, packageName); 939 XmlResourceParser parser = mPackageManager.getResourcesForApplication(packageName) 940 .getXml(mediaCapProperty.getResourceId()); 941 ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml( 942 parser); 943 Pair<Integer, Integer> flags = capabilitiesToMediaFormatFlags(capability); 944 supportedFlags = flags.first; 945 unsupportedFlags = flags.second; 946 identity.setApplicationMediaCapabilitiesFlags(supportedFlags, unsupportedFlags); 947 948 return checkAppMediaSupport(supportedFlags, unsupportedFlags, fileFlags, 949 "app_manifest"); 950 } catch (PackageManager.NameNotFoundException | UnsupportedOperationException e) { 951 return Optional.empty(); 952 } 953 } 954 955 @ApplicationMediaCapabilitiesFlags capabilitiesToMediaFormatFlags( ApplicationMediaCapabilities capability)956 private Pair<Integer, Integer> capabilitiesToMediaFormatFlags( 957 ApplicationMediaCapabilities capability) { 958 int supportedFlags = 0; 959 int unsupportedFlags = 0; 960 961 // MimeType 962 if (capability.isFormatSpecified(MediaFormat.MIMETYPE_VIDEO_HEVC)) { 963 if (capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC)) { 964 supportedFlags |= FLAG_HEVC; 965 } else { 966 unsupportedFlags |= FLAG_HEVC; 967 } 968 } 969 970 // HdrType 971 if (capability.isFormatSpecified(MediaFeature.HdrType.HDR10)) { 972 if (capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10)) { 973 supportedFlags |= FLAG_HDR_10; 974 } else { 975 unsupportedFlags |= FLAG_HDR_10; 976 } 977 } 978 979 if (capability.isFormatSpecified(MediaFeature.HdrType.HDR10_PLUS)) { 980 if (capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10_PLUS)) { 981 supportedFlags |= FLAG_HDR_10_PLUS; 982 } else { 983 unsupportedFlags |= FLAG_HDR_10_PLUS; 984 } 985 } 986 987 if (capability.isFormatSpecified(MediaFeature.HdrType.HLG)) { 988 if (capability.isHdrTypeSupported(MediaFeature.HdrType.HLG)) { 989 supportedFlags |= FLAG_HDR_HLG; 990 } else { 991 unsupportedFlags |= FLAG_HDR_HLG; 992 } 993 } 994 995 if (capability.isFormatSpecified(MediaFeature.HdrType.DOLBY_VISION)) { 996 if (capability.isHdrTypeSupported(MediaFeature.HdrType.DOLBY_VISION)) { 997 supportedFlags |= FLAG_HDR_DOLBY_VISION; 998 } else { 999 unsupportedFlags |= FLAG_HDR_DOLBY_VISION; 1000 } 1001 } 1002 1003 return Pair.create(supportedFlags, unsupportedFlags); 1004 } 1005 getTranscodeCacheInfoFromDB(String path)1006 private Pair<Long, Integer> getTranscodeCacheInfoFromDB(String path) { 1007 try (Cursor cursor = queryFileForTranscode(path, TRANSCODE_CACHE_INFO_PROJECTION)) { 1008 if (cursor != null && cursor.moveToNext()) { 1009 return Pair.create(cursor.getLong(0), cursor.getInt(1)); 1010 } 1011 } 1012 return Pair.create((long) -1, TRANSCODE_EMPTY); 1013 } 1014 1015 // called from MediaProvider onUriPublished(Uri uri)1016 public void onUriPublished(Uri uri) { 1017 if (!isTranscodeEnabled()) { 1018 return; 1019 } 1020 1021 try (Cursor c = mMediaProvider.queryForSingleItem(uri, 1022 new String[]{ 1023 FileColumns._VIDEO_CODEC_TYPE, 1024 FileColumns.SIZE, 1025 FileColumns.OWNER_PACKAGE_NAME, 1026 FileColumns.DATA, 1027 MediaColumns.DURATION, 1028 MediaColumns.CAPTURE_FRAMERATE, 1029 MediaColumns.WIDTH, 1030 MediaColumns.HEIGHT 1031 }, 1032 null, null, null)) { 1033 if (supportsTranscode(c.getString(3))) { 1034 if (isHevc(c.getString(0))) { 1035 MediaProviderStatsLog.write( 1036 TRANSCODING_DATA, 1037 c.getString(2) /* owner_package_name */, 1038 MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__HEVC_WRITE, 1039 c.getLong(1) /* file size */, 1040 TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED, 1041 -1 /* transcoding_duration */, 1042 c.getLong(4) /* video_duration */, 1043 c.getLong(5) /* capture_framerate */, 1044 -1 /* transcode_reason */, 1045 c.getLong(6) /* width */, 1046 c.getLong(7) /* height */, 1047 false /* hit_anr */, 1048 TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN, 1049 TranscodingSession.ERROR_NONE); 1050 1051 } else { 1052 MediaProviderStatsLog.write( 1053 TRANSCODING_DATA, 1054 c.getString(2) /* owner_package_name */, 1055 MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__AVC_WRITE, 1056 c.getLong(1) /* file size */, 1057 TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED, 1058 -1 /* transcoding_duration */, 1059 c.getLong(4) /* video_duration */, 1060 c.getLong(5) /* capture_framerate */, 1061 -1 /* transcode_reason */, 1062 c.getLong(6) /* width */, 1063 c.getLong(7) /* height */, 1064 false /* hit_anr */, 1065 TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN, 1066 TranscodingSession.ERROR_NONE); 1067 } 1068 } 1069 } catch (Exception e) { 1070 Log.w(TAG, "Couldn't get cursor for scanned file", e); 1071 } 1072 } 1073 onFileOpen(String path, String ioPath, int uid, int transformsReason)1074 public void onFileOpen(String path, String ioPath, int uid, int transformsReason) { 1075 if (!isTranscodeEnabled() || !supportsTranscode(path)) { 1076 return; 1077 } 1078 1079 String[] resolverInfoProjection = new String[] { 1080 FileColumns._VIDEO_CODEC_TYPE, 1081 FileColumns.SIZE, 1082 MediaColumns.DURATION, 1083 MediaColumns.CAPTURE_FRAMERATE, 1084 MediaColumns.WIDTH, 1085 MediaColumns.HEIGHT, 1086 VideoColumns.COLOR_STANDARD, 1087 VideoColumns.COLOR_TRANSFER 1088 }; 1089 1090 try (Cursor c = queryFileForTranscode(path, resolverInfoProjection)) { 1091 if (c != null && c.moveToNext() 1092 && isModernFormat(c.getString(0), c.getInt(6), c.getInt(7))) { 1093 if (transformsReason == 0) { 1094 MediaProviderStatsLog.write( 1095 TRANSCODING_DATA, 1096 getMetricsSafeNameForUid(uid) /* owner_package_name */, 1097 MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_DIRECT, 1098 c.getLong(1) /* file size */, 1099 TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED, 1100 -1 /* transcoding_duration */, 1101 c.getLong(2) /* video_duration */, 1102 c.getLong(3) /* capture_framerate */, 1103 -1 /* transcode_reason */, 1104 c.getLong(4) /* width */, 1105 c.getLong(5) /* height */, 1106 false /*hit_anr*/, 1107 TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN, 1108 TranscodingSession.ERROR_NONE); 1109 } else if (isTranscodeFileCached(path, ioPath)) { 1110 MediaProviderStatsLog.write( 1111 TRANSCODING_DATA, 1112 getMetricsSafeNameForUid(uid) /* owner_package_name */, 1113 MediaProviderStatsLog.TRANSCODING_DATA__ACCESS_TYPE__READ_CACHE, 1114 c.getLong(1) /* file size */, 1115 TRANSCODING_DATA__TRANSCODE_RESULT__UNDEFINED, 1116 -1 /* transcoding_duration */, 1117 c.getLong(2) /* video_duration */, 1118 c.getLong(3) /* capture_framerate */, 1119 transformsReason /* transcode_reason */, 1120 c.getLong(4) /* width */, 1121 c.getLong(5) /* height */, 1122 false /*hit_anr*/, 1123 TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN, 1124 TranscodingSession.ERROR_NONE); 1125 } // else if file is not in cache, we'll log at read(2) when we transcode 1126 } 1127 } catch (IllegalStateException e) { 1128 Log.w(TAG, "Unable to log metrics on file open", e); 1129 } 1130 } 1131 isTranscodeFileCached(String path, String transcodePath)1132 public boolean isTranscodeFileCached(String path, String transcodePath) { 1133 // This can only happen when we are in a version that supports transcoding. 1134 // So, no need to check for the SDK version here. 1135 1136 if (SystemProperties.getBoolean("persist.sys.fuse.disable_transcode_cache", false)) { 1137 // Caching is disabled. Hence, delete the cached transcode file. 1138 return false; 1139 } 1140 1141 Pair<Long, Integer> cacheInfo = getTranscodeCacheInfoFromDB(path); 1142 final long rowId = cacheInfo.first; 1143 if (rowId != -1) { 1144 final int transcodeStatus = cacheInfo.second; 1145 boolean result = transcodePath.equalsIgnoreCase(getTranscodePath(rowId)) && 1146 transcodeStatus == TRANSCODE_COMPLETE && 1147 new File(transcodePath).exists(); 1148 if (result) { 1149 logEvent("Transcode cache hit: " + path, null /* session */); 1150 } 1151 return result; 1152 } 1153 return false; 1154 } 1155 1156 @Nullable getVideoTrackFormat(String path)1157 private MediaFormat getVideoTrackFormat(String path) { 1158 String[] resolverInfoProjection = new String[]{ 1159 FileColumns._VIDEO_CODEC_TYPE, 1160 MediaStore.MediaColumns.WIDTH, 1161 MediaStore.MediaColumns.HEIGHT, 1162 MediaStore.MediaColumns.BITRATE, 1163 MediaStore.MediaColumns.CAPTURE_FRAMERATE 1164 }; 1165 try (Cursor c = queryFileForTranscode(path, resolverInfoProjection)) { 1166 if (c != null && c.moveToNext()) { 1167 String codecType = c.getString(0); 1168 int width = c.getInt(1); 1169 int height = c.getInt(2); 1170 int bitRate = c.getInt(3); 1171 float framerate = c.getFloat(4); 1172 1173 // TODO(b/169849854): Get this info from Manifest, for now if app got here it 1174 // definitely doesn't support hevc 1175 ApplicationMediaCapabilities capability = 1176 new ApplicationMediaCapabilities.Builder().build(); 1177 MediaFormat sourceFormat = MediaFormat.createVideoFormat( 1178 codecType, width, height); 1179 if (framerate > 0) { 1180 sourceFormat.setFloat(MediaFormat.KEY_FRAME_RATE, framerate); 1181 } 1182 VideoFormatResolver resolver = new VideoFormatResolver(capability, sourceFormat); 1183 MediaFormat resolvedFormat = resolver.resolveVideoFormat(); 1184 resolvedFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); 1185 1186 return resolvedFormat; 1187 } 1188 } 1189 throw new IllegalStateException("Couldn't get video format info from database for " + path); 1190 } 1191 enqueueTranscodingSession(String src, String dst, int uid, final CountDownLatch latch)1192 private TranscodingSession enqueueTranscodingSession(String src, String dst, int uid, 1193 final CountDownLatch latch) throws UnsupportedOperationException, IOException { 1194 // Fetch the service lazily to improve memory usage 1195 final MediaTranscodingManager mediaTranscodeManager = 1196 mContext.getSystemService(MediaTranscodingManager.class); 1197 File file = new File(src); 1198 File transcodeFile = new File(dst); 1199 1200 // These are file URIs (effectively file paths) and even if the |transcodeFile| is 1201 // inaccesible via FUSE, it works because the transcoding service calls into the 1202 // MediaProvider to open them and within the MediaProvider, it is opened directly on 1203 // the lower fs. 1204 Uri uri = Uri.fromFile(file); 1205 Uri transcodeUri = Uri.fromFile(transcodeFile); 1206 1207 ParcelFileDescriptor srcPfd = ParcelFileDescriptor.open(file, 1208 ParcelFileDescriptor.MODE_READ_ONLY); 1209 ParcelFileDescriptor dstPfd = ParcelFileDescriptor.open(transcodeFile, 1210 ParcelFileDescriptor.MODE_READ_WRITE); 1211 1212 MediaFormat format = getVideoTrackFormat(src); 1213 1214 VideoTranscodingRequest request = 1215 new VideoTranscodingRequest.Builder(uri, transcodeUri, format) 1216 .setClientUid(uid) 1217 .setSourceFileDescriptor(srcPfd) 1218 .setDestinationFileDescriptor(dstPfd) 1219 .build(); 1220 1221 TranscodingSession session = mediaTranscodeManager.enqueueRequest(request, 1222 ForegroundThread.getExecutor(), 1223 s -> { 1224 mTranscodingUiNotifier.stop(s, src); 1225 finishTranscodingResult(uid, src, s, latch); 1226 mSessionTiming.logSessionEnd(s); 1227 }); 1228 session.setOnProgressUpdateListener(ForegroundThread.getExecutor(), 1229 (s, progress) -> mTranscodingUiNotifier.setProgress(s, src, progress)); 1230 1231 mSessionTiming.logSessionStart(session); 1232 mTranscodingUiNotifier.start(session, src); 1233 logEvent("Transcoding start: " + src + ". Uid: " + uid, session); 1234 return session; 1235 } 1236 1237 /** 1238 * Returns an {@link Integer} indicating whether the transcoding {@code session} was successful 1239 * or not. 1240 * 1241 * @return {@link TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN} on success, 1242 * otherwise indicates failure. 1243 */ waitTranscodingResult(int uid, String src, TranscodingSession session, CountDownLatch latch)1244 private int waitTranscodingResult(int uid, String src, TranscodingSession session, 1245 CountDownLatch latch) { 1246 UUID uuid = getTranscodeVolumeUuid(); 1247 try { 1248 if (uuid != null) { 1249 // tid is 0 since we can't really get the apps tid over binder 1250 mStorageManager.notifyAppIoBlocked(uuid, uid, 0 /* tid */, 1251 StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING); 1252 } 1253 1254 int timeout = getTranscodeTimeoutSeconds(src); 1255 1256 String waitStartLog = "Transcoding wait start: " + src + ". Uid: " + uid + ". Timeout: " 1257 + timeout + "s"; 1258 logEvent(waitStartLog, session); 1259 1260 boolean latchResult = latch.await(timeout, TimeUnit.SECONDS); 1261 int sessionResult = session.getResult(); 1262 boolean transcodeResult = sessionResult == TranscodingSession.RESULT_SUCCESS; 1263 1264 String waitEndLog = "Transcoding wait end: " + src + ". Uid: " + uid + ". Timeout: " 1265 + !latchResult + ". Success: " + transcodeResult; 1266 logEvent(waitEndLog, session); 1267 1268 if (sessionResult == TranscodingSession.RESULT_SUCCESS) { 1269 return TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN; 1270 } else if (sessionResult == TranscodingSession.RESULT_CANCELED) { 1271 return TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SESSION_CANCELED; 1272 } else if (!latchResult) { 1273 return TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_CLIENT_TIMEOUT; 1274 } else { 1275 return TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SERVICE_ERROR; 1276 } 1277 } catch (InterruptedException e) { 1278 Thread.currentThread().interrupt(); 1279 Log.w(TAG, "Transcoding latch interrupted." + session); 1280 return TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_CLIENT_TIMEOUT; 1281 } finally { 1282 if (uuid != null) { 1283 // tid is 0 since we can't really get the apps tid over binder 1284 mStorageManager.notifyAppIoResumed(uuid, uid, 0 /* tid */, 1285 StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING); 1286 } 1287 } 1288 } 1289 getTranscodeTimeoutSeconds(String file)1290 private int getTranscodeTimeoutSeconds(String file) { 1291 double sizeMb = (new File(file).length() / (1024 * 1024)); 1292 // Ensure size is at least 1MB so transcoding timeout is at least the timeout coefficient 1293 sizeMb = Math.max(sizeMb, 1); 1294 return (int) (sizeMb * TRANSCODING_TIMEOUT_COEFFICIENT); 1295 } 1296 finishTranscodingResult(int uid, String src, TranscodingSession session, CountDownLatch latch)1297 private void finishTranscodingResult(int uid, String src, TranscodingSession session, 1298 CountDownLatch latch) { 1299 final StorageTranscodingSession finishedSession; 1300 1301 synchronized (mLock) { 1302 latch.countDown(); 1303 session.cancel(); 1304 1305 finishedSession = mStorageTranscodingSessions.remove(src); 1306 1307 switch (session.getResult()) { 1308 case TranscodingSession.RESULT_SUCCESS: 1309 mSuccessfulTranscodeSessions.put(finishedSession, false /* placeholder */); 1310 break; 1311 case TranscodingSession.RESULT_CANCELED: 1312 mCancelledTranscodeSessions.put(finishedSession, false /* placeholder */); 1313 break; 1314 case TranscodingSession.RESULT_ERROR: 1315 mErroredTranscodeSessions.put(finishedSession, false /* placeholder */); 1316 break; 1317 default: 1318 Log.w(TAG, "TranscodingSession.RESULT_NONE received for a finished session"); 1319 } 1320 } 1321 1322 logEvent("Transcoding end: " + src + ". Uid: " + uid, session); 1323 } 1324 updateTranscodeStatus(String path, int transcodeStatus)1325 private boolean updateTranscodeStatus(String path, int transcodeStatus) { 1326 final Uri uri = FileUtils.getContentUriForPath(path); 1327 // TODO(b/170465810): Replace this with matchUri when the code is refactored. 1328 final int match = LocalUriMatcher.FILES; 1329 final SQLiteQueryBuilder qb = mMediaProvider.getQueryBuilderForTranscoding(TYPE_UPDATE, 1330 match, uri, Bundle.EMPTY, null); 1331 final String[] selectionArgs = new String[]{path}; 1332 1333 ContentValues values = new ContentValues(); 1334 values.put(FileColumns._TRANSCODE_STATUS, transcodeStatus); 1335 final boolean success = qb.update(getDatabaseHelperForUri(uri), values, 1336 TRANSCODE_WHERE_CLAUSE, selectionArgs) == 1; 1337 if (!success) { 1338 Log.w(TAG, "Transcoding status update to: " + transcodeStatus + " failed for " + path); 1339 } 1340 return success; 1341 } 1342 deleteCachedTranscodeFile(long rowId)1343 public boolean deleteCachedTranscodeFile(long rowId) { 1344 return new File(mTranscodeDirectory, String.valueOf(rowId)).delete(); 1345 } 1346 getDatabaseHelperForUri(Uri uri)1347 private DatabaseHelper getDatabaseHelperForUri(Uri uri) { 1348 final DatabaseHelper helper; 1349 try { 1350 return mMediaProvider.getDatabaseForUriForTranscoding(uri); 1351 } catch (VolumeNotFoundException e) { 1352 throw new IllegalStateException("Volume not found while querying transcode path", e); 1353 } 1354 } 1355 1356 /** 1357 * @return given {@code projection} columns from database for given {@code path}. 1358 * Note that cursor might be empty if there is no database row or file is pending or trashed. 1359 * TODO(b/170465810): Optimize these queries by bypassing getQueryBuilder(). These queries are 1360 * always on Files table and doesn't have any dependency on calling package. i.e., query is 1361 * always called with callingPackage=self. 1362 */ 1363 @Nullable queryFileForTranscode(String path, String[] projection)1364 private Cursor queryFileForTranscode(String path, String[] projection) { 1365 final Uri uri = FileUtils.getContentUriForPath(path); 1366 // TODO(b/170465810): Replace this with matchUri when the code is refactored. 1367 final int match = LocalUriMatcher.FILES; 1368 final SQLiteQueryBuilder qb = mMediaProvider.getQueryBuilderForTranscoding(TYPE_QUERY, 1369 match, uri, Bundle.EMPTY, null); 1370 final String[] selectionArgs = new String[]{path}; 1371 1372 Bundle extras = new Bundle(); 1373 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_EXCLUDE); 1374 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_EXCLUDE); 1375 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, TRANSCODE_WHERE_CLAUSE); 1376 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs); 1377 return qb.query(getDatabaseHelperForUri(uri), projection, extras, null); 1378 } 1379 isTranscodeEnabled()1380 private boolean isTranscodeEnabled() { 1381 if (!IS_TRANSCODING_SUPPORTED) { 1382 return false; 1383 } 1384 1385 // If the user wants to override the default, respect that; otherwise use the DeviceConfig 1386 // which is filled with the values sent from server. 1387 if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) { 1388 return SystemProperties.getBoolean( 1389 TRANSCODE_ENABLED_SYS_PROP_KEY, /* defaultValue */ true); 1390 } 1391 1392 return mConfigStore.isTranscodeEnabled(); 1393 } 1394 shouldTranscodeDefault()1395 private boolean shouldTranscodeDefault() { 1396 // If the user wants to override the default, respect that; otherwise use the DeviceConfig 1397 // which is filled with the values sent from server. 1398 if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) { 1399 return SystemProperties.getBoolean( 1400 TRANSCODE_DEFAULT_SYS_PROP_KEY, /* defaultValue */ false); 1401 } 1402 1403 return mConfigStore.shouldTranscodeDefault(); 1404 } 1405 updateConfigs(boolean transcodeEnabled)1406 private void updateConfigs(boolean transcodeEnabled) { 1407 synchronized (mLock) { 1408 boolean isTranscodeEnabledChanged = transcodeEnabled != mIsTranscodeEnabled; 1409 1410 if (isTranscodeEnabledChanged) { 1411 Log.i(TAG, "Reloading transcode configs. transcodeEnabled: " + transcodeEnabled 1412 + ". lastTranscodeEnabled: " + mIsTranscodeEnabled); 1413 1414 mIsTranscodeEnabled = transcodeEnabled; 1415 parseTranscodeCompatManifest(); 1416 } 1417 } 1418 } 1419 parseTranscodeCompatManifest()1420 private void parseTranscodeCompatManifest() { 1421 synchronized (mLock) { 1422 // Clear the transcode_compat manifest before parsing. If transcode is disabled, 1423 // nothing will be parsed, effectively leaving the compat manifest empty. 1424 mAppCompatMediaCapabilities.clear(); 1425 if (!mIsTranscodeEnabled) { 1426 return; 1427 } 1428 1429 Set<String> stalePackages = getTranscodeCompatStale(); 1430 parseTranscodeCompatManifestFromResourceLocked(stalePackages); 1431 parseTranscodeCompatManifestFromDeviceConfigLocked(); 1432 } 1433 } 1434 1435 /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */ parseTranscodeCompatManifestFromDeviceConfigLocked()1436 private boolean parseTranscodeCompatManifestFromDeviceConfigLocked() { 1437 final List<String> manifest = mConfigStore.getTranscodeCompatManifest(); 1438 1439 if (manifest.isEmpty()) { 1440 Log.i(TAG, "Empty device config transcode compat manifest"); 1441 return false; 1442 } 1443 if ((manifest.size() % 2) != 0) { 1444 Log.w(TAG, "Uneven number of items in device config transcode compat manifest"); 1445 return false; 1446 } 1447 1448 String packageName = ""; 1449 int packageCompatValue; 1450 int i = 0; 1451 int count = 0; 1452 while (i < manifest.size() - 1) { 1453 try { 1454 packageName = manifest.get(i++); 1455 packageCompatValue = Integer.parseInt(manifest.get(i++)); 1456 synchronized (mLock) { 1457 // Lock is already held, explicitly hold again to make error prone happy 1458 mAppCompatMediaCapabilities.put(packageName, packageCompatValue); 1459 count++; 1460 } 1461 } catch (NumberFormatException e) { 1462 Log.w(TAG, "Failed to parse media capability from device config for package: " 1463 + packageName, e); 1464 } 1465 } 1466 1467 Log.i(TAG, "Parsed " + count + " packages from device config"); 1468 return count != 0; 1469 } 1470 1471 /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */ parseTranscodeCompatManifestFromResourceLocked(Set<String> stalePackages)1472 private boolean parseTranscodeCompatManifestFromResourceLocked(Set<String> stalePackages) { 1473 InputStream inputStream = mContext.getResources().openRawResource( 1474 R.raw.transcode_compat_manifest); 1475 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 1476 int count = 0; 1477 try { 1478 while (reader.ready()) { 1479 String line = reader.readLine(); 1480 String packageName = ""; 1481 int packageCompatValue; 1482 1483 if (line == null) { 1484 Log.w(TAG, "Unexpected null line while parsing transcode compat manifest"); 1485 continue; 1486 } 1487 1488 String[] lineValues = line.split(","); 1489 if (lineValues.length != 2) { 1490 Log.w(TAG, "Failed to read line while parsing transcode compat manifest"); 1491 continue; 1492 } 1493 try { 1494 packageName = lineValues[0]; 1495 packageCompatValue = Integer.parseInt(lineValues[1]); 1496 1497 if (stalePackages.contains(packageName)) { 1498 Log.i(TAG, "Skipping stale package in transcode compat manifest: " 1499 + packageName); 1500 continue; 1501 } 1502 1503 synchronized (mLock) { 1504 // Lock is already held, explicitly hold again to make error prone happy 1505 mAppCompatMediaCapabilities.put(packageName, packageCompatValue); 1506 count++; 1507 } 1508 } catch (NumberFormatException e) { 1509 Log.w(TAG, "Failed to parse media capability from resource for package: " 1510 + packageName, e); 1511 } 1512 } 1513 } catch (IOException e) { 1514 Log.w(TAG, "Failed to read transcode compat manifest", e); 1515 } 1516 1517 Log.i(TAG, "Parsed " + count + " packages from resource"); 1518 return count != 0; 1519 } 1520 getTranscodeCompatStale()1521 private Set<String> getTranscodeCompatStale() { 1522 Set<String> stalePackages = new ArraySet<>(); 1523 final List<String> staleConfig = mConfigStore.getTranscodeCompatStale(); 1524 1525 if (staleConfig.isEmpty()) { 1526 Log.i(TAG, "Empty transcode compat stale"); 1527 return stalePackages; 1528 } 1529 1530 for (String stalePackage : staleConfig) { 1531 stalePackages.add(stalePackage); 1532 } 1533 1534 int size = stalePackages.size(); 1535 Log.i(TAG, "Parsed " + size + " stale packages from device config"); 1536 return stalePackages; 1537 } 1538 dump(PrintWriter writer)1539 public void dump(PrintWriter writer) { 1540 writer.println("isTranscodeEnabled=" + isTranscodeEnabled()); 1541 writer.println("shouldTranscodeDefault=" + shouldTranscodeDefault()); 1542 1543 synchronized (mLock) { 1544 writer.println("mAppCompatMediaCapabilities=" + mAppCompatMediaCapabilities); 1545 writer.println("mStorageTranscodingSessions=" + mStorageTranscodingSessions); 1546 writer.println("mSupportedTranscodingRelativePaths=" + mSupportedRelativePaths); 1547 writer.println("mHasHdrPlugin=" + getHasHdrPlugin()); 1548 dumpFinishedSessions(writer); 1549 } 1550 } 1551 getSupportedRelativePaths()1552 public List<String> getSupportedRelativePaths() { 1553 return mSupportedRelativePaths; 1554 } 1555 dumpFinishedSessions(PrintWriter writer)1556 private void dumpFinishedSessions(PrintWriter writer) { 1557 synchronized (mLock) { 1558 writer.println("mSuccessfulTranscodeSessions=" + mSuccessfulTranscodeSessions.keySet()); 1559 1560 writer.println("mCancelledTranscodeSessions=" + mCancelledTranscodeSessions.keySet()); 1561 1562 writer.println("mErroredTranscodeSessions=" + mErroredTranscodeSessions.keySet()); 1563 } 1564 } 1565 logEvent(String event, @Nullable TranscodingSession session)1566 private static void logEvent(String event, @Nullable TranscodingSession session) { 1567 Log.d(TAG, event + (session == null ? "" : session)); 1568 } 1569 logVerbose(String message)1570 private static void logVerbose(String message) { 1571 if (DEBUG) { 1572 Log.v(TAG, message); 1573 } 1574 } 1575 1576 // We want to keep track of only the most recent [MAX_FINISHED_TRANSCODING_SESSION_STORE_COUNT] 1577 // finished transcoding sessions. createFinishedTranscodingSessionMap()1578 private static LinkedHashMap createFinishedTranscodingSessionMap() { 1579 return new LinkedHashMap<StorageTranscodingSession, Boolean>() { 1580 @Override 1581 protected boolean removeEldestEntry(Entry eldest) { 1582 return size() > MAX_FINISHED_TRANSCODING_SESSION_STORE_COUNT; 1583 } 1584 }; 1585 } 1586 1587 @VisibleForTesting 1588 static int getMyUid() { 1589 return MY_UID; 1590 } 1591 1592 private static class StorageTranscodingSession { 1593 private static final DateTimeFormatter DATE_FORMAT = 1594 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); 1595 1596 public final TranscodingSession session; 1597 public final CountDownLatch latch; 1598 private final String mSrcPath; 1599 private final String mDstPath; 1600 @GuardedBy("latch") 1601 private final Set<Integer> mBlockedUids = new ArraySet<>(); 1602 private final LocalDateTime mStartTime; 1603 @GuardedBy("latch") 1604 private LocalDateTime mFinishTime; 1605 @GuardedBy("latch") 1606 private boolean mHasAnr; 1607 @GuardedBy("latch") 1608 private int mFailureReason; 1609 @GuardedBy("latch") 1610 private int mErrorCode; 1611 1612 public StorageTranscodingSession(TranscodingSession session, CountDownLatch latch, 1613 String srcPath, String dstPath) { 1614 this.session = session; 1615 this.latch = latch; 1616 this.mSrcPath = srcPath; 1617 this.mDstPath = dstPath; 1618 this.mStartTime = LocalDateTime.now(); 1619 mErrorCode = TranscodingSession.ERROR_NONE; 1620 mFailureReason = TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN; 1621 } 1622 1623 public void addBlockedUid(int uid) { 1624 session.addClientUid(uid); 1625 } 1626 1627 public boolean isUidBlocked(int uid) { 1628 return session.getClientUids().contains(uid); 1629 } 1630 1631 public void setAnr() { 1632 synchronized (latch) { 1633 mHasAnr = true; 1634 } 1635 } 1636 1637 public boolean hasAnr() { 1638 synchronized (latch) { 1639 return mHasAnr; 1640 } 1641 } 1642 1643 public void notifyFinished(int failureReason, int errorCode) { 1644 synchronized (latch) { 1645 mFinishTime = LocalDateTime.now(); 1646 mFailureReason = failureReason; 1647 mErrorCode = errorCode; 1648 } 1649 } 1650 1651 @Override 1652 public String toString() { 1653 String startTime = mStartTime.format(DATE_FORMAT); 1654 String finishTime = "NONE"; 1655 String durationMs = "NONE"; 1656 boolean hasAnr; 1657 int failureReason; 1658 int errorCode; 1659 1660 synchronized (latch) { 1661 if (mFinishTime != null) { 1662 finishTime = mFinishTime.format(DATE_FORMAT); 1663 durationMs = String.valueOf(mStartTime.until(mFinishTime, ChronoUnit.MILLIS)); 1664 } 1665 hasAnr = mHasAnr; 1666 failureReason = mFailureReason; 1667 errorCode = mErrorCode; 1668 } 1669 1670 return String.format(Locale.ROOT, 1671 "<%s. Src: %s. Dst: %s. BlockedUids: %s. DurationMs: %sms" 1672 + ". Start: %s. Finish: %sms. HasAnr: %b. FailureReason: %d. ErrorCode: %d>", 1673 session.toString(), mSrcPath, mDstPath, session.getClientUids(), durationMs, 1674 startTime, finishTime, hasAnr, failureReason, errorCode); 1675 } 1676 } 1677 1678 private static class TranscodeUiNotifier { 1679 private static final int PROGRESS_MAX = 100; 1680 private static final int ALERT_DISMISS_DELAY_MS = 1000; 1681 private static final int SHOW_PROGRESS_THRESHOLD_TIME_MS = 1000; 1682 private static final String TRANSCODE_ALERT_CHANNEL_ID = "native_transcode_alert_channel"; 1683 private static final String TRANSCODE_PROGRESS_CHANNEL_ID = 1684 "native_transcode_progress_channel"; 1685 1686 // Related to notification settings 1687 private static final String TRANSCODE_NOTIFICATION_SYS_PROP_KEY = 1688 "persist.sys.fuse.transcode_notification"; 1689 private static final boolean NOTIFICATION_ALLOWED_DEFAULT_VALUE = false; 1690 1691 private final Context mContext; 1692 private final NotificationManagerCompat mNotificationManager; 1693 private final PackageManager mPackageManager; 1694 // Builder for creating alert notifications. 1695 private final NotificationCompat.Builder mAlertBuilder; 1696 // Builder for creating progress notifications. 1697 private final NotificationCompat.Builder mProgressBuilder; 1698 private final SessionTiming mSessionTiming; 1699 1700 TranscodeUiNotifier(Context context, SessionTiming sessionTiming) { 1701 mContext = context; 1702 mNotificationManager = NotificationManagerCompat.from(context); 1703 mPackageManager = context.getPackageManager(); 1704 createAlertNotificationChannel(context); 1705 createProgressNotificationChannel(context); 1706 mAlertBuilder = createAlertNotificationBuilder(context); 1707 mProgressBuilder = createProgressNotificationBuilder(context); 1708 mSessionTiming = sessionTiming; 1709 } 1710 1711 void start(TranscodingSession session, String filePath) { 1712 if (!notificationEnabled()) { 1713 return; 1714 } 1715 ForegroundThread.getHandler().post(() -> { 1716 mAlertBuilder.setContentTitle(getString(mContext, 1717 R.string.transcode_processing_started)); 1718 mAlertBuilder.setContentText(FileUtils.extractDisplayName(filePath)); 1719 final int notificationId = session.getSessionId(); 1720 mNotificationManager.notify(notificationId, mAlertBuilder.build()); 1721 }); 1722 } 1723 1724 void stop(TranscodingSession session, String filePath) { 1725 if (!notificationEnabled()) { 1726 return; 1727 } 1728 endSessionWithMessage(session, filePath, getResultMessageForSession(mContext, session)); 1729 } 1730 1731 void denied(int uid) { 1732 String appName = getAppName(uid); 1733 if (appName == null) { 1734 Log.w(TAG, "Not showing denial, no app name "); 1735 return; 1736 } 1737 1738 final Handler handler = ForegroundThread.getHandler(); 1739 handler.post(() -> { 1740 Toast.makeText(mContext, 1741 mContext.getResources().getString(R.string.transcode_denied, appName), 1742 Toast.LENGTH_LONG).show(); 1743 }); 1744 } 1745 1746 void setProgress(TranscodingSession session, String filePath, 1747 @IntRange(from = 0, to = PROGRESS_MAX) int progress) { 1748 if (!notificationEnabled()) { 1749 return; 1750 } 1751 if (shouldShowProgress(session)) { 1752 mProgressBuilder.setContentText(FileUtils.extractDisplayName(filePath)); 1753 mProgressBuilder.setProgress(PROGRESS_MAX, progress, /* indeterminate= */ false); 1754 final int notificationId = session.getSessionId(); 1755 mNotificationManager.notify(notificationId, mProgressBuilder.build()); 1756 } 1757 } 1758 1759 private boolean shouldShowProgress(TranscodingSession session) { 1760 return (System.currentTimeMillis() - mSessionTiming.getSessionStartTime(session)) 1761 > SHOW_PROGRESS_THRESHOLD_TIME_MS; 1762 } 1763 1764 private void endSessionWithMessage(TranscodingSession session, String filePath, 1765 String message) { 1766 final Handler handler = ForegroundThread.getHandler(); 1767 handler.post(() -> { 1768 mAlertBuilder.setContentTitle(message); 1769 mAlertBuilder.setContentText(FileUtils.extractDisplayName(filePath)); 1770 final int notificationId = session.getSessionId(); 1771 mNotificationManager.notify(notificationId, mAlertBuilder.build()); 1772 // Auto-dismiss after a delay. 1773 handler.postDelayed(() -> mNotificationManager.cancel(notificationId), 1774 ALERT_DISMISS_DELAY_MS); 1775 }); 1776 } 1777 1778 private String getAppName(int uid) { 1779 String name = mPackageManager.getNameForUid(uid); 1780 if (name == null) { 1781 Log.w(TAG, "Couldn't find name"); 1782 return null; 1783 } 1784 1785 final ApplicationInfo aInfo; 1786 try { 1787 aInfo = mPackageManager.getApplicationInfo(name, 0); 1788 } catch (PackageManager.NameNotFoundException e) { 1789 Log.w(TAG, "unable to look up package name", e); 1790 return null; 1791 } 1792 1793 // If the label contains new line characters it may push the security 1794 // message below the fold of the dialog. Labels shouldn't have new line 1795 // characters anyways, so we just delete all of the newlines (if there are any). 1796 return aInfo.loadSafeLabel(mPackageManager, MAX_APP_NAME_SIZE_PX, 1797 TextUtils.SAFE_STRING_FLAG_SINGLE_LINE).toString(); 1798 } 1799 1800 private static String getString(Context context, int resourceId) { 1801 return context.getResources().getString(resourceId); 1802 } 1803 1804 private static void createAlertNotificationChannel(Context context) { 1805 NotificationChannel channel = new NotificationChannel(TRANSCODE_ALERT_CHANNEL_ID, 1806 getString(context, R.string.transcode_alert_channel), 1807 NotificationManager.IMPORTANCE_HIGH); 1808 NotificationManager notificationManager = context.getSystemService( 1809 NotificationManager.class); 1810 notificationManager.createNotificationChannel(channel); 1811 } 1812 1813 private static void createProgressNotificationChannel(Context context) { 1814 NotificationChannel channel = new NotificationChannel(TRANSCODE_PROGRESS_CHANNEL_ID, 1815 getString(context, R.string.transcode_progress_channel), 1816 NotificationManager.IMPORTANCE_LOW); 1817 NotificationManager notificationManager = context.getSystemService( 1818 NotificationManager.class); 1819 notificationManager.createNotificationChannel(channel); 1820 } 1821 1822 private static NotificationCompat.Builder createAlertNotificationBuilder(Context context) { 1823 NotificationCompat.Builder builder = new NotificationCompat.Builder(context, 1824 TRANSCODE_ALERT_CHANNEL_ID); 1825 builder.setAutoCancel(false) 1826 .setOngoing(true) 1827 .setSmallIcon(R.drawable.thumb_clip); 1828 return builder; 1829 } 1830 1831 private static NotificationCompat.Builder createProgressNotificationBuilder( 1832 Context context) { 1833 NotificationCompat.Builder builder = new NotificationCompat.Builder(context, 1834 TRANSCODE_PROGRESS_CHANNEL_ID); 1835 builder.setAutoCancel(false) 1836 .setOngoing(true) 1837 .setContentTitle(getString(context, R.string.transcode_processing)) 1838 .setSmallIcon(R.drawable.thumb_clip); 1839 return builder; 1840 } 1841 1842 private static String getResultMessageForSession(Context context, 1843 TranscodingSession session) { 1844 switch (session.getResult()) { 1845 case TranscodingSession.RESULT_CANCELED: 1846 return getString(context, R.string.transcode_processing_cancelled); 1847 case TranscodingSession.RESULT_ERROR: 1848 return getString(context, R.string.transcode_processing_error); 1849 case TranscodingSession.RESULT_SUCCESS: 1850 return getString(context, R.string.transcode_processing_success); 1851 default: 1852 return getString(context, R.string.transcode_processing_error); 1853 } 1854 } 1855 1856 private static boolean notificationEnabled() { 1857 return SystemProperties.getBoolean(TRANSCODE_NOTIFICATION_SYS_PROP_KEY, 1858 NOTIFICATION_ALLOWED_DEFAULT_VALUE); 1859 } 1860 } 1861 1862 private static class TranscodeDenialController implements OnUidImportanceListener { 1863 private final int mMaxDurationMs; 1864 private final ActivityManager mActivityManager; 1865 private final TranscodeUiNotifier mUiNotifier; 1866 private final Object mLock = new Object(); 1867 @GuardedBy("mLock") 1868 private final Set<Integer> mActiveDeniedUids = new ArraySet<>(); 1869 @GuardedBy("mLock") 1870 private final Set<Integer> mDroppedUids = new ArraySet<>(); 1871 1872 TranscodeDenialController(ActivityManager activityManager, TranscodeUiNotifier uiNotifier, 1873 int maxDurationMs) { 1874 mActivityManager = activityManager; 1875 mUiNotifier = uiNotifier; 1876 mMaxDurationMs = maxDurationMs; 1877 } 1878 1879 @Override 1880 public void onUidImportance(int uid, int importance) { 1881 if (importance != IMPORTANCE_FOREGROUND) { 1882 synchronized (mLock) { 1883 if (mActiveDeniedUids.remove(uid) && mActiveDeniedUids.isEmpty()) { 1884 // Stop the uid listener if this is the last uid triggering a denial UI 1885 mActivityManager.removeOnUidImportanceListener(this); 1886 } 1887 } 1888 } 1889 } 1890 1891 /** @return {@code true} if file access should be denied, {@code false} otherwise */ 1892 boolean checkFileAccess(int uid, long durationMs) { 1893 boolean shouldDeny = false; 1894 synchronized (mLock) { 1895 shouldDeny = durationMs > mMaxDurationMs || mDroppedUids.contains(uid); 1896 } 1897 1898 if (!shouldDeny) { 1899 // Nothing to do 1900 return false; 1901 } 1902 1903 synchronized (mLock) { 1904 if (!mActiveDeniedUids.contains(uid) 1905 && mActivityManager.getUidImportance(uid) == IMPORTANCE_FOREGROUND) { 1906 // Show UI for the first denial while foreground 1907 mUiNotifier.denied(uid); 1908 1909 if (mActiveDeniedUids.isEmpty()) { 1910 // Start a uid listener if this is the first uid triggering a denial UI 1911 mActivityManager.addOnUidImportanceListener(this, IMPORTANCE_FOREGROUND); 1912 } 1913 mActiveDeniedUids.add(uid); 1914 } 1915 } 1916 return true; 1917 } 1918 1919 void onTranscodingDropped(int uid) { 1920 synchronized (mLock) { 1921 mDroppedUids.add(uid); 1922 } 1923 // Notify about file access, so we might show a denial UI 1924 checkFileAccess(uid, 0 /* duration */); 1925 } 1926 } 1927 1928 private static final class SessionTiming { 1929 // This should be accessed only in foreground thread. 1930 private final SparseArray<Long> mSessionStartTimes = new SparseArray<>(); 1931 1932 // Call this only in foreground thread. 1933 private long getSessionStartTime(MediaTranscodingManager.TranscodingSession session) { 1934 return mSessionStartTimes.get(session.getSessionId()); 1935 } 1936 1937 private void logSessionStart(MediaTranscodingManager.TranscodingSession session) { 1938 ForegroundThread.getHandler().post( 1939 () -> mSessionStartTimes.append(session.getSessionId(), 1940 System.currentTimeMillis())); 1941 } 1942 1943 private void logSessionEnd(MediaTranscodingManager.TranscodingSession session) { 1944 ForegroundThread.getHandler().post( 1945 () -> mSessionStartTimes.remove(session.getSessionId())); 1946 } 1947 } 1948 } 1949