Add ability to unmount storage volumes

Bug: 134806870
Test: manual
Change-Id: I3f283692c63324d01e25bd009a5b447ad23df323
Merged-In: I3f283692c63324d01e25bd009a5b447ad23df323
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ce060ff..5b0bb69 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -45,6 +45,7 @@
     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
     <uses-permission android:name="android.permission.MANAGE_USERS"/>
     <uses-permission android:name="android.permission.MASTER_CLEAR" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
     <uses-permission android:name="android.permission.NETWORK_SETTINGS" />
     <uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"/>
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
@@ -392,6 +393,11 @@
             </intent-filter>
         </receiver>
 
+        <!-- Exported for SystemUI to trigger -->
+        <receiver android:name=".storage.StorageUnmountReceiver"
+                  android:exported="true"
+                  android:permission="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
+
         <!-- FileProvider to share a generated license html file.
              Note that "com.android.settings.files" is set here as its authorities because a Uri
              permission grant should be allowed to share a file with an external browser but it is
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 244f3bb..7b27df0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1037,6 +1037,10 @@
     <string name="storage_clear_data_dlg_text">All this app\u2019s data will be deleted permanently. This includes all files, settings, accounts, databases, etc.</string>
     <!-- Text for dialog if clear data fails. [CHAR LIMIT=55] -->
     <string name="storage_clear_failed_dlg_text">Couldn\u2019t clear storage for app.</string>
+    <!-- Toast informing that storage unmount operation was successful. [CHAR LIMIT=80]-->
+    <string name="storage_unmount_success"><xliff:g id="name" example="SD card">%1$s</xliff:g> is safely ejected</string>
+    <!-- Toast informing that storage unmount operation failed. [CHAR LIMIT=80]-->
+    <string name="storage_unmount_failure">Couldn\u2019t safely eject <xliff:g id="name" example="SD card">%1$s</xliff:g></string>
 
     <!-- Account management --><skip/>
     <!-- Title for settings that lead into information about User's accounts [CHAR LIMIT=35] -->
diff --git a/src/com/android/car/settings/storage/StorageUnmountReceiver.java b/src/com/android/car/settings/storage/StorageUnmountReceiver.java
new file mode 100644
index 0000000..13327c7
--- /dev/null
+++ b/src/com/android/car/settings/storage/StorageUnmountReceiver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.car.settings.storage;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+
+import com.android.car.settings.common.Logger;
+
+/**
+ * A {@link BroadcastReceiver} which unmounts a {@link android.os.storage.StorageVolume} based on
+ * the provided volume id.
+ */
+public class StorageUnmountReceiver extends BroadcastReceiver {
+
+    private static final Logger LOG = new Logger(StorageUnmountReceiver.class);
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        StorageManager storage = context.getSystemService(StorageManager.class);
+
+        String volId = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
+        VolumeInfo vol = storage.findVolumeById(volId);
+        if (vol != null) {
+            new UnmountTask(context, vol).execute();
+        } else {
+            LOG.w("Missing volume " + volId);
+        }
+    }
+}
diff --git a/src/com/android/car/settings/storage/UnmountTask.java b/src/com/android/car/settings/storage/UnmountTask.java
new file mode 100644
index 0000000..5e117dd
--- /dev/null
+++ b/src/com/android/car/settings/storage/UnmountTask.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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.car.settings.storage;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+
+/** Task used to unmount a {@link android.os.storage.StorageVolume}. */
+public class UnmountTask extends AsyncTask<Void, Void, Exception> {
+
+    private static final Logger LOG = new Logger(UnmountTask.class);
+
+    private final Context mContext;
+    private final StorageManager mStorageManager;
+    private final String mVolumeId;
+    private final String mDescription;
+
+    public UnmountTask(Context context, VolumeInfo volume) {
+        mContext = context.getApplicationContext();
+        mStorageManager = mContext.getSystemService(StorageManager.class);
+        mVolumeId = volume.getId();
+        mDescription = mStorageManager.getBestVolumeDescription(volume);
+    }
+
+    @Override
+    protected Exception doInBackground(Void... params) {
+        try {
+            mStorageManager.unmount(mVolumeId);
+            return null;
+        } catch (Exception e) {
+            return e;
+        }
+    }
+
+    @Override
+    protected void onPostExecute(@Nullable Exception e) {
+        if (e == null) {
+            Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_success,
+                    mDescription), Toast.LENGTH_SHORT).show();
+        } else {
+            LOG.e("Failed to unmount " + mVolumeId, e);
+            Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_failure,
+                    mDescription), Toast.LENGTH_SHORT).show();
+        }
+    }
+}