FMRadio: fix listen to recordings

FM app used to use file:// Uri to start a music player. However, in
recent Android versions, this became a crashable offense
(android.os.FileUriExposedException).

To fix the crash, we can either:

1. Disable StrictMode check -> won't help if music app cannot read video
   files (3gpp counts as video)
2. Cleanly expose via FileProvider -> we can't, because uid=system
   cannot give out uri permissions for security reasons
3. Not be uid=system anymore -> breaks on dirty flash/upgrade path
   because file ownership user is still system, and also breaks existing
   device trees that chmod /dev/fm to system
4. impersonate one of the system-whitelisted file providers (settings
   and tvsettings) -> HACK
5. Remove the feature -> No thanks
6. Make FM not systemUid -> Proper fix! but requires all maintainers to
   edit their trees, which is a longer-term project, that said a later
   patch preliminarily addresses this by adding this app into media GID
7. Implement stub app which hosts the FileProvider but isn't uid=system

And that's why I implemented a tiny, no-UI stub app for making listen
possible. To avoid security holes, the stub app is gated behind
"signature" permission.

Change-Id: Ibf3f3c06967dda025d489e6612678f38b549cbcb
diff --git a/Android.mk b/Android.mk
index 5d77cdc..5abf929 100644
--- a/Android.mk
+++ b/Android.mk
@@ -28,7 +28,9 @@
 
 LOCAL_JNI_SHARED_LIBRARIES := libfmjni
 
-LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.fmradio.xml
+LOCAL_REQUIRED_MODULES := \
+    privapp_whitelist_com.android.fmradio.xml \
+    FmRecordingsProvider
 LOCAL_STATIC_ANDROID_LIBRARIES := \
     androidx.cardview_cardview
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 12a6f31..b161ee7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -40,6 +40,7 @@
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="com.android.fmradio.recordings.permission.PLAY_RECORDING" />
 
     <application
         android:hardwareAccelerated="true"
