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
This commit is contained in:
18
packages/SoundPicker/Android.bp
Normal file
18
packages/SoundPicker/Android.bp
Normal file
@@ -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,
|
||||
}
|
||||
33
packages/SoundPicker/AndroidManifest.xml
Normal file
33
packages/SoundPicker/AndroidManifest.xml
Normal file
@@ -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>
|
||||
24
packages/SoundPicker/res/drawable/ic_add.xml
Normal file
24
packages/SoundPicker/res/drawable/ic_add.xml
Normal file
@@ -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>
|
||||
22
packages/SoundPicker/res/drawable/ic_add_padded.xml
Normal file
22
packages/SoundPicker/res/drawable/ic_add_padded.xml
Normal file
@@ -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"/>
|
||||
35
packages/SoundPicker/res/layout-watch/add_new_sound_item.xml
Normal file
35
packages/SoundPicker/res/layout-watch/add_new_sound_item.xml
Normal file
@@ -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" />
|
||||
@@ -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>
|
||||
47
packages/SoundPicker/res/layout/add_new_sound_item.xml
Normal file
47
packages/SoundPicker/res/layout/add_new_sound_item.xml
Normal file
@@ -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>
|
||||
48
packages/SoundPicker/res/layout/radio_with_work_badge.xml
Normal file
48
packages/SoundPicker/res/layout/radio_with_work_badge.xml
Normal file
@@ -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>
|
||||
0
packages/SoundPicker/res/raw/default_ringtone.ogg
Normal file
0
packages/SoundPicker/res/raw/default_ringtone.ogg
Normal file
25
packages/SoundPicker/res/values-watch/config.xml
Normal file
25
packages/SoundPicker/res/values-watch/config.xml
Normal file
@@ -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>
|
||||
25
packages/SoundPicker/res/values/config.xml
Normal file
25
packages/SoundPicker/res/values/config.xml
Normal file
@@ -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>
|
||||
40
packages/SoundPicker/res/values/strings.xml
Normal file
40
packages/SoundPicker/res/values/strings.xml
Normal file
@@ -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>
|
||||
22
packages/SoundPicker/res/values/styles.xml
Normal file
22
packages/SoundPicker/res/values/styles.xml
Normal file
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user