Migrate MTP service into separate project.

As part of getting MediaProvider to compile against supported APIs,
we're moving MTP related logic into its own repository.

Bug: 135340257
Test: manual
Change-Id: I437d2b7f52c0ecbb54b992f3f4eedc73f23729c8
diff --git a/Android.bp b/Android.bp
index db0eabe..b01f444 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,6 +1,9 @@
 android_app {
     name: "MtpService",
 
+    static_libs: [
+        "androidx.appcompat_appcompat",
+    ],
     srcs: ["src/**/*.java"],
     platform_apis: true,
     certificate: "media",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c0a59b3..6f4b75f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,9 +3,14 @@
           package="com.android.mtp"
           android:sharedUserId="android.media">
     <uses-feature android:name="android.hardware.usb.host" />
+    <uses-permission android:name="android.permission.ACCESS_MTP" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.MANAGE_USB" />
-    <application android:label="@string/app_label">
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <application
+            android:process="android.process.media"
+            android:label="@string/app_label"
+            android:allowBackup="false">
         <provider
             android:name=".MtpDocumentsProvider"
             android:authorities="com.android.mtp.documents"
@@ -39,5 +44,16 @@
             <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                        android:resource="@xml/device_filter" />
         </receiver>
+
+        <receiver android:name=".MtpReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.hardware.usb.action.USB_STATE" />
+            </intent-filter>
+        </receiver>
+
+        <service android:name="MtpService" />
     </application>
 </manifest>
diff --git a/src/com/android/mtp/MtpReceiver.java b/src/com/android/mtp/MtpReceiver.java
new file mode 100644
index 0000000..cd6ad16
--- /dev/null
+++ b/src/com/android/mtp/MtpReceiver.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.mtp;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+
+public class MtpReceiver extends BroadcastReceiver {
+    private static final String TAG = MtpReceiver.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+            final Intent usbState = context.registerReceiver(
+                    null, new IntentFilter(UsbManager.ACTION_USB_STATE));
+            if (usbState != null) {
+                handleUsbState(context, usbState);
+            }
+        } else if (UsbManager.ACTION_USB_STATE.equals(action)) {
+            handleUsbState(context, intent);
+        }
+    }
+
+    private void handleUsbState(Context context, Intent intent) {
+        Bundle extras = intent.getExtras();
+        boolean configured = extras.getBoolean(UsbManager.USB_CONFIGURED);
+        boolean connected = extras.getBoolean(UsbManager.USB_CONNECTED);
+        boolean mtpEnabled = extras.getBoolean(UsbManager.USB_FUNCTION_MTP);
+        boolean ptpEnabled = extras.getBoolean(UsbManager.USB_FUNCTION_PTP);
+        boolean unlocked = extras.getBoolean(UsbManager.USB_DATA_UNLOCKED);
+        boolean isCurrentUser = UserHandle.myUserId() == ActivityManager.getCurrentUser();
+
+        if (configured && (mtpEnabled || ptpEnabled)) {
+            if (!isCurrentUser)
+                return;
+            intent = new Intent(context, MtpService.class);
+            intent.putExtra(UsbManager.USB_DATA_UNLOCKED, unlocked);
+            if (ptpEnabled) {
+                intent.putExtra(UsbManager.USB_FUNCTION_PTP, true);
+            }
+            if (DEBUG) { Log.d(TAG, "handleUsbState startService"); }
+            context.startService(intent);
+        } else if (!connected || !(mtpEnabled || ptpEnabled)) {
+            // Only unbind if disconnected or disabled.
+            boolean status = context.stopService(new Intent(context, MtpService.class));
+            if (DEBUG) { Log.d(TAG, "handleUsbState stopService status=" + status); }
+        }
+    }
+}
diff --git a/src/com/android/mtp/MtpService.java b/src/com/android/mtp/MtpService.java
new file mode 100644
index 0000000..0b6e1ab
--- /dev/null
+++ b/src/com/android/mtp/MtpService.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.mtp;
+
+import android.app.ActivityManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.IUsbManager;
+import android.hardware.usb.UsbManager;
+import android.mtp.MtpDatabase;
+import android.mtp.MtpServer;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.storage.StorageEventListener;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.util.HashMap;
+
+/**
+ * The singleton service backing instances of MtpServer that are started for the foreground user.
+ * The service has the responsibility of retrieving user storage information and managing server
+ * lifetime.
+ */
+public class MtpService extends Service {
+    private static final String TAG = "MtpService";
+    private static final boolean LOGD = false;
+
+    // We restrict PTP to these subdirectories
+    private static final String[] PTP_DIRECTORIES = new String[] {
+        Environment.DIRECTORY_DCIM,
+        Environment.DIRECTORY_PICTURES,
+    };
+
+    private final StorageEventListener mStorageEventListener = new StorageEventListener() {
+        @Override
+        public void onStorageStateChanged(String path, String oldState, String newState) {
+            synchronized (MtpService.this) {
+                Log.d(TAG, "onStorageStateChanged " + path + " " + oldState + " -> " + newState);
+                if (Environment.MEDIA_MOUNTED.equals(newState)) {
+                    for (int i = 0; i < mVolumes.length; i++) {
+                        StorageVolume volume = mVolumes[i];
+                        if (volume.getPath().equals(path)) {
+                            mVolumeMap.put(path, volume);
+                            if (mUnlocked && (volume.isPrimary() || !mPtpMode)) {
+                                addStorage(volume);
+                            }
+                            break;
+                        }
+                    }
+                } else if (Environment.MEDIA_MOUNTED.equals(oldState)) {
+                    if (mVolumeMap.containsKey(path)) {
+                        removeStorage(mVolumeMap.remove(path));
+                    }
+                }
+            }
+        }
+    };
+
+    /**
+     * Static state of MtpServer. MtpServer opens FD for MTP driver internally and we cannot open
+     * multiple MtpServer at the same time. The static field used to handle the case where MtpServer
+     * lives beyond the lifetime of MtpService.
+     *
+     * Lock MtpService.this before locking MtpService.class if needed. Otherwise it goes to
+     * deadlock.
+     */
+    @GuardedBy("MtpService.class")
+    private static ServerHolder sServerHolder;
+
+    private StorageManager mStorageManager;
+
+    @GuardedBy("this")
+    private boolean mUnlocked;
+    @GuardedBy("this")
+    private boolean mPtpMode;
+
+    // A map of user volumes that are currently mounted.
+    @GuardedBy("this")
+    private HashMap<String, StorageVolume> mVolumeMap;
+
+    // All user volumes in existence, in any state.
+    @GuardedBy("this")
+    private StorageVolume[] mVolumes;
+
+    @Override
+    public void onCreate() {
+        mVolumes = StorageManager.getVolumeList(getUserId(), 0);
+        mVolumeMap = new HashMap<>();
+
+        mStorageManager = this.getSystemService(StorageManager.class);
+        mStorageManager.registerListener(mStorageEventListener);
+    }
+
+    @Override
+    public void onDestroy() {
+        mStorageManager.unregisterListener(mStorageEventListener);
+        synchronized (MtpService.class) {
+            if (sServerHolder != null) {
+                sServerHolder.database.setServer(null);
+            }
+        }
+    }
+
+    @Override
+    public synchronized int onStartCommand(Intent intent, int flags, int startId) {
+        mUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false);
+        mPtpMode = intent.getBooleanExtra(UsbManager.USB_FUNCTION_PTP, false);
+
+        for (StorageVolume v : mVolumes) {
+            if (v.getState().equals(Environment.MEDIA_MOUNTED)) {
+                mVolumeMap.put(v.getPath(), v);
+            }
+        }
+        String[] subdirs = null;
+        if (mPtpMode) {
+            Environment.UserEnvironment env = new Environment.UserEnvironment(getUserId());
+            int count = PTP_DIRECTORIES.length;
+            subdirs = new String[count];
+            for (int i = 0; i < count; i++) {
+                File file = env.buildExternalStoragePublicDirs(PTP_DIRECTORIES[i])[0];
+                // make sure this directory exists
+                file.mkdirs();
+                subdirs[i] = file.getName();
+            }
+        }
+        final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
+        startServer(primary, subdirs);
+        return START_REDELIVER_INTENT;
+    }
+
+    private synchronized void startServer(StorageVolume primary, String[] subdirs) {
+        if (!(UserHandle.myUserId() == ActivityManager.getCurrentUser())) {
+            return;
+        }
+        synchronized (MtpService.class) {
+            if (sServerHolder != null) {
+                if (LOGD) {
+                    Log.d(TAG, "Cannot launch second MTP server.");
+                }
+                // Previously executed MtpServer is still running. It will be terminated
+                // because MTP device FD will become invalid soon. Also MtpService will get new
+                // intent after that when UsbDeviceManager configures USB with new state.
+                return;
+            }
+
+            Log.d(TAG, "starting MTP server in " + (mPtpMode ? "PTP mode" : "MTP mode") +
+                    " with storage " + primary.getPath() + (mUnlocked ? " unlocked" : "") + " as user " + UserHandle.myUserId());
+
+            final MtpDatabase database = new MtpDatabase(this, subdirs);
+            IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
+                    Context.USB_SERVICE));
+            ParcelFileDescriptor controlFd = null;
+            try {
+                controlFd = usbMgr.getControlFd(
+                        mPtpMode ? UsbManager.FUNCTION_PTP : UsbManager.FUNCTION_MTP);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error communicating with UsbManager: " + e);
+            }
+            FileDescriptor fd = null;
+            if (controlFd == null) {
+                Log.i(TAG, "Couldn't get control FD!");
+            } else {
+                fd = controlFd.getFileDescriptor();
+            }
+
+            final MtpServer server =
+                    new MtpServer(database, fd, mPtpMode,
+                            new OnServerTerminated(), Build.MANUFACTURER,
+                            Build.MODEL, "1.0");
+            database.setServer(server);
+            sServerHolder = new ServerHolder(server, database);
+
+            // Add currently mounted and enabled storages to the server
+            if (mUnlocked) {
+                if (mPtpMode) {
+                    addStorage(primary);
+                } else {
+                    for (StorageVolume v : mVolumeMap.values()) {
+                        addStorage(v);
+                    }
+                }
+            }
+            server.start();
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new Binder();
+    }
+
+    private void addStorage(StorageVolume volume) {
+        Log.v(TAG, "Adding MTP storage:" + volume.getPath());
+        synchronized (MtpService.class) {
+            if (sServerHolder != null) {
+                sServerHolder.database.addStorage(volume);
+            }
+        }
+    }
+
+    private void removeStorage(StorageVolume volume) {
+        synchronized (MtpService.class) {
+            if (sServerHolder != null) {
+                sServerHolder.database.removeStorage(volume);
+            }
+        }
+    }
+
+    private static class ServerHolder {
+        @NonNull final MtpServer server;
+        @NonNull final MtpDatabase database;
+
+        ServerHolder(@NonNull MtpServer server, @NonNull MtpDatabase database) {
+            Preconditions.checkNotNull(server);
+            Preconditions.checkNotNull(database);
+            this.server = server;
+            this.database = database;
+        }
+
+        void close() {
+            this.database.setServer(null);
+        }
+    }
+
+    private class OnServerTerminated implements Runnable {
+        @Override
+        public void run() {
+            synchronized (MtpService.class) {
+                if (sServerHolder == null) {
+                    Log.e(TAG, "sServerHolder is unexpectedly null.");
+                    return;
+                }
+                sServerHolder.close();
+                sServerHolder = null;
+            }
+        }
+    }
+}