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