diff --git a/provider/Android.bp b/provider/Android.bp
new file mode 100644
index 0000000..da9e630
--- /dev/null
+++ b/provider/Android.bp
@@ -0,0 +1,28 @@
+//
+// Copyright (C) 2014 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.
+//
+
+android_app {
+    name: "FmRecordingsProvider",
+    certificate: "platform",
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+
+    static_libs: ["androidx.core_core"],
+
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/provider/AndroidManifest.xml b/provider/AndroidManifest.xml
new file mode 100644
index 0000000..1fa872f
--- /dev/null
+++ b/provider/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.fmradio.recordings"
+    android:versionCode="1"
+    android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="33" />
+
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+
+    <permission
+        android:name="com.android.fmradio.recordings.permission.PLAY_RECORDING"
+        android:protectionLevel="signature" />
+
+    <application
+        android:label="@string/app_name"
+        android:hardwareAccelerated="true">
+
+        <activity
+            android:name=".PlayRecording"
+            android:exported="true"
+            android:theme="@android:style/Theme.Translucent.NoTitleBar"
+            android:permission="com.android.fmradio.recordings.permission.PLAY_RECORDING" />
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="com.android.fmradio.recordings"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths" />
+        </provider>
+
+    </application>
+
+</manifest>
diff --git a/provider/res/values/strings.xml b/provider/res/values/strings.xml
new file mode 100644
index 0000000..2083fae
--- /dev/null
+++ b/provider/res/values/strings.xml
@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">FM recordings</string>
+    <string name="info_toast">FM recordings needs media permissions to play back recordings.</string>
+    <string name="denied_toast">FM recordings needs media permissions to play back recordings. Please grant media permissions in Settings.</string>
+</resources>
diff --git a/provider/res/xml/provider_paths.xml b/provider/res/xml/provider_paths.xml
new file mode 100644
index 0000000..ff724ab
--- /dev/null
+++ b/provider/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <external-path name="FM Recording" path="FM Recording"/>
+</paths>
diff --git a/provider/src/com/android/fmradio/recordings/PlayRecording.java b/provider/src/com/android/fmradio/recordings/PlayRecording.java
new file mode 100644
index 0000000..d906c01
--- /dev/null
+++ b/provider/src/com/android/fmradio/recordings/PlayRecording.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 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.fmradio.recordings;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+
+public class PlayRecording extends Activity {
+
+    private static final int REQUEST_CODE = 1;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        boolean hasPermissionAudio =
+                checkSelfPermission(android.Manifest.permission.READ_MEDIA_AUDIO)
+                == PackageManager.PERMISSION_GRANTED;
+        boolean hasPermissionVideo =
+                checkSelfPermission(android.Manifest.permission.READ_MEDIA_VIDEO)
+                == PackageManager.PERMISSION_GRANTED;
+        boolean shouldShowRationale = false;
+        if (!hasPermissionAudio) {
+            shouldShowRationale |= shouldShowRequestPermissionRationale(
+                    android.Manifest.permission.READ_MEDIA_AUDIO);
+        }
+        if (!hasPermissionVideo) {
+            shouldShowRationale |= shouldShowRequestPermissionRationale(
+                    android.Manifest.permission.READ_MEDIA_VIDEO);
+        }
+
+        if (hasPermissionAudio && hasPermissionVideo) {
+            startPlayerNow();
+            return;
+        } else if (shouldShowRationale) {
+            Toast.makeText(this, R.string.info_toast, Toast.LENGTH_LONG).show();
+        }
+
+        requestPermissions(new String[] {
+            android.Manifest.permission.READ_MEDIA_AUDIO,
+            android.Manifest.permission.READ_MEDIA_VIDEO
+        }, REQUEST_CODE);
+    }
+
+    private void startPlayerNow() {
+        if (checkSelfPermission(android.Manifest.permission.READ_MEDIA_AUDIO)
+                   != PackageManager.PERMISSION_GRANTED
+                || checkSelfPermission(android.Manifest.permission.READ_MEDIA_VIDEO)
+                   != PackageManager.PERMISSION_GRANTED) {
+            Toast.makeText(this, R.string.denied_toast, Toast.LENGTH_LONG).show();
+            finish();
+            return;
+        }
+
+        final Intent oldIntent = getIntent();
+        if (oldIntent != null && oldIntent.hasExtra("type") && oldIntent.hasExtra("path")) {
+            Intent nextIntent = new Intent(Intent.ACTION_VIEW);
+            Uri uri = FileProvider.getUriForFile(this,
+                    getApplicationContext().getPackageName(),
+                    new File(Uri.parse(oldIntent.getStringExtra("path")).getPath()));
+            nextIntent.setDataAndType(uri, oldIntent.getStringExtra("type"));
+            nextIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            startActivity(nextIntent);
+        }
+
+        finish();
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+                                           int[] grantResults) {
+        switch (requestCode) {
+            case REQUEST_CODE:
+                // If not granted, we show toast, so just go for it whatever happens
+                startPlayerNow();
+                break;
+        }
+   }
+}
diff --git a/src/com/android/fmradio/FmMainActivity.java b/src/com/android/fmradio/FmMainActivity.java
index daf52a9..95e027e 100644
--- a/src/com/android/fmradio/FmMainActivity.java
+++ b/src/com/android/fmradio/FmMainActivity.java
@@ -883,7 +883,11 @@
                         public void onActionTriggered() {
                             Intent playMusicIntent = new Intent(Intent.ACTION_VIEW);
                             try {
-                                playMusicIntent.setDataAndType(playUri, "audio/3gpp");
+                                playMusicIntent.setComponent(new ComponentName(
+                                        "com.android.fmradio.recordings",
+                                        "com.android.fmradio.recordings.PlayRecording"));
+                                playMusicIntent.putExtra("path", playUri.toString());
+                                playMusicIntent.putExtra("type", "audio/3gpp");
                                 startActivity(playMusicIntent);
                             } catch (ActivityNotFoundException e2) {
                                 // No activity respond