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