Migrate ringtone picking out of MediaProvider.
Some OEMs have requested the ability to customize the ringtone
picker logic, and that's already supported by the existing
"config_defaultRingtonePickerEnabled" parameter today.
This change is a clean refactoring of the AOSP ringtone picker out
of MediaProvider and into a new "SoundPicker" module, which is
consistent with how some OEMs have already defined their own
"SoundPickerPrebuilt" module.
Bug: 134542205
Test: atest --test-mapping packages/providers/MediaProvider
Change-Id: Iadc60e731e6dcfce651f89bc7598e65541ab0ddf
diff --git a/packages/SoundPicker/Android.bp b/packages/SoundPicker/Android.bp
new file mode 100644
index 0000000..3be7ca9
--- /dev/null
+++ b/packages/SoundPicker/Android.bp
@@ -0,0 +1,18 @@
+android_app {
+ name: "SoundPicker",
+ manifest: "AndroidManifest.xml",
+
+ static_libs: [
+ "androidx.appcompat_appcompat",
+ ],
+ resource_dirs: [
+ "res",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ platform_apis: true,
+ certificate: "media",
+ privileged: true,
+}
diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml
new file mode 100644
index 0000000..9d08182
--- /dev/null
+++ b/packages/SoundPicker/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.soundpicker"
+ android:sharedUserId="android.media">
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+ <application
+ android:allowBackup="false"
+ android:supportsRtl="true">
+ <receiver android:name="RingtoneReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/>
+ </intent-filter>
+ </receiver>
+
+ <service android:name="RingtoneOverlayService" />
+
+ <activity android:name="RingtonePickerActivity"
+ android:theme="@style/PickerDialogTheme"
+ android:enabled="@*android:bool/config_defaultRingtonePickerEnabled"
+ android:excludeFromRecents="true">
+ <intent-filter>
+ <action android:name="android.intent.action.RINGTONE_PICKER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/packages/SoundPicker/res/drawable/ic_add.xml b/packages/SoundPicker/res/drawable/ic_add.xml
new file mode 100644
index 0000000..22b3fe9
--- /dev/null
+++ b/packages/SoundPicker/res/drawable/ic_add.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2016 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="?android:attr/colorAccent"
+ android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SoundPicker/res/drawable/ic_add_padded.xml b/packages/SoundPicker/res/drawable/ic_add_padded.xml
new file mode 100644
index 0000000..c376867
--- /dev/null
+++ b/packages/SoundPicker/res/drawable/ic_add_padded.xml
@@ -0,0 +1,22 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_add"
+ android:insetTop="4dp"
+ android:insetRight="4dp"
+ android:insetBottom="4dp"
+ android:insetLeft="4dp"/>
diff --git a/packages/SoundPicker/res/layout-watch/add_new_sound_item.xml b/packages/SoundPicker/res/layout-watch/add_new_sound_item.xml
new file mode 100644
index 0000000..6f91d77
--- /dev/null
+++ b/packages/SoundPicker/res/layout-watch/add_new_sound_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<!--
+ Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent.
+ Make the visibility to "gone" to prevent failures.
+ -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@null"
+ android:textColor="?android:attr/colorAccent"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:drawableStart="@drawable/ic_add_padded"
+ android:drawablePadding="8dp"
+ android:ellipsize="marquee"
+ android:visibility="gone" />
diff --git a/packages/SoundPicker/res/layout-watch/radio_with_work_badge.xml b/packages/SoundPicker/res/layout-watch/radio_with_work_badge.xml
new file mode 100644
index 0000000..0e11621
--- /dev/null
+++ b/packages/SoundPicker/res/layout-watch/radio_with_work_badge.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<com.android.providers.media.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ >
+
+ <CheckedTextView
+ android:id="@+id/checked_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:drawableStart="?android:attr/listChoiceIndicatorSingle"
+ android:drawablePadding="8dp"
+ android:ellipsize="marquee"
+ android:layout_toLeftOf="@+id/work_icon"
+ android:maxLines="3" />
+
+ <ImageView
+ android:id="@id/work_icon"
+ android:layout_width="18dp"
+ android:layout_height="18dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:scaleType="centerCrop"
+ android:layout_marginRight="20dp" />
+</com.android.providers.media.CheckedListItem>
diff --git a/packages/SoundPicker/res/layout/add_new_sound_item.xml b/packages/SoundPicker/res/layout/add_new_sound_item.xml
new file mode 100644
index 0000000..14421c9
--- /dev/null
+++ b/packages/SoundPicker/res/layout/add_new_sound_item.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground">
+
+<ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:scaleType="centerCrop"
+ android:layout_marginRight="24dp"
+ android:layout_marginLeft="24dp"
+ android:src="@drawable/ic_add" />
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/add_new_sound_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:text="@null"
+ android:textColor="?android:attr/colorAccent"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:maxLines="3"
+ android:gravity="center_vertical"
+ android:paddingEnd="?android:attr/dialogPreferredPadding"
+ android:drawablePadding="20dp"
+ android:ellipsize="marquee" />
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker/res/layout/radio_with_work_badge.xml b/packages/SoundPicker/res/layout/radio_with_work_badge.xml
new file mode 100644
index 0000000..e7d37ea
--- /dev/null
+++ b/packages/SoundPicker/res/layout/radio_with_work_badge.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<com.android.providers.media.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ >
+
+ <CheckedTextView
+ android:id="@+id/checked_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="20dp"
+ android:paddingEnd="?android:attr/dialogPreferredPadding"
+ android:drawableStart="?android:attr/listChoiceIndicatorSingle"
+ android:drawablePadding="20dp"
+ android:ellipsize="marquee"
+ android:layout_toLeftOf="@+id/work_icon"
+ android:maxLines="3" />
+
+ <ImageView
+ android:id="@id/work_icon"
+ android:layout_width="18dp"
+ android:layout_height="18dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:scaleType="centerCrop"
+ android:layout_marginRight="20dp" />
+</com.android.providers.media.CheckedListItem>
diff --git a/packages/SoundPicker/res/raw/default_alarm_alert.ogg b/packages/SoundPicker/res/raw/default_alarm_alert.ogg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/SoundPicker/res/raw/default_alarm_alert.ogg
diff --git a/packages/SoundPicker/res/raw/default_notification_sound.ogg b/packages/SoundPicker/res/raw/default_notification_sound.ogg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/SoundPicker/res/raw/default_notification_sound.ogg
diff --git a/packages/SoundPicker/res/raw/default_ringtone.ogg b/packages/SoundPicker/res/raw/default_ringtone.ogg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/SoundPicker/res/raw/default_ringtone.ogg
diff --git a/packages/SoundPicker/res/values-watch/config.xml b/packages/SoundPicker/res/values-watch/config.xml
new file mode 100644
index 0000000..0bc24fa
--- /dev/null
+++ b/packages/SoundPicker/res/values-watch/config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds. Do not translate.
+
+ NOTE: The naming convention is "config_camelCaseValue". -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the
+ ringtone will be automatically selected when the picker is closed. -->
+ <bool name="config_showOkCancelButtons">false</bool>
+</resources>
diff --git a/packages/SoundPicker/res/values/config.xml b/packages/SoundPicker/res/values/config.xml
new file mode 100644
index 0000000..4e237a2
--- /dev/null
+++ b/packages/SoundPicker/res/values/config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds. Do not translate.
+
+ NOTE: The naming convention is "config_camelCaseValue". -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the
+ ringtone will be automatically selected when the picker is closed. -->
+ <bool name="config_showOkCancelButtons">true</bool>
+</resources>
diff --git a/packages/SoundPicker/res/values/strings.xml b/packages/SoundPicker/res/values/strings.xml
new file mode 100644
index 0000000..56ed5fd
--- /dev/null
+++ b/packages/SoundPicker/res/values/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Choice in the ringtone picker. If chosen, the default ringtone will be used. -->
+ <string name="ringtone_default">Default ringtone</string>
+
+ <!-- Choice in the notification sound picker. If chosen, the default notification sound will be
+ used. -->
+ <string name="notification_sound_default">Default notification sound</string>
+
+ <!-- Choice in the alarm sound picker. If chosen, the default alarm sound will be used. -->
+ <string name="alarm_sound_default">Default alarm sound</string>
+
+ <!-- Text for the RingtonePicker item that allows adding a new ringtone. -->
+ <string name="add_ringtone_text">Add ringtone</string>
+ <!-- Text for the RingtonePicker item that allows adding a new alarm. -->
+ <string name="add_alarm_text">Add alarm</string>
+ <!-- Text for the RingtonePicker item that allows adding a new notification. -->
+ <string name="add_notification_text">Add notification</string>
+ <!-- Text for the RingtonePicker item ContextMenu that allows deleting a custom ringtone. -->
+ <string name="delete_ringtone_text">Delete</string>
+ <!-- Text for the Toast displayed when adding a custom ringtone fails. -->
+ <string name="unable_to_add_ringtone">Unable to add custom ringtone</string>
+ <!-- Text for the Toast displayed when deleting a custom ringtone fails. -->
+ <string name="unable_to_delete_ringtone">Unable to delete custom ringtone</string>
+</resources>
diff --git a/packages/SoundPicker/res/values/styles.xml b/packages/SoundPicker/res/values/styles.xml
new file mode 100644
index 0000000..d22d9c4
--- /dev/null
+++ b/packages/SoundPicker/res/values/styles.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
+ </style>
+
+</resources>
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java
new file mode 100644
index 0000000..2d37b4c
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 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.soundpicker;
+
+import android.app.Service;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.provider.MediaStore;
+import android.provider.Settings.System;
+import android.util.Log;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Service to copy and set customization of default sounds
+ */
+public class RingtoneOverlayService extends Service {
+ private static final String TAG = "RingtoneOverlayService";
+ private static final boolean DEBUG = false;
+
+ @Override
+ public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
+ AsyncTask.execute(() -> {
+ updateRingtones();
+ stopSelf();
+ });
+
+ // Try again later if we are killed before we finish.
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ @Override
+ public IBinder onBind(@Nullable final Intent intent) {
+ return null;
+ }
+
+ private void updateRingtones() {
+ copyResourceAndSetAsSound(R.raw.default_ringtone,
+ System.RINGTONE, Environment.DIRECTORY_RINGTONES);
+ copyResourceAndSetAsSound(R.raw.default_notification_sound,
+ System.NOTIFICATION_SOUND, Environment.DIRECTORY_NOTIFICATIONS);
+ copyResourceAndSetAsSound(R.raw.default_alarm_alert,
+ System.ALARM_ALERT, Environment.DIRECTORY_ALARMS);
+ }
+
+ /* If the resource contains any data, copy a resource to the file system, scan it, and set the
+ * file URI as the default for a sound. */
+ private void copyResourceAndSetAsSound(@IdRes final int id, @NonNull final String name,
+ @NonNull final String subPath) {
+ final File destDir = Environment.getExternalStoragePublicDirectory(subPath);
+ if (!destDir.exists() && !destDir.mkdirs()) {
+ Log.e(TAG, "can't create " + destDir.getAbsolutePath());
+ return;
+ }
+
+ final File dest = new File(destDir, "default_" + name + ".ogg");
+ try (
+ InputStream is = getResources().openRawResource(id);
+ FileOutputStream os = new FileOutputStream(dest);
+ ) {
+ if (is.available() > 0) {
+ FileUtils.copy(is, os);
+ final Uri uri = scanFile(dest);
+ if (uri != null) {
+ set(name, uri);
+ }
+ } else {
+ // TODO Shall we remove any former copied resource in this case and unset
+ // the defaults if we use this event a second time to clear the data?
+ if (DEBUG) Log.d(TAG, "Resource for " + name + " has no overlay");
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to open resource for " + name + ": " + e);
+ }
+ }
+
+ private Uri scanFile(@NonNull final File file) {
+ return MediaStore.scanFile(this, file);
+ }
+
+ private void set(@NonNull final String name, @NonNull final Uri uri) {
+ final Uri settingUri = System.getUriFor(name);
+ RingtoneManager.setActualDefaultRingtoneUri(this,
+ RingtoneManager.getDefaultType(settingUri), uri);
+ System.putInt(getContentResolver(), name + "_set", 1);
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
new file mode 100644
index 0000000..4ba5146
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2007 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.soundpicker;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.media.AudioAttributes;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.app.AlertActivity;
+import com.android.internal.app.AlertController;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * The {@link RingtonePickerActivity} allows the user to choose one from all of the
+ * available ringtones. The chosen ringtone's URI will be persisted as a string.
+ *
+ * @see RingtoneManager#ACTION_RINGTONE_PICKER
+ */
+public final class RingtonePickerActivity extends AlertActivity implements
+ AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener,
+ AlertController.AlertParams.OnPrepareListViewListener {
+
+ private static final int POS_UNKNOWN = -1;
+
+ private static final String TAG = "RingtonePickerActivity";
+
+ private static final int DELAY_MS_SELECTION_PLAYED = 300;
+
+ private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
+
+ private static final String SAVE_CLICKED_POS = "clicked_pos";
+
+ private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
+
+ private static final int ADD_FILE_REQUEST_CODE = 300;
+
+ private RingtoneManager mRingtoneManager;
+ private int mType;
+
+ private Cursor mCursor;
+ private Handler mHandler;
+ private BadgedRingtoneAdapter mAdapter;
+
+ /** The position in the list of the 'Silent' item. */
+ private int mSilentPos = POS_UNKNOWN;
+
+ /** The position in the list of the 'Default' item. */
+ private int mDefaultRingtonePos = POS_UNKNOWN;
+
+ /** The position in the list of the ringtone to sample. */
+ private int mSampleRingtonePos = POS_UNKNOWN;
+
+ /** Whether this list has the 'Silent' item. */
+ private boolean mHasSilentItem;
+
+ /** The Uri to place a checkmark next to. */
+ private Uri mExistingUri;
+
+ /** The number of static items in the list. */
+ private int mStaticItemCount;
+
+ /** Whether this list has the 'Default' item. */
+ private boolean mHasDefaultItem;
+
+ /** The Uri to play when the 'Default' item is clicked. */
+ private Uri mUriForDefaultItem;
+
+ /** Id of the user to which the ringtone picker should list the ringtones */
+ private int mPickerUserId;
+
+ /** Context of the user specified by mPickerUserId */
+ private Context mTargetContext;
+
+ /**
+ * A Ringtone for the default ringtone. In most cases, the RingtoneManager
+ * will stop the previous ringtone. However, the RingtoneManager doesn't
+ * manage the default ringtone for us, so we should stop this one manually.
+ */
+ private Ringtone mDefaultRingtone;
+
+ /**
+ * The ringtone that's currently playing, unless the currently playing one is the default
+ * ringtone.
+ */
+ private Ringtone mCurrentRingtone;
+
+ /**
+ * Stable ID for the ringtone that is currently checked (may be -1 if no ringtone is checked).
+ */
+ private long mCheckedItemId = -1;
+
+ private int mAttributesFlags;
+
+ private boolean mShowOkCancelButtons;
+
+ /**
+ * Keep the currently playing ringtone around when changing orientation, so that it
+ * can be stopped later, after the activity is recreated.
+ */
+ private static Ringtone sPlayingRingtone;
+
+ private DialogInterface.OnClickListener mRingtoneClickListener =
+ new DialogInterface.OnClickListener() {
+
+ /*
+ * On item clicked
+ */
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == mCursor.getCount() + mStaticItemCount) {
+ // The "Add new ringtone" item was clicked. Start a file picker intent to select
+ // only audio files (MIME type "audio/*")
+ final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
+ chooseFile.setType("audio/*");
+ chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
+ new String[] { "audio/*", "application/ogg" });
+ startActivityForResult(chooseFile, ADD_FILE_REQUEST_CODE);
+ return;
+ }
+
+ // Save the position of most recently clicked item
+ setCheckedItem(which);
+
+ // In the buttonless (watch-only) version, preemptively set our result since we won't
+ // have another chance to do so before the activity closes.
+ if (!mShowOkCancelButtons) {
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
+ }
+
+ // Play clip
+ playRingtone(which, 0);
+ }
+
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mHandler = new Handler();
+
+ Intent intent = getIntent();
+ mPickerUserId = UserHandle.myUserId();
+ mTargetContext = this;
+
+ // Get the types of ringtones to show
+ mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1);
+ initRingtoneManager();
+
+ /*
+ * Get whether to show the 'Default' item, and the URI to play when the
+ * default is clicked
+ */
+ mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
+ mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
+ if (mUriForDefaultItem == null) {
+ if (mType == RingtoneManager.TYPE_NOTIFICATION) {
+ mUriForDefaultItem = Settings.System.DEFAULT_NOTIFICATION_URI;
+ } else if (mType == RingtoneManager.TYPE_ALARM) {
+ mUriForDefaultItem = Settings.System.DEFAULT_ALARM_ALERT_URI;
+ } else if (mType == RingtoneManager.TYPE_RINGTONE) {
+ mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
+ } else {
+ // or leave it null for silence.
+ mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
+ }
+ }
+
+ // Get whether to show the 'Silent' item
+ mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
+ // AudioAttributes flags
+ mAttributesFlags |= intent.getIntExtra(
+ RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
+ 0 /*defaultValue == no flags*/);
+
+ mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
+
+ // The volume keys will control the stream that we are choosing a ringtone for
+ setVolumeControlStream(mRingtoneManager.inferStreamType());
+
+ // Get the URI whose list item should have a checkmark
+ mExistingUri = intent
+ .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+
+ // Create the list of ringtones and hold on to it so we can update later.
+ mAdapter = new BadgedRingtoneAdapter(this, mCursor,
+ /* isManagedProfile = */ UserManager.get(this).isManagedProfile(mPickerUserId));
+ if (savedInstanceState != null) {
+ setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN));
+ }
+
+ final AlertController.AlertParams p = mAlertParams;
+ p.mAdapter = mAdapter;
+ p.mOnClickListener = mRingtoneClickListener;
+ p.mLabelColumn = COLUMN_LABEL;
+ p.mIsSingleChoice = true;
+ p.mOnItemSelectedListener = this;
+ if (mShowOkCancelButtons) {
+ p.mPositiveButtonText = getString(com.android.internal.R.string.ok);
+ p.mPositiveButtonListener = this;
+ p.mNegativeButtonText = getString(com.android.internal.R.string.cancel);
+ p.mPositiveButtonListener = this;
+ }
+ p.mOnPrepareListViewListener = this;
+
+ p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
+ if (p.mTitle == null) {
+ if (mType == RingtoneManager.TYPE_ALARM) {
+ p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title_alarm);
+ } else if (mType == RingtoneManager.TYPE_NOTIFICATION) {
+ p.mTitle =
+ getString(com.android.internal.R.string.ringtone_picker_title_notification);
+ } else {
+ p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title);
+ }
+ }
+
+ setupAlert();
+ }
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(SAVE_CLICKED_POS, getCheckedItem());
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
+ // Add the custom ringtone in a separate thread
+ final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() {
+ @Override
+ protected Uri doInBackground(Uri... params) {
+ try {
+ return mRingtoneManager.addCustomExternalRingtone(params[0], mType);
+ } catch (IOException | IllegalArgumentException e) {
+ Log.e(TAG, "Unable to add new ringtone", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Uri ringtoneUri) {
+ if (ringtoneUri != null) {
+ requeryForAdapter();
+ } else {
+ // Ringtone was not added, display error Toast
+ Toast.makeText(RingtonePickerActivity.this, R.string.unable_to_add_ringtone,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+ installTask.execute(data.getData());
+ }
+ }
+
+ // Disabled because context menus aren't Material Design :(
+ /*
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+ int position = ((AdapterContextMenuInfo) menuInfo).position;
+
+ Ringtone ringtone = getRingtone(getRingtoneManagerPosition(position));
+ if (ringtone != null && mRingtoneManager.isCustomRingtone(ringtone.getUri())) {
+ // It's a custom ringtone so we display the context menu
+ menu.setHeaderTitle(ringtone.getTitle(this));
+ menu.add(Menu.NONE, Menu.FIRST, Menu.NONE, R.string.delete_ringtone_text);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Menu.FIRST: {
+ int deletedRingtonePos = ((AdapterContextMenuInfo) item.getMenuInfo()).position;
+ Uri deletedRingtoneUri = getRingtone(
+ getRingtoneManagerPosition(deletedRingtonePos)).getUri();
+ if(mRingtoneManager.deleteExternalRingtone(deletedRingtoneUri)) {
+ requeryForAdapter();
+ } else {
+ Toast.makeText(this, R.string.unable_to_delete_ringtone, Toast.LENGTH_SHORT)
+ .show();
+ }
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+ */
+
+ @Override
+ public void onDestroy() {
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ super.onDestroy();
+ }
+
+ public void onPrepareListView(ListView listView) {
+ // Reset the static item count, as this method can be called multiple times
+ mStaticItemCount = 0;
+
+ if (mHasDefaultItem) {
+ mDefaultRingtonePos = addDefaultRingtoneItem(listView);
+
+ if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
+ setCheckedItem(mDefaultRingtonePos);
+ }
+ }
+
+ if (mHasSilentItem) {
+ mSilentPos = addSilentItem(listView);
+
+ // The 'Silent' item should use a null Uri
+ if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) {
+ setCheckedItem(mSilentPos);
+ }
+ }
+
+ if (getCheckedItem() == POS_UNKNOWN) {
+ setCheckedItem(getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri)));
+ }
+
+ // In the buttonless (watch-only) version, preemptively set our result since we won't
+ // have another chance to do so before the activity closes.
+ if (!mShowOkCancelButtons) {
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
+ }
+ // If external storage is available, add a button to install sounds from storage.
+ if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ addNewSoundItem(listView);
+ }
+
+ // Enable context menu in ringtone items
+ registerForContextMenu(listView);
+ }
+
+ /**
+ * Re-query RingtoneManager for the most recent set of installed ringtones. May move the
+ * selected item position to match the new position of the chosen sound.
+ *
+ * This should only need to happen after adding or removing a ringtone.
+ */
+ private void requeryForAdapter() {
+ // Refresh and set a new cursor, closing the old one.
+ initRingtoneManager();
+ mAdapter.changeCursor(mCursor);
+
+ // Update checked item location.
+ int checkedPosition = POS_UNKNOWN;
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItemId(i) == mCheckedItemId) {
+ checkedPosition = getListPosition(i);
+ break;
+ }
+ }
+ if (mHasSilentItem && checkedPosition == POS_UNKNOWN) {
+ checkedPosition = mSilentPos;
+ }
+ setCheckedItem(checkedPosition);
+ setupAlert();
+ }
+
+ /**
+ * Adds a static item to the top of the list. A static item is one that is not from the
+ * RingtoneManager.
+ *
+ * @param listView The ListView to add to.
+ * @param textResId The resource ID of the text for the item.
+ * @return The position of the inserted item.
+ */
+ private int addStaticItem(ListView listView, int textResId) {
+ TextView textView = (TextView) getLayoutInflater().inflate(
+ com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false);
+ textView.setText(textResId);
+ listView.addHeaderView(textView);
+ mStaticItemCount++;
+ return listView.getHeaderViewsCount() - 1;
+ }
+
+ private int addDefaultRingtoneItem(ListView listView) {
+ if (mType == RingtoneManager.TYPE_NOTIFICATION) {
+ return addStaticItem(listView, R.string.notification_sound_default);
+ } else if (mType == RingtoneManager.TYPE_ALARM) {
+ return addStaticItem(listView, R.string.alarm_sound_default);
+ }
+
+ return addStaticItem(listView, R.string.ringtone_default);
+ }
+
+ private int addSilentItem(ListView listView) {
+ return addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
+ }
+
+ private void addNewSoundItem(ListView listView) {
+ View view = getLayoutInflater().inflate(R.layout.add_new_sound_item, listView,
+ false /* attachToRoot */);
+ TextView text = (TextView)view.findViewById(R.id.add_new_sound_text);
+
+ if (mType == RingtoneManager.TYPE_ALARM) {
+ text.setText(R.string.add_alarm_text);
+ } else if (mType == RingtoneManager.TYPE_NOTIFICATION) {
+ text.setText(R.string.add_notification_text);
+ } else {
+ text.setText(R.string.add_ringtone_text);
+ }
+ listView.addFooterView(view);
+ }
+
+ private void initRingtoneManager() {
+ // Reinstantiate the RingtoneManager. Cursor.requery() was deprecated and calling it
+ // causes unexpected behavior.
+ mRingtoneManager = new RingtoneManager(mTargetContext, /* includeParentRingtones */ true);
+ if (mType != -1) {
+ mRingtoneManager.setType(mType);
+ }
+ mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL);
+ }
+
+ private Ringtone getRingtone(int ringtoneManagerPosition) {
+ if (ringtoneManagerPosition < 0) {
+ return null;
+ }
+ return mRingtoneManager.getRingtone(ringtoneManagerPosition);
+ }
+
+ private int getCheckedItem() {
+ return mAlertParams.mCheckedItem;
+ }
+
+ private void setCheckedItem(int pos) {
+ mAlertParams.mCheckedItem = pos;
+ mCheckedItemId = mAdapter.getItemId(getRingtoneManagerPosition(pos));
+ }
+
+ /*
+ * On click of Ok/Cancel buttons
+ */
+ public void onClick(DialogInterface dialog, int which) {
+ boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
+
+ // Stop playing the previous ringtone
+ mRingtoneManager.stopPreviousRingtone();
+
+ if (positiveResult) {
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+
+ finish();
+ }
+
+ /*
+ * On item selected via keys
+ */
+ public void onItemSelected(AdapterView parent, View view, int position, long id) {
+ // footer view
+ if (position >= mCursor.getCount() + mStaticItemCount) {
+ return;
+ }
+
+ playRingtone(position, DELAY_MS_SELECTION_PLAYED);
+
+ // In the buttonless (watch-only) version, preemptively set our result since we won't
+ // have another chance to do so before the activity closes.
+ if (!mShowOkCancelButtons) {
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
+ }
+ }
+
+ public void onNothingSelected(AdapterView parent) {
+ }
+
+ private void playRingtone(int position, int delayMs) {
+ mHandler.removeCallbacks(this);
+ mSampleRingtonePos = position;
+ mHandler.postDelayed(this, delayMs);
+ }
+
+ public void run() {
+ stopAnyPlayingRingtone();
+ if (mSampleRingtonePos == mSilentPos) {
+ return;
+ }
+
+ Ringtone ringtone;
+ if (mSampleRingtonePos == mDefaultRingtonePos) {
+ if (mDefaultRingtone == null) {
+ mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem);
+ }
+ /*
+ * Stream type of mDefaultRingtone is not set explicitly here.
+ * It should be set in accordance with mRingtoneManager of this Activity.
+ */
+ if (mDefaultRingtone != null) {
+ mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType());
+ }
+ ringtone = mDefaultRingtone;
+ mCurrentRingtone = null;
+ } else {
+ ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos));
+ mCurrentRingtone = ringtone;
+ }
+
+ if (ringtone != null) {
+ if (mAttributesFlags != 0) {
+ ringtone.setAudioAttributes(
+ new AudioAttributes.Builder(ringtone.getAudioAttributes())
+ .setFlags(mAttributesFlags)
+ .build());
+ }
+ ringtone.play();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if (!isChangingConfigurations()) {
+ stopAnyPlayingRingtone();
+ } else {
+ saveAnyPlayingRingtone();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (!isChangingConfigurations()) {
+ stopAnyPlayingRingtone();
+ }
+ }
+
+ private void setSuccessResultWithRingtone(Uri ringtoneUri) {
+ setResult(RESULT_OK,
+ new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
+ }
+
+ private Uri getCurrentlySelectedRingtoneUri() {
+ if (getCheckedItem() == mDefaultRingtonePos) {
+ // Use the default Uri that they originally gave us.
+ return mUriForDefaultItem;
+ } else if (getCheckedItem() == mSilentPos) {
+ // Use a null Uri for the 'Silent' item.
+ return null;
+ } else {
+ return mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
+ }
+ }
+
+ private void saveAnyPlayingRingtone() {
+ if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
+ sPlayingRingtone = mDefaultRingtone;
+ } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
+ sPlayingRingtone = mCurrentRingtone;
+ }
+ }
+
+ private void stopAnyPlayingRingtone() {
+ if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
+ sPlayingRingtone.stop();
+ }
+ sPlayingRingtone = null;
+
+ if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
+ mDefaultRingtone.stop();
+ }
+
+ if (mRingtoneManager != null) {
+ mRingtoneManager.stopPreviousRingtone();
+ }
+ }
+
+ private int getRingtoneManagerPosition(int listPos) {
+ return listPos - mStaticItemCount;
+ }
+
+ private int getListPosition(int ringtoneManagerPos) {
+
+ // If the manager position is -1 (for not found), return that
+ if (ringtoneManagerPos < 0) return ringtoneManagerPos;
+
+ return ringtoneManagerPos + mStaticItemCount;
+ }
+
+ private static class LocalizedCursor extends CursorWrapper {
+
+ final int mTitleIndex;
+ final Resources mResources;
+ String mNamePrefix;
+ final Pattern mSanitizePattern;
+
+ LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
+ super(cursor);
+ mTitleIndex = mCursor.getColumnIndex(columnLabel);
+ mResources = resources;
+ mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
+ if (mTitleIndex == -1) {
+ Log.e(TAG, "No index for column " + columnLabel);
+ mNamePrefix = null;
+ } else {
+ try {
+ // Build the prefix for the name of the resource to look up
+ // format is: "ResourcePackageName::ResourceTypeName/"
+ // (the type name is expected to be "string" but let's not hardcode it).
+ // Here we use an existing resource "notification_sound_default" which is
+ // always expected to be found.
+ mNamePrefix = String.format("%s:%s/%s",
+ mResources.getResourcePackageName(R.string.notification_sound_default),
+ mResources.getResourceTypeName(R.string.notification_sound_default),
+ SOUND_NAME_RES_PREFIX);
+ } catch (NotFoundException e) {
+ mNamePrefix = null;
+ }
+ }
+ }
+
+ /**
+ * Process resource name to generate a valid resource name.
+ * @param input
+ * @return a non-null String
+ */
+ private String sanitize(String input) {
+ if (input == null) {
+ return "";
+ }
+ return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase();
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ final String defaultName = mCursor.getString(columnIndex);
+ if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
+ return defaultName;
+ }
+ TypedValue value = new TypedValue();
+ try {
+ // the name currently in the database is used to derive a name to match
+ // against resource names in this package
+ mResources.getValue(mNamePrefix + sanitize(defaultName), value, false);
+ } catch (NotFoundException e) {
+ // no localized string, use the default string
+ return defaultName;
+ }
+ if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
+ Log.d(TAG, String.format("Replacing name %s with %s",
+ defaultName, value.string.toString()));
+ return value.string.toString();
+ } else {
+ Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
+ return defaultName;
+ }
+ }
+ }
+
+ private class BadgedRingtoneAdapter extends CursorAdapter {
+ private final boolean mIsManagedProfile;
+
+ public BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile) {
+ super(context, cursor);
+ mIsManagedProfile = isManagedProfile;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (position < 0) {
+ return position;
+ }
+ return super.getItemId(position);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(R.layout.radio_with_work_badge, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ // Set text as the title of the ringtone
+ ((TextView) view.findViewById(R.id.checked_text_view))
+ .setText(cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX));
+
+ boolean isWorkRingtone = false;
+ if (mIsManagedProfile) {
+ /*
+ * Display the work icon if the ringtone belongs to a work profile. We can tell that
+ * a ringtone belongs to a work profile if the picker user is a managed profile, the
+ * ringtone Uri is in external storage, and either the uri has no user id or has the
+ * id of the picker user
+ */
+ Uri currentUri = mRingtoneManager.getRingtoneUri(cursor.getPosition());
+ int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId);
+ Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
+
+ if (uriUserId == mPickerUserId && uriWithoutUserId.toString()
+ .startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
+ isWorkRingtone = true;
+ }
+ }
+
+ ImageView workIcon = (ImageView) view.findViewById(R.id.work_icon);
+ if(isWorkRingtone) {
+ workIcon.setImageDrawable(getPackageManager().getUserBadgeForDensityNoBackground(
+ UserHandle.of(mPickerUserId), -1 /* density */));
+ workIcon.setVisibility(View.VISIBLE);
+ } else {
+ workIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneReceiver.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneReceiver.java
new file mode 100644
index 0000000..9e1ba3a
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneReceiver.java
@@ -0,0 +1,37 @@
+/* //device/content/providers/media/src/com/android/providers/media/MediaScannerReceiver.java
+**
+** Copyright 2007, 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.soundpicker;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class RingtoneReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_DEVICE_CUSTOMIZATION_READY.equals(action)) {
+ initResourceRingtones(context);
+ }
+ }
+
+ private void initResourceRingtones(Context context) {
+ context.startService(
+ new Intent(context, RingtoneOverlayService.class));
+ }
+}