Merge "Support transcode path RRO for OEMs" into sc-mainline-prod
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 1e2a82b..53f86cd 100755
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -247,14 +247,16 @@
 
 /* Single FUSE mount */
 struct fuse {
-    explicit fuse(const std::string& _path, ino_t _ino)
+    explicit fuse(const std::string& _path, const ino_t _ino,
+                  const std::vector<string>& _supported_transcoding_relative_paths)
         : path(_path),
           tracker(mediaprovider::fuse::NodeTracker(&lock)),
           root(node::CreateRoot(_path, &lock, _ino, &tracker)),
           mp(0),
           zero_addr(0),
           disable_dentry_cache(false),
-          passthrough(false) {}
+          passthrough(false),
+          supported_transcoding_relative_paths(_supported_transcoding_relative_paths) {}
 
     inline bool IsRoot(const node* node) const { return node == root; }
 
@@ -287,6 +289,22 @@
         return node::ToInode(node);
     }
 
+    inline bool IsTranscodeSupportedPath(const string& path) {
+        // Keep in sync with MediaProvider#supportsTranscode
+        if (!android::base::EndsWithIgnoreCase(path, ".mp4")) {
+            return false;
+        }
+
+        const std::string& base_path = GetEffectiveRootPath() + "/";
+        for (const std::string& relative_path : supported_transcoding_relative_paths) {
+            if (android::base::StartsWithIgnoreCase(path, base_path + relative_path)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     std::recursive_mutex lock;
     const string path;
     // The Inode tracker associated with this FUSE instance.
@@ -314,6 +332,7 @@
     std::atomic_bool passthrough;
     // FUSE device id.
     std::atomic_uint dev;
+    const std::vector<string> supported_transcoding_relative_paths;
 };
 
 enum class FuseOp { lookup, readdir, mknod, mkdir, create };
@@ -437,13 +456,6 @@
             path, fuse->GetTransformsDir() + "/" + TRANSFORM_SYNTHETIC_DIR);
 }
 
-static inline bool is_transcode_supported_path(const string& path, struct fuse* fuse) {
-    // Keep in sync with MediaProvider#supportsTranscode
-    return android::base::EndsWithIgnoreCase(path, ".mp4") &&
-           android::base::StartsWithIgnoreCase(path,
-                                               fuse->GetEffectiveRootPath() + "/dcim/camera/");
-}
-
 static inline bool is_transforms_dir_path(const string& path, struct fuse* fuse) {
     return android::base::StartsWithIgnoreCase(path, fuse->GetTransformsDir());
 }
@@ -484,7 +496,7 @@
         return std::make_unique<mediaprovider::fuse::FileLookupResult>(0, 0, 0, true, false, "");
     }
 
-    if (!synthetic_path && !is_transcode_supported_path(path, fuse)) {
+    if (!synthetic_path && !fuse->IsTranscodeSupportedPath(path)) {
         // Transforms are only supported for synthetic or transcode-supported paths
         return std::make_unique<mediaprovider::fuse::FileLookupResult>(0, 0, 0, true, false, "");
     }
@@ -2040,7 +2052,8 @@
     return active.load(std::memory_order_acquire);
 }
 
-void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path) {
+void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path,
+                       const std::vector<std::string>& supported_transcoding_relative_paths) {
     android::base::SetDefaultTag(LOG_TAG);
 
     struct fuse_args args;
@@ -2065,7 +2078,7 @@
         return;
     }
 
-    struct fuse fuse_default(path, stat.st_ino);
+    struct fuse fuse_default(path, stat.st_ino, supported_transcoding_relative_paths);
     fuse_default.mp = &mp;
     // fuse_default is stack allocated, but it's safe to save it as an instance variable because
     // this method blocks and FuseDaemon#active tells if we are currently blocking
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index f7c5614..3164830 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -37,7 +37,8 @@
     /**
      * Start the FUSE daemon loop that will handle filesystem calls.
      */
