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