diff options
author | 2023-09-29 10:49:04 +0000 | |
---|---|---|
committer | 2023-10-02 15:46:25 +0000 | |
commit | ce0f9c0460712eb07b1b5cb408c101e13452dd53 (patch) | |
tree | ee629d62fbee7dfc430484343a6f1c6562d190c9 | |
parent | 102064b4ab6c4e04ad099b39f9ce71e2fe1b1ab7 (diff) |
Add back the legacy SoundPicker and create a separate directory for the new picker.
We're bringing back the legacy SoundPicker and moving the new picker implementation into a separate directory (SoundPicker2). This way the currently released picker won't be using any unreleased (flagged) apis, and we won't leak the new features until they are un-flagged.
Bug: 293846645
Test: N/A
Change-Id: Iaf5780bc0efcb2095c9eba4129a75d06982bd140
45 files changed, 1583 insertions, 201 deletions
diff --git a/packages/SoundPicker/Android.bp b/packages/SoundPicker/Android.bp index 235e6724f91d..2c89d6dbce56 100644 --- a/packages/SoundPicker/Android.bp +++ b/packages/SoundPicker/Android.bp @@ -7,40 +7,22 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -android_library { - name: "SoundPickerLib", - srcs: [ - "src/**/*.java", +android_app { + name: "SoundPicker", + defaults: ["platform_app_defaults"], + manifest: "AndroidManifest.xml", + + static_libs: [ + "androidx.appcompat_appcompat", ], resource_dirs: [ "res", ], - static_libs: [ - "androidx.appcompat_appcompat", - "hilt_android", - "guava", - "androidx.recyclerview_recyclerview", - "androidx-constraintlayout_constraintlayout", - "androidx.viewpager2_viewpager2", - "com.google.android.material_material", + srcs: [ + "src/**/*.java", ], -} -android_app { - name: "SoundPicker", - defaults: ["platform_app_defaults"], - manifest: "AndroidManifest.xml", - static_libs: ["SoundPickerLib"], platform_apis: true, certificate: "media", privileged: true, - - optimize: { - enabled: true, - optimize: true, - shrink: true, - shrink_resources: true, - obfuscate: false, - proguard_compatibility: false, - }, } diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml index 934b003c605c..44295a5cbdd2 100644 --- a/packages/SoundPicker/AndroidManifest.xml +++ b/packages/SoundPicker/AndroidManifest.xml @@ -1,6 +1,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.soundpicker" - android:sharedUserId="android.media"> + 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" /> @@ -9,16 +9,12 @@ <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> - <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> - <application - android:name=".RingtonePickerApplication" - android:allowBackup="false" - android:label="@string/app_label" - android:theme="@style/Theme.AppCompat" - android:supportsRtl="true"> + android:allowBackup="false" + android:label="@string/app_label" + android:supportsRtl="true"> <receiver android:name="RingtoneReceiver" - android:exported="true"> + android:exported="true"> <intent-filter> <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/> </intent-filter> @@ -27,17 +23,14 @@ <service android:name="RingtoneOverlayService" /> <activity android:name="RingtonePickerActivity" - android:theme="@style/Theme.AppCompat.Dialog" - android:enabled="@*android:bool/config_defaultRingtonePickerEnabled" - android:excludeFromRecents="true" - android:exported="true"> + android:theme="@style/PickerDialogTheme" + android:enabled="@*android:bool/config_defaultRingtonePickerEnabled" + android:excludeFromRecents="true" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.RINGTONE_PICKER" /> <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.RINGTONE_PICKER_SOUND" /> - <category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" /> - <category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" /> </intent-filter> </activity> </application> -</manifest> +</manifest>
\ No newline at end of file diff --git a/packages/SoundPicker/res/layout/add_new_sound_item.xml b/packages/SoundPicker/res/layout/add_new_sound_item.xml index 024b97ef23be..57b70d7db0a9 100644 --- a/packages/SoundPicker/res/layout/add_new_sound_item.xml +++ b/packages/SoundPicker/res/layout/add_new_sound_item.xml @@ -19,9 +19,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" - android:focusable="true" - android:clickable="true"> + android:background="?android:attr/selectableItemBackground"> <ImageView android:layout_width="24dp" @@ -31,19 +29,19 @@ android:scaleType="centerCrop" android:layout_marginRight="24dp" android:layout_marginLeft="24dp" - android:src="@drawable/ic_add"/> + android:src="@drawable/ic_add" /> - <TextView - 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"/> + <TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/add_new_sound_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:text="@null" + android:textColor="?android:attr/colorAccent" + android:textAppearance="?android:attr/textAppearanceMedium" + android:maxLines="3" + android:gravity="center_vertical" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:drawablePadding="20dp" + android:ellipsize="marquee" /> </LinearLayout>
\ No newline at end of file diff --git a/packages/SoundPicker/res/layout/radio_with_work_badge.xml b/packages/SoundPicker/res/layout/radio_with_work_badge.xml index 36ac93ed630b..2e44b6f4dd7a 100644 --- a/packages/SoundPicker/res/layout/radio_with_work_badge.xml +++ b/packages/SoundPicker/res/layout/radio_with_work_badge.xml @@ -14,14 +14,12 @@ limitations under the License. --> -<com.android.soundpicker.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" - android:focusable="true" - android:clickable="true"> +<com.android.soundpicker.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" @@ -37,7 +35,7 @@ android:drawablePadding="20dp" android:ellipsize="marquee" android:layout_toLeftOf="@+id/work_icon" - android:maxLines="3"/> + android:maxLines="3" /> <ImageView android:id="@id/work_icon" @@ -46,5 +44,5 @@ android:layout_alignParentRight="true" android:layout_centerVertical="true" android:scaleType="centerCrop" - android:layout_marginRight="20dp"/> -</com.android.soundpicker.CheckedListItem> + android:layout_marginRight="20dp" /> +</com.android.soundpicker.CheckedListItem>
\ No newline at end of file diff --git a/packages/SoundPicker/res/values/strings.xml b/packages/SoundPicker/res/values/strings.xml index ab7b95a09028..04a2c2bb83c3 100644 --- a/packages/SoundPicker/res/values/strings.xml +++ b/packages/SoundPicker/res/values/strings.xml @@ -40,8 +40,4 @@ <!-- Text for the name of the app. [CHAR LIMIT=12] --> <string name="app_label">Sounds</string> - - <string name="empty_list">The list is empty</string> - <string name="sound_page_title">Sound</string> - <string name="vibration_page_title">Vibration</string> </resources> diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java index 90a14f9717db..ea46c0cee6b9 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java @@ -16,19 +16,43 @@ 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 androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.lifecycle.ViewModelProvider; - -import dagger.hilt.android.AndroidEntryPoint; +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.regex.Pattern; /** * The {@link RingtonePickerActivity} allows the user to choose one from all of the @@ -36,183 +60,727 @@ import dagger.hilt.android.AndroidEntryPoint; * * @see RingtoneManager#ACTION_RINGTONE_PICKER */ -@AndroidEntryPoint(AppCompatActivity.class) -public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity { +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"; - // TODO: Use the extra keys from RingtoneManager once they're added. - private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY"; - private static final String EXTRA_VIBRATION_SHOW_DEFAULT = "EXTRA_VIBRATION_SHOW_DEFAULT"; - private static final String EXTRA_VIBRATION_DEFAULT_URI = "EXTRA_VIBRATION_DEFAULT_URI"; - private static final String EXTRA_VIBRATION_SHOW_SILENT = "EXTRA_VIBRATION_SHOW_SILENT"; - private static final String EXTRA_VIBRATION_EXISTING_URI = "EXTRA_VIBRATION_EXISTING_URI"; - private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false; - - private RingtonePickerViewModel mRingtonePickerViewModel; + + 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 = getMediaFilePickerIntent(); + 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); - setContentView(R.layout.activity_ringtone_picker); - mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class); + mHandler = new Handler(); Intent intent = getIntent(); - /** - * Id of the user to which the ringtone picker should list the ringtones - */ - int pickerUserId = UserHandle.myUserId(); + mPickerUserId = UserHandle.myUserId(); + mTargetContext = this; // Get the types of ringtones to show - int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, - RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN); + 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*/); - boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons); + 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); - String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); - if (title == null) { - title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType)); + // 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)); } - String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY); - RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType( - ringtonePickerCategory); - RingtoneListHandler.Config soundListConfig = getSoundListConfig(pickerType, intent, - ringtoneType); - RingtoneListHandler.Config vibrationListConfig = getVibrationListConfig(pickerType, intent); + 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); + } + } - RingtonePickerViewModel.Config pickerConfig = - new RingtonePickerViewModel.Config(title, pickerUserId, ringtoneType, - showOkCancelButtons, mAttributesFlags, pickerType); + setupAlert(); - mRingtonePickerViewModel.init(pickerConfig, soundListConfig, vibrationListConfig); + ListView listView = mAlert.getListView(); + if (listView != null) { + // List view needs to gain focus in order for RSB to work. + if (!listView.requestFocus()) { + Log.e(TAG, "Unable to gain focus! RSB may not work properly."); + } + } + } + @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()); + } + } - if (savedInstanceState == null) { - TabbedDialogFragment dialogFragment = new TabbedDialogFragment(); + // 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); + } + } - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG); - if (prev != null) { - ft.remove(prev); + @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; } - ft.addToBackStack(null); - dialogFragment.show(ft, TabbedDialogFragment.TAG); } + } + */ - // The volume keys will control the stream that we are choosing a ringtone for - setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType()); + @Override + public void onDestroy() { + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + if (mCursor != null) { + mCursor.close(); + mCursor = null; + } + super.onDestroy(); } - private RingtoneListHandler.Config getSoundListConfig( - RingtonePickerViewModel.PickerType pickerType, Intent intent, int ringtoneType) { - if (pickerType != RingtonePickerViewModel.PickerType.SOUND_PICKER - && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { - // This ringtone picker does not require a sound picker. - return null; + 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); + } } - // Get whether to show the 'Default' sound item, and the URI to play when it's clicked - boolean hasDefaultSoundItem = - intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + if (mHasSilentItem) { + mSilentPos = addSilentItem(listView); - // The Uri to play when the 'Default' sound item is clicked. - Uri uriForDefaultSoundItem = - intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI); - if (uriForDefaultSoundItem == null) { - uriForDefaultSoundItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType); + // The 'Silent' item should use a null Uri + if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) { + setCheckedItem(mSilentPos); + } } - // Get whether this list has the 'Silent' sound item. - boolean hasSilentSoundItem = - intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + if (getCheckedItem() == POS_UNKNOWN) { + setCheckedItem(getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri))); + } - // AudioAttributes flags - mAttributesFlags |= intent.getIntExtra( - RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, - 0 /*defaultValue == no flags*/); + // 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 (resolvesMediaFilePicker() + && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + addNewSoundItem(listView); + } - // Get the sound URI whose list item should have a checkmark - Uri existingSoundUri = intent - .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); + // Enable context menu in ringtone items + registerForContextMenu(listView); + } - return new RingtoneListHandler.Config(hasDefaultSoundItem, - uriForDefaultSoundItem, hasSilentSoundItem, existingSoundUri); + /** + * 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(); } - private RingtoneListHandler.Config getVibrationListConfig( - RingtonePickerViewModel.PickerType pickerType, Intent intent) { - if (pickerType != RingtonePickerViewModel.PickerType.VIBRATION_PICKER - && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { - // This ringtone picker does not require a vibration picker. + /** + * 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; + } - // Get whether to show the 'Default' vibration item, and the URI to play when it's clicked - boolean hasDefaultVibrationItem = - intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_DEFAULT, false); + private void setCheckedItem(int pos) { + mAlertParams.mCheckedItem = pos; + mCheckedItemId = mAdapter.getItemId(getRingtoneManagerPosition(pos)); + } - // The Uri to play when the 'Default' vibration item is clicked. - Uri uriForDefaultVibrationItem = intent.getParcelableExtra(EXTRA_VIBRATION_DEFAULT_URI); + /* + * On click of Ok/Cancel buttons + */ + public void onClick(DialogInterface dialog, int which) { + boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE; - // Get whether this list has the 'Silent' vibration item. - boolean hasSilentVibrationItem = - intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_SILENT, true); + // Stop playing the previous ringtone + mRingtoneManager.stopPreviousRingtone(); - // Get the vibration URI whose list item should have a checkmark - Uri existingVibrationUri = intent.getParcelableExtra(EXTRA_VIBRATION_EXISTING_URI); + if (positiveResult) { + setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri()); + } else { + setResult(RESULT_CANCELED); + } - return new RingtoneListHandler.Config( - hasDefaultVibrationItem, uriForDefaultVibrationItem, hasSilentVibrationItem, - existingVibrationUri); + finish(); } - @Override - public void onDestroy() { - mRingtonePickerViewModel.cancelPendingAsyncTasks(); - super.onDestroy(); + /* + * 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(); - mRingtonePickerViewModel.onStop(isChangingConfigurations()); + + if (!isChangingConfigurations()) { + stopAnyPlayingRingtone(); + } else { + saveAnyPlayingRingtone(); + } } @Override protected void onPause() { super.onPause(); - mRingtonePickerViewModel.onPause(isChangingConfigurations()); + if (!isChangingConfigurations()) { + stopAnyPlayingRingtone(); + } } - /** - * Maps the ringtone picker category to the appropriate PickerType. - * If the category is null or the feature is still not released, then it defaults to sound - * picker. - * - * @param category the ringtone picker category. - * @return the corresponding picker type. - */ - private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) { - if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) { - return RingtonePickerViewModel.PickerType.SOUND_PICKER; + private void setSuccessResultWithRingtone(Uri ringtoneUri) { + setResult(RESULT_OK, + new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri)); + } + + private Uri getCurrentlySelectedRingtoneUri() { + if (getCheckedItem() == POS_UNKNOWN) { + // When the getCheckItem is POS_UNKNOWN, it is not the case we expected. + // We return null for this case. + return null; + } else 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(); } - switch (category) { - case "android.intent.category.RINGTONE_PICKER_RINGTONE": - return RingtonePickerViewModel.PickerType.RINGTONE_PICKER; - case "android.intent.category.RINGTONE_PICKER_SOUND": - return RingtonePickerViewModel.PickerType.SOUND_PICKER; - case "android.intent.category.RINGTONE_PICKER_VIBRATION": - return RingtonePickerViewModel.PickerType.VIBRATION_PICKER; - default: - Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker."); - return RingtonePickerViewModel.PickerType.SOUND_PICKER; + 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 Intent getMediaFilePickerIntent() { + final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); + chooseFile.setType("audio/*"); + chooseFile.putExtra(Intent.EXTRA_MIME_TYPES, + new String[] { "audio/*", "application/ogg" }); + return chooseFile; + } + + private boolean resolvesMediaFilePicker() { + return getMediaFilePickerIntent().resolveActivity(getPackageManager()) != null; + } + + 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); + } } } -} +}
\ No newline at end of file diff --git a/packages/SoundPicker2/Android.bp b/packages/SoundPicker2/Android.bp new file mode 100644 index 000000000000..f4d8bf2c76b5 --- /dev/null +++ b/packages/SoundPicker2/Android.bp @@ -0,0 +1,46 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SoundPicker2Lib", + srcs: [ + "src/**/*.java", + ], + resource_dirs: [ + "res", + ], + static_libs: [ + "androidx.appcompat_appcompat", + "hilt_android", + "guava", + "androidx.recyclerview_recyclerview", + "androidx-constraintlayout_constraintlayout", + "androidx.viewpager2_viewpager2", + "com.google.android.material_material", + ], +} + +android_app { + name: "SoundPicker2", + defaults: ["platform_app_defaults"], + manifest: "AndroidManifest.xml", + static_libs: ["SoundPicker2Lib"], + platform_apis: true, + certificate: "media", + privileged: true, + + optimize: { + enabled: true, + optimize: true, + shrink: true, + shrink_resources: true, + obfuscate: false, + proguard_compatibility: false, + }, +} diff --git a/packages/SoundPicker2/AndroidManifest.xml b/packages/SoundPicker2/AndroidManifest.xml new file mode 100644 index 000000000000..934b003c605c --- /dev/null +++ b/packages/SoundPicker2/AndroidManifest.xml @@ -0,0 +1,43 @@ +<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" /> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + + <application + android:name=".RingtonePickerApplication" + android:allowBackup="false" + android:label="@string/app_label" + android:theme="@style/Theme.AppCompat" + android:supportsRtl="true"> + <receiver android:name="RingtoneReceiver" + android:exported="true"> + <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/Theme.AppCompat.Dialog" + android:enabled="@*android:bool/config_defaultRingtonePickerEnabled" + android:excludeFromRecents="true" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.RINGTONE_PICKER" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.RINGTONE_PICKER_SOUND" /> + <category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" /> + <category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/packages/SoundPicker2/OWNERS b/packages/SoundPicker2/OWNERS new file mode 100644 index 000000000000..5bf46e039e96 --- /dev/null +++ b/packages/SoundPicker2/OWNERS @@ -0,0 +1,2 @@ +# Haptics team works on the SoundPicker +include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/packages/SoundPicker2/res/drawable/ic_add.xml b/packages/SoundPicker2/res/drawable/ic_add.xml new file mode 100644 index 000000000000..22b3fe9176e5 --- /dev/null +++ b/packages/SoundPicker2/res/drawable/ic_add.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="?android:attr/colorAccent" + android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/> +</vector>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/drawable/ic_add_padded.xml b/packages/SoundPicker2/res/drawable/ic_add_padded.xml new file mode 100644 index 000000000000..c376867896d0 --- /dev/null +++ b/packages/SoundPicker2/res/drawable/ic_add_padded.xml @@ -0,0 +1,22 @@ +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/ic_add" + android:insetTop="4dp" + android:insetRight="4dp" + android:insetBottom="4dp" + android:insetLeft="4dp"/> diff --git a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml b/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml new file mode 100644 index 000000000000..edfc0aba5be7 --- /dev/null +++ b/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml @@ -0,0 +1,36 @@ +<?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:id="@+id/add_new_sound_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@null" + android:textColor="?android:attr/colorAccent" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:drawableStart="@drawable/ic_add_padded" + android:drawablePadding="8dp" + android:ellipsize="marquee" + android:visibility="gone" /> diff --git a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml new file mode 100644 index 000000000000..ee29a3710143 --- /dev/null +++ b/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<com.android.soundpicker.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.soundpicker.CheckedListItem> diff --git a/packages/SoundPicker/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml index 6fc60801ad3a..6fc60801ad3a 100644 --- a/packages/SoundPicker/res/layout/activity_ringtone_picker.xml +++ b/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml diff --git a/packages/SoundPicker2/res/layout/add_new_sound_item.xml b/packages/SoundPicker2/res/layout/add_new_sound_item.xml new file mode 100644 index 000000000000..024b97ef23be --- /dev/null +++ b/packages/SoundPicker2/res/layout/add_new_sound_item.xml @@ -0,0 +1,49 @@ +<?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" + android:focusable="true" + android:clickable="true"> + + <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 + android:id="@+id/add_new_sound_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:text="@null" + android:textColor="?android:attr/colorAccent" + android:textAppearance="?android:attr/textAppearanceMedium" + android:maxLines="3" + android:gravity="center_vertical" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:drawablePadding="20dp" + android:ellipsize="marquee"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SoundPicker/res/layout/fragment_ringtone_picker.xml b/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml index 787f92ec06d6..787f92ec06d6 100644 --- a/packages/SoundPicker/res/layout/fragment_ringtone_picker.xml +++ b/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml diff --git a/packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml b/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml index 7efd91191b79..7efd91191b79 100644 --- a/packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml +++ b/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml diff --git a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout/radio_with_work_badge.xml new file mode 100644 index 000000000000..36ac93ed630b --- /dev/null +++ b/packages/SoundPicker2/res/layout/radio_with_work_badge.xml @@ -0,0 +1,50 @@ +<?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.soundpicker.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" + android:focusable="true" + android:clickable="true"> + + <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.soundpicker.CheckedListItem> diff --git a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg b/packages/SoundPicker2/res/raw/default_alarm_alert.ogg new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/SoundPicker2/res/raw/default_alarm_alert.ogg diff --git a/packages/SoundPicker2/res/raw/default_notification_sound.ogg b/packages/SoundPicker2/res/raw/default_notification_sound.ogg new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/SoundPicker2/res/raw/default_notification_sound.ogg diff --git a/packages/SoundPicker2/res/raw/default_ringtone.ogg b/packages/SoundPicker2/res/raw/default_ringtone.ogg new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/SoundPicker2/res/raw/default_ringtone.ogg diff --git a/packages/SoundPicker2/res/values/config.xml b/packages/SoundPicker2/res/values/config.xml new file mode 100644 index 000000000000..4e237a2f1644 --- /dev/null +++ b/packages/SoundPicker2/res/values/config.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds. Do not translate. + + NOTE: The naming convention is "config_camelCaseValue". --> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the + ringtone will be automatically selected when the picker is closed. --> + <bool name="config_showOkCancelButtons">true</bool> +</resources> diff --git a/packages/SoundPicker2/res/values/strings.xml b/packages/SoundPicker2/res/values/strings.xml new file mode 100644 index 000000000000..ab7b95a09028 --- /dev/null +++ b/packages/SoundPicker2/res/values/strings.xml @@ -0,0 +1,47 @@ +<?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> + + <!-- Text for the name of the app. [CHAR LIMIT=12] --> + <string name="app_label">Sounds</string> + + <string name="empty_list">The list is empty</string> + <string name="sound_page_title">Sound</string> + <string name="vibration_page_title">Vibration</string> +</resources> diff --git a/packages/SoundPicker2/res/values/styles.xml b/packages/SoundPicker2/res/values/styles.xml new file mode 100644 index 000000000000..d22d9c43d0fb --- /dev/null +++ b/packages/SoundPicker2/res/values/styles.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog"> + </style> + +</resources> diff --git a/packages/SoundPicker/src/com/android/soundpicker/BasePickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java index 4fc2a86537c1..4fc2a86537c1 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/BasePickerFragment.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java diff --git a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java b/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java new file mode 100644 index 000000000000..819ae987269d --- /dev/null +++ b/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java @@ -0,0 +1,67 @@ +/* + * 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. + */ + +package com.android.soundpicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.CheckedTextView; +import android.widget.RelativeLayout; + +/** + * The {@link CheckedListItem} is a layout item that represents a ringtone, and is used in + * {@link RingtonePickerActivity}. It contains the ringtone's name, and a work badge to right of the + * name if the ringtone belongs to a work profile. + */ +public class CheckedListItem extends RelativeLayout implements Checkable { + + public CheckedListItem(Context context) { + super(context); + } + + public CheckedListItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setChecked(boolean checked) { + getCheckedTextView().setChecked(checked); + } + + @Override + public boolean isChecked() { + return getCheckedTextView().isChecked(); + } + + @Override + public void toggle() { + getCheckedTextView().toggle(); + } + + private CheckedTextView getCheckedTextView() { + return (CheckedTextView) findViewById(R.id.checked_text_view); + } + +} diff --git a/packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java index afdbf053ac22..afdbf053ac22 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/LocalizedCursor.java b/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java index 83d04a345f8b..83d04a345f8b 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/LocalizedCursor.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java index 6817f534c00b..6817f534c00b 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneListHandler.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java index bb38e0e2ecaa..bb38e0e2ecaa 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneListHandler.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneListViewAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java index 4ca8943b5fd4..4ca8943b5fd4 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneListViewAdapter.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java index f08eb24ec20d..f08eb24ec20d 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java new file mode 100644 index 000000000000..b94ebebd825b --- /dev/null +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.soundpicker; + +import android.app.Service; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.os.FileUtils; +import android.os.IBinder; +import android.provider.MediaStore; +import android.provider.Settings.System; +import android.util.Log; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Service to copy and set customization of default sounds + */ +public class RingtoneOverlayService extends Service { + private static final String TAG = "RingtoneOverlayService"; + private static final boolean DEBUG = false; + + @Override + public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) { + AsyncTask.execute(() -> { + updateRingtones(); + stopSelf(); + }); + + // Try again later if we are killed before we finish. + return Service.START_REDELIVER_INTENT; + } + + @Override + public IBinder onBind(@Nullable final Intent intent) { + return null; + } + + private void updateRingtones() { + copyResourceAndSetAsSound(R.raw.default_ringtone, + System.RINGTONE, Environment.DIRECTORY_RINGTONES); + copyResourceAndSetAsSound(R.raw.default_notification_sound, + System.NOTIFICATION_SOUND, Environment.DIRECTORY_NOTIFICATIONS); + copyResourceAndSetAsSound(R.raw.default_alarm_alert, + System.ALARM_ALERT, Environment.DIRECTORY_ALARMS); + } + + /* If the resource contains any data, copy a resource to the file system, scan it, and set the + * file URI as the default for a sound. */ + private void copyResourceAndSetAsSound(@IdRes final int id, @NonNull final String name, + @NonNull final String subPath) { + final File destDir = Environment.getExternalStoragePublicDirectory(subPath); + if (!destDir.exists() && !destDir.mkdirs()) { + Log.e(TAG, "can't create " + destDir.getAbsolutePath()); + return; + } + + final File dest = new File(destDir, "default_" + name + ".ogg"); + try ( + InputStream is = getResources().openRawResource(id); + FileOutputStream os = new FileOutputStream(dest); + ) { + if (is.available() > 0) { + FileUtils.copy(is, os); + final Uri uri = scanFile(dest); + if (uri != null) { + set(name, uri); + } + } else { + // TODO Shall we remove any former copied resource in this case and unset + // the defaults if we use this event a second time to clear the data? + if (DEBUG) Log.d(TAG, "Resource for " + name + " has no overlay"); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource for " + name + ": " + e); + } + } + + private Uri scanFile(@NonNull final File file) { + return MediaStore.scanFile(getContentResolver(), file); + } + + private void set(@NonNull final String name, @NonNull final Uri uri) { + final Uri settingUri = System.getUriFor(name); + RingtoneManager.setActualDefaultRingtoneUri(this, + RingtoneManager.getDefaultType(settingUri), uri); + System.putInt(getContentResolver(), name + "_set", 1); + } +} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java new file mode 100644 index 000000000000..90a14f9717db --- /dev/null +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java @@ -0,0 +1,218 @@ +/* + * 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.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.ViewModelProvider; + +import dagger.hilt.android.AndroidEntryPoint; + +/** + * 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 + */ +@AndroidEntryPoint(AppCompatActivity.class) +public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity { + + private static final String TAG = "RingtonePickerActivity"; + // TODO: Use the extra keys from RingtoneManager once they're added. + private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY"; + private static final String EXTRA_VIBRATION_SHOW_DEFAULT = "EXTRA_VIBRATION_SHOW_DEFAULT"; + private static final String EXTRA_VIBRATION_DEFAULT_URI = "EXTRA_VIBRATION_DEFAULT_URI"; + private static final String EXTRA_VIBRATION_SHOW_SILENT = "EXTRA_VIBRATION_SHOW_SILENT"; + private static final String EXTRA_VIBRATION_EXISTING_URI = "EXTRA_VIBRATION_EXISTING_URI"; + private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false; + + private RingtonePickerViewModel mRingtonePickerViewModel; + private int mAttributesFlags; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_ringtone_picker); + + mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class); + + Intent intent = getIntent(); + /** + * Id of the user to which the ringtone picker should list the ringtones + */ + int pickerUserId = UserHandle.myUserId(); + + // Get the types of ringtones to show + int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN); + + // AudioAttributes flags + mAttributesFlags |= intent.getIntExtra( + RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, + 0 /*defaultValue == no flags*/); + + boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons); + + String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); + if (title == null) { + title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType)); + } + String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY); + RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType( + ringtonePickerCategory); + + RingtoneListHandler.Config soundListConfig = getSoundListConfig(pickerType, intent, + ringtoneType); + RingtoneListHandler.Config vibrationListConfig = getVibrationListConfig(pickerType, intent); + + RingtonePickerViewModel.Config pickerConfig = + new RingtonePickerViewModel.Config(title, pickerUserId, ringtoneType, + showOkCancelButtons, mAttributesFlags, pickerType); + + mRingtonePickerViewModel.init(pickerConfig, soundListConfig, vibrationListConfig); + + if (savedInstanceState == null) { + TabbedDialogFragment dialogFragment = new TabbedDialogFragment(); + + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG); + if (prev != null) { + ft.remove(prev); + } + ft.addToBackStack(null); + dialogFragment.show(ft, TabbedDialogFragment.TAG); + } + + // The volume keys will control the stream that we are choosing a ringtone for + setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType()); + } + + private RingtoneListHandler.Config getSoundListConfig( + RingtonePickerViewModel.PickerType pickerType, Intent intent, int ringtoneType) { + if (pickerType != RingtonePickerViewModel.PickerType.SOUND_PICKER + && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { + // This ringtone picker does not require a sound picker. + return null; + } + + // Get whether to show the 'Default' sound item, and the URI to play when it's clicked + boolean hasDefaultSoundItem = + intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + + // The Uri to play when the 'Default' sound item is clicked. + Uri uriForDefaultSoundItem = + intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI); + if (uriForDefaultSoundItem == null) { + uriForDefaultSoundItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType); + } + + // Get whether this list has the 'Silent' sound item. + boolean hasSilentSoundItem = + intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + + // AudioAttributes flags + mAttributesFlags |= intent.getIntExtra( + RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, + 0 /*defaultValue == no flags*/); + + // Get the sound URI whose list item should have a checkmark + Uri existingSoundUri = intent + .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); + + return new RingtoneListHandler.Config(hasDefaultSoundItem, + uriForDefaultSoundItem, hasSilentSoundItem, existingSoundUri); + } + + private RingtoneListHandler.Config getVibrationListConfig( + RingtonePickerViewModel.PickerType pickerType, Intent intent) { + if (pickerType != RingtonePickerViewModel.PickerType.VIBRATION_PICKER + && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { + // This ringtone picker does not require a vibration picker. + return null; + } + + // Get whether to show the 'Default' vibration item, and the URI to play when it's clicked + boolean hasDefaultVibrationItem = + intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_DEFAULT, false); + + // The Uri to play when the 'Default' vibration item is clicked. + Uri uriForDefaultVibrationItem = intent.getParcelableExtra(EXTRA_VIBRATION_DEFAULT_URI); + + // Get whether this list has the 'Silent' vibration item. + boolean hasSilentVibrationItem = + intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_SILENT, true); + + // Get the vibration URI whose list item should have a checkmark + Uri existingVibrationUri = intent.getParcelableExtra(EXTRA_VIBRATION_EXISTING_URI); + + return new RingtoneListHandler.Config( + hasDefaultVibrationItem, uriForDefaultVibrationItem, hasSilentVibrationItem, + existingVibrationUri); + } + + @Override + public void onDestroy() { + mRingtonePickerViewModel.cancelPendingAsyncTasks(); + super.onDestroy(); + } + + @Override + protected void onStop() { + super.onStop(); + mRingtonePickerViewModel.onStop(isChangingConfigurations()); + } + + @Override + protected void onPause() { + super.onPause(); + mRingtonePickerViewModel.onPause(isChangingConfigurations()); + } + + /** + * Maps the ringtone picker category to the appropriate PickerType. + * If the category is null or the feature is still not released, then it defaults to sound + * picker. + * + * @param category the ringtone picker category. + * @return the corresponding picker type. + */ + private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) { + if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) { + return RingtonePickerViewModel.PickerType.SOUND_PICKER; + } + + switch (category) { + case "android.intent.category.RINGTONE_PICKER_RINGTONE": + return RingtonePickerViewModel.PickerType.RINGTONE_PICKER; + case "android.intent.category.RINGTONE_PICKER_SOUND": + return RingtonePickerViewModel.PickerType.SOUND_PICKER; + case "android.intent.category.RINGTONE_PICKER_VIBRATION": + return RingtonePickerViewModel.PickerType.VIBRATION_PICKER; + default: + Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker."); + return RingtonePickerViewModel.PickerType.SOUND_PICKER; + } + } +} diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java index 48fd4fe2f15e..48fd4fe2f15e 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java index 2c0971121ccd..2c0971121ccd 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java new file mode 100644 index 000000000000..6a349366e744 --- /dev/null +++ b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java @@ -0,0 +1,36 @@ +/* + * 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.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)); + } +} diff --git a/packages/SoundPicker/src/com/android/soundpicker/SoundPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java index a37191f33668..a37191f33668 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/SoundPickerFragment.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/TabbedDialogFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java index 50ea9d7d3056..50ea9d7d3056 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/TabbedDialogFragment.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/VibrationPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java index 7412c1995b5a..7412c1995b5a 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/VibrationPickerFragment.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java diff --git a/packages/SoundPicker/src/com/android/soundpicker/ViewPagerAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java index 179068e9f20f..179068e9f20f 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/ViewPagerAdapter.java +++ b/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java diff --git a/packages/SoundPicker/tests/Android.bp b/packages/SoundPicker2/tests/Android.bp index c38426fa6746..d88d442afa17 100644 --- a/packages/SoundPicker/tests/Android.bp +++ b/packages/SoundPicker2/tests/Android.bp @@ -17,7 +17,7 @@ package { } android_test { - name: "SoundPickerTests", + name: "SoundPicker2Tests", certificate: "platform", libs: [ "android.test.runner", @@ -30,7 +30,7 @@ android_test { "androidx.test.ext.truth", "mockito-target-minus-junit4", "guava-android-testlib", - "SoundPickerLib", + "SoundPicker2Lib", ], srcs: [ "src/**/*.java", diff --git a/packages/SoundPicker/tests/AndroidManifest.xml b/packages/SoundPicker2/tests/AndroidManifest.xml index 295aeb1faa55..295aeb1faa55 100644 --- a/packages/SoundPicker/tests/AndroidManifest.xml +++ b/packages/SoundPicker2/tests/AndroidManifest.xml diff --git a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java index 80e71e200a53..80e71e200a53 100644 --- a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java +++ b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java diff --git a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java index cde6c76d27ff..cde6c76d27ff 100644 --- a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java +++ b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java |