-    void Start(android::base::unique_fd fd, const std::string& path);
+    void Start(android::base::unique_fd fd, const std::string& path,
+               const std::vector<std::string>& supported_transcoding_relative_paths);
 
     /**
      * Checks if the FUSE daemon is started.
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
index d67123d..46ed041 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -17,6 +17,7 @@
 // Need to use LOGE_EX.
 #define LOG_TAG "FuseDaemonJNI"
 
+#include <nativehelper/scoped_local_ref.h>
 #include <nativehelper/scoped_utf_chars.h>
 
 #include <string>
@@ -32,14 +33,39 @@
 constexpr const char* CLASS_NAME = "com/android/providers/media/fuse/FuseDaemon";
 static jclass gFuseDaemonClass;
 
+static std::vector<std::string> get_supported_transcoding_relative_paths(
+        JNIEnv* env, jobjectArray java_supported_transcoding_relative_paths) {
+    ScopedLocalRef<jobjectArray> j_transcoding_relative_paths(
+            env, java_supported_transcoding_relative_paths);
+    std::vector<std::string> transcoding_relative_paths;
+
+    const int transcoding_relative_paths_count =
+            env->GetArrayLength(j_transcoding_relative_paths.get());
+    for (int i = 0; i < transcoding_relative_paths_count; i++) {
+        ScopedLocalRef<jstring> j_ref_relative_path(
+                env, (jstring)env->GetObjectArrayElement(j_transcoding_relative_paths.get(), i));
+        ScopedUtfChars j_utf_relative_path(env, j_ref_relative_path.get());
+        const char* relative_path = j_utf_relative_path.c_str();
+
+        if (relative_path) {
+            transcoding_relative_paths.push_back(relative_path);
+        } else {
+            LOG(ERROR) << "Error reading supported transcoding relative path at index: " << i;
+        }
+    }
+
+    return transcoding_relative_paths;
+}
+
 jlong com_android_providers_media_FuseDaemon_new(JNIEnv* env, jobject self,
                                                  jobject media_provider) {
     LOG(DEBUG) << "Creating the FUSE daemon...";
     return reinterpret_cast<jlong>(new fuse::FuseDaemon(env, media_provider));
 }
 
-void com_android_providers_media_FuseDaemon_start(JNIEnv* env, jobject self, jlong java_daemon,
-                                                  jint fd, jstring java_path) {
+void com_android_providers_media_FuseDaemon_start(
+        JNIEnv* env, jobject self, jlong java_daemon, jint fd, jstring java_path,
+        jobjectArray java_supported_transcoding_relative_paths) {
     LOG(DEBUG) << "Starting the FUSE daemon...";
     fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
 
@@ -50,7 +76,11 @@
         return;
     }
 
-    daemon->Start(std::move(ufd), utf_chars_path.c_str());
+    const std::vector<std::string>& transcoding_relative_paths =
+            get_supported_transcoding_relative_paths(env,
+                    java_supported_transcoding_relative_paths);
+
+    daemon->Start(std::move(ufd), utf_chars_path.c_str(), transcoding_relative_paths);
 }
 
 bool com_android_providers_media_FuseDaemon_is_started(JNIEnv* env, jobject self,
@@ -127,7 +157,7 @@
 const JNINativeMethod methods[] = {
         {"native_new", "(Lcom/android/providers/media/MediaProvider;)J",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_new)},
-        {"native_start", "(JILjava/lang/String;)V",
+        {"native_start", "(JILjava/lang/String;[Ljava/lang/String;)V",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_start)},
         {"native_delete", "(J)V",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete)},
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..d640cb7
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <string-array name="config_supported_transcoding_relative_paths" translatable="false">
+      <item>DCIM/Camera/</item>
+    </string-array>
+</resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
new file mode 100644
index 0000000..6b89bf4
--- /dev/null
+++ b/res/values/overlayable.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <overlayable name="MediaProviderConfig">
+        <policy type="product|system|vendor">
+            <item type="array" name="config_supported_transcoding_relative_paths"/>
+        </policy>
+    </overlayable>
+</resources>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 0ef9878..52acdad 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -9416,6 +9416,10 @@
         }
     }
 
+    public List<String> getSupportedTranscodingRelativePaths() {
+        return mTranscodeHelper.getSupportedRelativePaths();
+    }
+
     /**
      * Creating a new method for Transcoding to avoid any merge conflicts.
      * TODO(b/170465810): Remove this when the code is refactored.
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index 488c40d..5e2b13b 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -19,6 +19,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import java.io.PrintWriter;
+import java.util.List;
 
 /** Interface over MediaTranscodeManager access */
 public interface TranscodeHelper {
@@ -43,4 +44,6 @@
     public boolean deleteCachedTranscodeFile(long rowId);
 
     public void dump(PrintWriter writer);
+
+    public List<String> getSupportedRelativePaths();
 }
