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