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