diff --git a/src/com/android/providers/media/TranscodeHelperImpl.java b/src/com/android/providers/media/TranscodeHelperImpl.java
index 8017a59..a3e2731 100644
--- a/src/com/android/providers/media/TranscodeHelperImpl.java
+++ b/src/com/android/providers/media/TranscodeHelperImpl.java
@@ -49,6 +49,8 @@
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.Property;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
 import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.media.ApplicationMediaCapabilities;
@@ -109,7 +111,10 @@
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -225,6 +230,7 @@
     private final StorageManager mStorageManager;
     private final ActivityManager mActivityManager;
     private final File mTranscodeDirectory;
+    private final List<String> mSupportedRelativePaths;
     @GuardedBy("mLock")
     private UUID mTranscodeVolumeUuid;
 
@@ -276,6 +282,8 @@
                         MAX_TRANSCODE_DURATION_MS);
         mTranscodeDenialController = new TranscodeDenialController(mActivityManager,
                 mTranscodingUiNotifier, maxTranscodeDurationMs);
+        mSupportedRelativePaths = verifySupportedRelativePaths(getStringArrayConfig(
+                        R.array.config_supported_transcoding_relative_paths));
 
         parseTranscodeCompatManifest();
         // The storage namespace is a boot namespace so we actually don't expect this to be changed
