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:
Jeff Sharkey
2019-08-21 14:37:07 -06:00
parent 570926daef
commit 8f18b2a0c1
18 changed files with 1298 additions and 0 deletions

View 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,
}

View 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>

View 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>

View 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"/>

View 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" />

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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));
}
}