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