@@ -803,14 +811,48 @@
     }
 
     public boolean supportsTranscode(String path) {
-        File file = new File(path);
-        String name = file.getName();
-        final String cameraRelativePath =
-                String.format("%s/%s/", Environment.DIRECTORY_DCIM, DIRECTORY_CAMERA);
+        final File file = new File(path);
+        final String name = file.getName();
+        final String relativePath = FileUtils.extractRelativePath(path);
 
-        return !isTranscodeFile(path) && name.toLowerCase(Locale.ROOT).endsWith(".mp4")
-                && path.startsWith("/storage/emulated/")
-                && cameraRelativePath.equalsIgnoreCase(FileUtils.extractRelativePath(path));
+        if (isTranscodeFile(path) || !name.toLowerCase(Locale.ROOT).endsWith(".mp4")
+                || !path.startsWith("/storage/emulated/")) {
+            return false;
+        }
+
+        for (String supportedRelativePath : mSupportedRelativePaths) {
+            if (supportedRelativePath.equalsIgnoreCase(relativePath)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static List<String> verifySupportedRelativePaths(List<String> relativePaths) {
+        final List<String> verifiedPaths = new ArrayList<>();
+        final String lowerCaseDcimDir = Environment.DIRECTORY_DCIM.toLowerCase(Locale.ROOT) + "/";
+
+        for (String path : relativePaths) {
+            if (path.toLowerCase(Locale.ROOT).startsWith(lowerCaseDcimDir) && path.endsWith("/")) {
+                verifiedPaths.add(path);
+            } else {
+                Log.w(TAG, "Transcoding relative path must be a descendant of DCIM/ and end with"
+                        + " '/'. Ignoring: " + path);
+            }
+        }
+
+        return verifiedPaths;
+    }
+
+    private List<String> getStringArrayConfig(int resId) {
+        final Resources res = mContext.getResources();
+        try {
+            final String[] configValue = res.getStringArray(resId);
+            return Arrays.asList(configValue);
+        } catch (NotFoundException e) {
+            return new ArrayList<String>();
+        }
     }
 
     private Optional<Boolean> checkAppCompatSupport(int uid, int fileFlags) {
@@ -1458,11 +1500,16 @@
         synchronized (mLock) {
             writer.println("mAppCompatMediaCapabilities=" + mAppCompatMediaCapabilities);
             writer.println("mStorageTranscodingSessions=" + mStorageTranscodingSessions);
+            writer.println("mSupportedTranscodingRelativePaths=" + mSupportedRelativePaths);
 
             dumpFinishedSessions(writer);
         }
     }
 
+    public List<String> getSupportedRelativePaths() {
+        return mSupportedRelativePaths;
+    }
+
     private void dumpFinishedSessions(PrintWriter writer) {
         synchronized (mLock) {
             writer.println("mSuccessfulTranscodeSessions=" + mSuccessfulTranscodeSessions.keySet());
diff --git a/src/com/android/providers/media/TranscodeHelperNoOp.java b/src/com/android/providers/media/TranscodeHelperNoOp.java
index 355f21d..ee4549b 100644
--- a/src/com/android/providers/media/TranscodeHelperNoOp.java
+++ b/src/com/android/providers/media/TranscodeHelperNoOp.java
@@ -20,6 +20,9 @@
 import android.os.Bundle;
 import java.io.PrintWriter;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * No-op transcode helper to avoid loading MediaTranscodeManager classes in Android R
  */
@@ -57,4 +60,8 @@
     }
 
     public void dump(PrintWriter writer) {}
+
+    public List<String> getSupportedRelativePaths() {
+        return new ArrayList<String>();
+    }
 }
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index 1f8f3cd..956ae5d 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -70,8 +70,10 @@
                 // We only use the upperFileSystemPath because the media process is mounted as
                 // REMOUNT_MODE_PASS_THROUGH which guarantees that all /storage paths are bind
                 // mounts of the lower filesystem.
+                final String[] supportedTranscodingRelativePaths =
+                        mediaProvider.getSupportedTranscodingRelativePaths().toArray(new String[0]);
                 FuseDaemon daemon = new FuseDaemon(mediaProvider, this, deviceFd, sessionId,
-                        upperFileSystemPath.getPath());
+                        upperFileSystemPath.getPath(), supportedTranscodingRelativePaths);
                 daemon.start();
                 sFuseDaemons.put(sessionId, daemon);
             }
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 1175249..0c9ca85 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -40,18 +40,22 @@
     private final MediaProvider mMediaProvider;
     private final int mFuseDeviceFd;
     private final String mPath;
+    private final String[] mSupportedTranscodingRelativePaths;
     private final ExternalStorageServiceImpl mService;
     @GuardedBy("mLock")
     private long mPtr;
 
     public FuseDaemon(@NonNull MediaProvider mediaProvider,
             @NonNull ExternalStorageServiceImpl service, @NonNull ParcelFileDescriptor fd,
-            @NonNull String sessionId, @NonNull String path) {
+            @NonNull String sessionId, @NonNull String path,
+            String[] supportedTranscodingRelativePaths) {
         mMediaProvider = Objects.requireNonNull(mediaProvider);
         mService = Objects.requireNonNull(service);
         setName(Objects.requireNonNull(sessionId));
         mFuseDeviceFd = Objects.requireNonNull(fd).detachFd();
         mPath = Objects.requireNonNull(path);
+        mSupportedTranscodingRelativePaths
+                = Objects.requireNonNull(supportedTranscodingRelativePaths);
     }
 
     /** Starts a FUSE session. Does not return until the lower filesystem is unmounted. */
@@ -67,7 +71,7 @@
         }
 
         Log.i(TAG, "Starting thread for " + getName() + " ...");
-        native_start(ptr, mFuseDeviceFd, mPath); // Blocks
+        native_start(ptr, mFuseDeviceFd, mPath, mSupportedTranscodingRelativePaths); // Blocks
         Log.i(TAG, "Exiting thread for " + getName() + " ...");
 
         synchronized (mLock) {
@@ -180,7 +184,8 @@
     private native long native_new(MediaProvider mediaProvider);
 
     // Takes ownership of the passed in file descriptor!
-    private native void native_start(long daemon, int deviceFd, String path);
+    private native void native_start(long daemon, int deviceFd, String path,
+            String[] supportedTranscodingRelativePaths);
 
     private native void native_delete(long daemon);
     private native boolean native_should_open_with_fuse(long daemon, String path, boolean readLock,