Add two tabs to the ringtone picker pop-up view.
We're converting the AlertDialog to a DialogFragment with a ViewPage and two tabs (Sound and Vibration). The Vibration tab will be empty for now, while the Sound tab will include a recyclerview with all available sounds.
Fix: 275541592
Test: com.android.soundpicker.RingtonePickerViewModelTest
Change-Id: Idda019d2ef460785d2b2fd9df35858d4650829fc
diff --git a/packages/SoundPicker/Android.bp b/packages/SoundPicker/Android.bp
index c8999fb..1ac9bbb 100644
--- a/packages/SoundPicker/Android.bp
+++ b/packages/SoundPicker/Android.bp
@@ -19,6 +19,10 @@
"androidx.appcompat_appcompat",
"hilt_android",
"guava",
+ "androidx.recyclerview_recyclerview",
+ "androidx-constraintlayout_constraintlayout",
+ "androidx.viewpager2_viewpager2",
+ "com.google.android.material_material",
],
}
diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml
index 1f99e75..934b003 100644
--- a/packages/SoundPicker/AndroidManifest.xml
+++ b/packages/SoundPicker/AndroidManifest.xml
@@ -34,6 +34,9 @@
<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>
diff --git a/packages/SoundPicker/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker/res/layout/activity_ringtone_picker.xml
index 4eecf89..6fc6080 100644
--- a/packages/SoundPicker/res/layout/activity_ringtone_picker.xml
+++ b/packages/SoundPicker/res/layout/activity_ringtone_picker.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +16,6 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" />
\ No newline at end of file
+ android:layout_height="match_parent"/>
\ 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 14421c9..024b97e 100644
--- a/packages/SoundPicker/res/layout/add_new_sound_item.xml
+++ b/packages/SoundPicker/res/layout/add_new_sound_item.xml
@@ -16,12 +16,14 @@
-->
<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: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
+ <ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
@@ -29,9 +31,9 @@
android:scaleType="centerCrop"
android:layout_marginRight="24dp"
android:layout_marginLeft="24dp"
- android:src="@drawable/ic_add" />
+ android:src="@drawable/ic_add"/>
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ <TextView
android:id="@+id/add_new_sound_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -43,5 +45,5 @@
android:gravity="center_vertical"
android:paddingEnd="?android:attr/dialogPreferredPadding"
android:drawablePadding="20dp"
- android:ellipsize="marquee" />
+ android:ellipsize="marquee"/>
</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker/res/layout/fragment_sound_picker.xml b/packages/SoundPicker/res/layout/fragment_sound_picker.xml
new file mode 100644
index 0000000..787f92e
--- /dev/null
+++ b/packages/SoundPicker/res/layout/fragment_sound_picker.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+-->
+<androidx.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+/>
\ No newline at end of file
diff --git a/packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml b/packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml
new file mode 100644
index 0000000..7efd911
--- /dev/null
+++ b/packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tabLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/masterViewPager"
+ android:paddingTop="12dp"
+ android:paddingBottom="12dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker/res/layout/fragment_vibration_picker.xml b/packages/SoundPicker/res/layout/fragment_vibration_picker.xml
new file mode 100644
index 0000000..34d95aa2
--- /dev/null
+++ b/packages/SoundPicker/res/layout/fragment_vibration_picker.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/empty_list"
+ android:textColor="?android:attr/colorAccent"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center"
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ 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 c8ca231..36ac93e 100644
--- a/packages/SoundPicker/res/layout/radio_with_work_badge.xml
+++ b/packages/SoundPicker/res/layout/radio_with_work_badge.xml
@@ -14,12 +14,14 @@
limitations under the License.
-->
-<com.android.soundpicker.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
+<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"
@@ -35,7 +37,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"
@@ -44,5 +46,5 @@
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:scaleType="centerCrop"
- android:layout_marginRight="20dp" />
+ android:layout_marginRight="20dp"/>
</com.android.soundpicker.CheckedListItem>
diff --git a/packages/SoundPicker/res/values/strings.xml b/packages/SoundPicker/res/values/strings.xml
index 04a2c2b..ab7b95a 100644
--- a/packages/SoundPicker/res/values/strings.xml
+++ b/packages/SoundPicker/res/values/strings.xml
@@ -40,4 +40,8 @@
<!-- 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/LocalizedCursor.java b/packages/SoundPicker/src/com/android/soundpicker/LocalizedCursor.java
new file mode 100644
index 0000000..83d04a3
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/LocalizedCursor.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 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.res.Resources;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.util.Log;
+import android.util.TypedValue;
+
+import androidx.annotation.Nullable;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * A cursor wrapper class mainly used to guarantee getting a ringtone title
+ */
+final class LocalizedCursor extends CursorWrapper {
+
+ private static final String TAG = "LocalizedCursor";
+ private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
+
+ private final int mTitleIndex;
+ private final Resources mResources;
+ private final Pattern mSanitizePattern;
+ private final String mNamePrefix;
+
+ 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 {
+ mNamePrefix = buildNamePrefix(mResources);
+ }
+ }
+
+ /**
+ * Builds the prefix for the name of the resource to look up.
+ * The 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.
+ *
+ * @param resources Application's resources
+ * @return the built name prefix, or null if failed to build.
+ */
+ @Nullable
+ private static String buildNamePrefix(Resources resources) {
+ try {
+ return String.format("%s:%s/%s",
+ resources.getResourcePackageName(R.string.notification_sound_default),
+ resources.getResourceTypeName(R.string.notification_sound_default),
+ SOUND_NAME_RES_PREFIX);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Failed to build the prefix for the name of the resource.", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Process resource name to generate a valid resource name.
+ *
+ * @return a non-null String
+ */
+ private String sanitize(String input) {
+ if (input == null) {
+ return "";
+ }
+ return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(Locale.ROOT);
+ }
+
+ @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,
+ /* resolveRefs= */ false);
+ } catch (Resources.NotFoundException e) {
+ Log.d(TAG, "Failed to get localized string. Using default string instead.", e);
+ 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;
+ }
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneAdapter.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneAdapter.java
new file mode 100644
index 0000000..1f33aa2
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneAdapter.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 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 static com.android.internal.widget.RecyclerView.NO_ID;
+
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.media.RingtoneManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckedTextView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.StringRes;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The adapter presents a list of ringtones which may include fixed item in the list and an action
+ * button at the end.
+ *
+ * The adapter handles three different types of items:
+ * <ul>
+ * <li>FIXED: Fixed items are items added to the top of the list. These items can not be modified
+ * and their position will never change.
+ * <li>DYNAMIC: Dynamic items are items from the ringtone manager. These items can be modified
+ * and their position can change.
+ * <li>FOOTER: A footer item is an added button to the end of the list. This item can be clicked
+ * but not selected and its position will never change.
+ * </ul>
+ */
+final class RingtoneAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final int VIEW_TYPE_FIXED_ITEM = 0;
+ private static final int VIEW_TYPE_DYNAMIC_ITEM = 1;
+ private static final int VIEW_TYPE_ADD_RINGTONE_ITEM = 2;
+ private final Cursor mCursor;
+ private final List<Integer> mFixedItemTitles;
+ private final WorkRingtoneProvider mWorkRingtoneProvider;
+ private final RingtoneSelectionListener mRingtoneSelectionListener;
+ private final int mRowIDColumn;
+ private int mSelectedItem = -1;
+ @StringRes private Integer mAddRingtoneItemTitle;
+
+ /** Listener for ringtone selections. */
+ interface RingtoneSelectionListener {
+ void onRingtoneSelected(int position);
+ void onAddRingtoneSelected();
+ }
+ /** Provides a mean to detect work ringtones. */
+ interface WorkRingtoneProvider {
+ boolean isWorkRingtone(int position);
+
+ Drawable getWorkIconDrawable();
+ }
+
+ RingtoneAdapter(Cursor cursor, RingtoneSelectionListener ringtoneSelectionListener,
+ WorkRingtoneProvider workRingtoneProvider) {
+ mCursor = cursor;
+ mRingtoneSelectionListener = ringtoneSelectionListener;
+ mWorkRingtoneProvider = workRingtoneProvider;
+ mFixedItemTitles = new ArrayList<>();
+ mRowIDColumn = mCursor != null ? mCursor.getColumnIndex("_id") : -1;
+ setHasStableIds(true);
+ }
+
+ void setSelectedItem(int position) {
+ notifyItemChanged(mSelectedItem);
+ mSelectedItem = position;
+ notifyItemChanged(mSelectedItem);
+ }
+
+ void addTitleForFixedItem(@StringRes int textResId) {
+ mFixedItemTitles.add(textResId);
+ notifyItemInserted(mFixedItemTitles.size() - 1);
+ }
+
+ void addTitleForAddRingtoneItem(@StringRes int textResId) {
+ mAddRingtoneItemTitle = textResId;
+ notifyItemInserted(getItemCount() - 1);
+ }
+
+ @NotNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+ if (viewType == VIEW_TYPE_FIXED_ITEM) {
+ View fixedItemView = inflater.inflate(
+ com.android.internal.R.layout.select_dialog_singlechoice_material, parent,
+ false);
+
+ return new FixedItemViewHolder(fixedItemView, mRingtoneSelectionListener);
+ }
+
+ if (viewType == VIEW_TYPE_ADD_RINGTONE_ITEM) {
+ View addRingtoneItemView = inflater.inflate(R.layout.add_new_sound_item, parent, false);
+
+ return new AddRingtoneItemViewHolder(addRingtoneItemView, mRingtoneSelectionListener);
+ }
+
+ View view = inflater.inflate(R.layout.radio_with_work_badge, parent, false);
+
+ return new DynamicItemViewHolder(view, mRingtoneSelectionListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NotNull RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof FixedItemViewHolder) {
+ FixedItemViewHolder viewHolder = (FixedItemViewHolder) holder;
+
+ viewHolder.onBind(mFixedItemTitles.get(position),
+ /* isChecked= */ position == mSelectedItem);
+ return;
+ }
+ if (holder instanceof AddRingtoneItemViewHolder) {
+ AddRingtoneItemViewHolder viewHolder = (AddRingtoneItemViewHolder) holder;
+
+ viewHolder.onBind(mAddRingtoneItemTitle);
+ return;
+ }
+
+ if (!(holder instanceof DynamicItemViewHolder)) {
+ throw new IllegalArgumentException("holder type is not supported");
+ }
+
+ DynamicItemViewHolder viewHolder = (DynamicItemViewHolder) holder;
+ int pos = position - mFixedItemTitles.size();
+ if (!mCursor.moveToPosition(pos)) {
+ throw new IllegalStateException("Could not move cursor to position: " + pos);
+ }
+
+ Drawable workIcon = (mWorkRingtoneProvider != null)
+ && mWorkRingtoneProvider.isWorkRingtone(position)
+ ? mWorkRingtoneProvider.getWorkIconDrawable() : null;
+
+ viewHolder.onBind(mCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX),
+ /* isChecked= */ position == mSelectedItem, workIcon);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (!mFixedItemTitles.isEmpty() && position < mFixedItemTitles.size()) {
+ return VIEW_TYPE_FIXED_ITEM;
+ }
+ if (mAddRingtoneItemTitle != null && position == getItemCount() - 1) {
+ return VIEW_TYPE_ADD_RINGTONE_ITEM;
+ }
+
+ return VIEW_TYPE_DYNAMIC_ITEM;
+ }
+
+ @Override
+ public int getItemCount() {
+ int itemCount = mFixedItemTitles.size() + mCursor.getCount();
+
+ if (mAddRingtoneItemTitle != null) {
+ itemCount++;
+ }
+
+ return itemCount;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ int itemViewType = getItemViewType(position);
+ if (itemViewType == VIEW_TYPE_FIXED_ITEM) {
+ // Since the item is a fixed item, then we can use the position as a stable ID
+ // since the order of the fixed items should never change.
+ return position;
+ }
+ if (itemViewType == VIEW_TYPE_DYNAMIC_ITEM && mCursor != null
+ && mCursor.moveToPosition(position - mFixedItemTitles.size())
+ && mRowIDColumn != -1) {
+ return mCursor.getLong(mRowIDColumn) + mFixedItemTitles.size();
+ }
+
+ // The position is either invalid or the item is the add ringtone item view, so no stable
+ // ID is returned. Add ringtone item view cannot be selected and only include an action
+ // buttons.
+ return NO_ID;
+ }
+
+ private static class DynamicItemViewHolder extends RecyclerView.ViewHolder {
+ private final CheckedTextView mTitleTextView;
+ private final ImageView mWorkIcon;
+
+ DynamicItemViewHolder(View itemView, RingtoneSelectionListener listener) {
+ super(itemView);
+
+ mTitleTextView = itemView.findViewById(R.id.checked_text_view);
+ mWorkIcon = itemView.findViewById(R.id.work_icon);
+ itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
+ }
+
+ void onBind(String title, boolean isChecked, Drawable workIcon) {
+ Objects.requireNonNull(mTitleTextView);
+ Objects.requireNonNull(mWorkIcon);
+
+ mTitleTextView.setText(title);
+ mTitleTextView.setChecked(isChecked);
+
+ if (workIcon == null) {
+ mWorkIcon.setVisibility(View.GONE);
+ } else {
+ mWorkIcon.setImageDrawable(workIcon);
+ mWorkIcon.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ private static class FixedItemViewHolder extends RecyclerView.ViewHolder {
+ private final CheckedTextView mTitleTextView;
+
+ FixedItemViewHolder(View itemView, RingtoneSelectionListener listener) {
+ super(itemView);
+
+ mTitleTextView = (CheckedTextView) itemView;
+ itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
+ }
+
+ void onBind(@StringRes int title, boolean isChecked) {
+ Objects.requireNonNull(mTitleTextView);
+
+ mTitleTextView.setText(title);
+ mTitleTextView.setChecked(isChecked);
+ }
+ }
+
+ private static class AddRingtoneItemViewHolder extends RecyclerView.ViewHolder {
+ private final TextView mTitleTextView;
+
+ AddRingtoneItemViewHolder(View itemView, RingtoneSelectionListener listener) {
+ super(itemView);
+
+ mTitleTextView = itemView.findViewById(R.id.add_new_sound_text);
+ itemView.setOnClickListener(v -> listener.onAddRingtoneSelected());
+ }
+
+ void onBind(@StringRes int title) {
+ Objects.requireNonNull(mTitleTextView);
+
+ mTitleTextView.setText(title);
+ }
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
index f591aa5..4d7cf1c 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@@ -16,46 +16,20 @@
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.RingtoneManager;
import android.net.Uri;
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.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 androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
-import com.google.common.util.concurrent.FutureCallback;
-
import dagger.hilt.android.AndroidEntryPoint;
-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.
@@ -63,106 +37,17 @@
* @see RingtoneManager#ACTION_RINGTONE_PICKER
*/
@AndroidEntryPoint(AppCompatActivity.class)
-public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity implements
- AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener,
- DialogInterface.OnDismissListener {
-
- private static final int POS_UNKNOWN = -1;
+public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity {
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;
+ // TODO: Use the extra key from RingtoneManager once it's added.
+ private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY";
+ private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false;
private RingtonePickerViewModel mRingtonePickerViewModel;
- private int mType;
-
- private Cursor mCursor;
- private Handler mHandler;
- private BadgedRingtoneAdapter mAdapter;
-
- /** Whether this list has the 'Silent' item. */
- private boolean mHasSilentItem;
-
- /** The Uri to place a checkmark next to. */
- private Uri mExistingUri;
-
- /** 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;
-
- /**
- * 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;
-
- private AlertDialog mAlertDialog;
-
- private int mCheckedItem = POS_UNKNOWN;
-
- private final DialogInterface.OnClickListener mRingtoneClickListener =
- new DialogInterface.OnClickListener() {
-
- /*
- * On item clicked
- */
- public void onClick(DialogInterface dialog, int which) {
- if (which == mCursor.getCount() + mRingtonePickerViewModel.getFixedItemCount()) {
- // 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(
- mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
- mUriForDefaultItem));
- }
-
- // Play clip
- playRingtone(which, 0);
- }
-
- };
- private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() {
- @Override
- public void onSuccess(Uri ringtoneUri) {
- requeryForAdapter();
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.e(TAG, "Failed to add custom ringtone.", throwable);
- // Ringtone was not added, display error Toast
- Toast.makeText(RingtonePickerActivity.this.getApplicationContext(),
- R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show();
- }
- };
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -170,300 +55,75 @@
mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class);
- mHandler = new Handler();
-
Intent intent = getIntent();
- mPickerUserId = UserHandle.myUserId();
+ /**
+ * Id of the user to which the ringtone picker should list the ringtones
+ */
+ int pickerUserId = UserHandle.myUserId();
// Get the types of ringtones to show
- mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
+ int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN);
- mRingtonePickerViewModel.initRingtoneManager(mType);
- setupCursor();
- /*
- * 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) {
- mUriForDefaultItem = RingtonePickerViewModel.getDefaultItemUriByType(mType);
+ // Get whether to show the 'Default' item, and the URI to play when the default is clicked
+ boolean hasDefaultItem =
+ intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
+ // The Uri to play when the 'Default' item is clicked.
+ Uri uriForDefaultItem =
+ intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
+ if (uriForDefaultItem == null) {
+ uriForDefaultItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType);
}
- // Get whether to show the 'Silent' item
- mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
+ // Get whether this list has the 'Silent' item.
+ boolean hasSilentItem =
+ 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);
+ boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
+
+ // Get the URI whose list item should have a checkmark
+ Uri existingUri = intent
+ .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+
+ 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);
+
+ mRingtonePickerViewModel.init(new RingtonePickerViewModel.PickerConfig(title, pickerUserId,
+ ringtoneType, hasDefaultItem, uriForDefaultItem, hasSilentItem,
+ mAttributesFlags, existingUri, showOkCancelButtons, pickerType));
// The volume keys will control the stream that we are choosing a ringtone for
setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType());
- // Get the URI whose list item should have a checkmark
- mExistingUri = intent
- .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+ if (savedInstanceState == null) {
+ TabbedDialogFragment dialogFragment = new TabbedDialogFragment();
- // 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));
-
- AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this,
- android.R.style.ThemeOverlay_Material_Dialog);
- alertDialogBuilder
- .setSingleChoiceItems(mAdapter, getCheckedItem(), mRingtoneClickListener)
- .setOnItemSelectedListener(this)
- .setOnDismissListener(this);
- if (mShowOkCancelButtons) {
- alertDialogBuilder
- .setPositiveButton(getString(com.android.internal.R.string.ok), this)
- .setNegativeButton(getString(com.android.internal.R.string.cancel), this);
- }
-
- String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
- alertDialogBuilder.setTitle(
- title != null ? title : getString(RingtonePickerViewModel.getTitleByType(mType)));
-
- mAlertDialog = alertDialogBuilder.show();
- ListView listView = mAlertDialog.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.");
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG);
+ if (prev != null) {
+ ft.remove(prev);
}
- prepareListView(listView);
- }
- if (savedInstanceState != null) {
- setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN));
- }
- }
- @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) {
- mRingtonePickerViewModel.addRingtoneAsync(data.getData(),
- mType,
- mAddCustomRingtoneCallback,
- // Causes the callback to be executed on the main thread.
- ContextCompat.getMainExecutor(this.getApplicationContext()));
+ ft.addToBackStack(null);
+ dialogFragment.show(ft, TabbedDialogFragment.TAG);
}
}
- @Override
- public void onDismiss(DialogInterface dialog) {
- if (!isChangingConfigurations()) {
- finish();
- }
- }
@Override
public void onDestroy() {
mRingtonePickerViewModel.cancelPendingAsyncTasks();
- if (mAlertDialog != null && mAlertDialog.isShowing()) {
- mAlertDialog.dismiss();
- }
- if (mHandler != null) {
- mHandler.removeCallbacksAndMessages(null);
- }
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
super.onDestroy();
}
- private void prepareListView(@NonNull ListView listView) {
- // Reset the static item count, as this method can be called multiple times
- mRingtonePickerViewModel.resetFixedItemCount();
-
- if (mHasDefaultItem) {
- int defaultItemPos = addDefaultRingtoneItem(listView);
-
- if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
- setCheckedItem(defaultItemPos);
- }
- }
-
- if (mHasSilentItem) {
- int silentItemPos = addSilentItem(listView);
-
- // The 'Silent' item should use a null Uri
- if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) {
- setCheckedItem(silentItemPos);
- }
- }
-
- if (getCheckedItem() == POS_UNKNOWN) {
- setCheckedItem(
- getListPosition(mRingtonePickerViewModel.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(
- mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
- mUriForDefaultItem));
- }
- // If external storage is available, add a button to install sounds from storage.
- if (resolvesMediaFilePicker()
- && 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.
- mRingtonePickerViewModel.initRingtoneManager(mType);
- setupCursor();
- 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 = mRingtonePickerViewModel.getSilentItemPosition();
- }
- setCheckedItem(checkedPosition);
- }
-
- /**
- * 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(@NonNull 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);
- mRingtonePickerViewModel.incrementFixedItemCount();
- return listView.getHeaderViewsCount() - 1;
- }
-
- private int addDefaultRingtoneItem(@NonNull ListView listView) {
- int defaultRingtoneItemPos = addStaticItem(listView,
- RingtonePickerViewModel.getDefaultRingtoneItemTextByType(mType));
- mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos);
- return defaultRingtoneItemPos;
- }
-
- private int addSilentItem(@NonNull ListView listView) {
- int silentItemPos = addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
- mRingtonePickerViewModel.setSilentItemPosition(silentItemPos);
- return silentItemPos;
- }
-
- private void addNewSoundItem(@NonNull 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);
-
- text.setText(RingtonePickerViewModel.getAddNewItemTextByType(mType));
-
- listView.addFooterView(view);
- }
-
- private void setupCursor() {
- mCursor = new LocalizedCursor(
- mRingtonePickerViewModel.getRingtoneCursor(), getResources(), COLUMN_LABEL);
- }
-
- private int getCheckedItem() {
- return mCheckedItem;
- }
-
- private void setCheckedItem(int pos) {
- mCheckedItem = pos;
- ListView listView = mAlertDialog.getListView();
- if (listView != null) {
- listView.setItemChecked(pos, true);
- listView.smoothScrollToPosition(pos);
- }
- mCheckedItemId = mAdapter.getItemId(
- mRingtonePickerViewModel.itemPositionToRingtonePosition(pos));
- }
-
- /*
- * On click of Ok/Cancel buttons
- */
- public void onClick(DialogInterface dialog, int which) {
- boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
-
- if (positiveResult) {
- setSuccessResultWithRingtone(
- mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
- mUriForDefaultItem));
- } 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() + mRingtonePickerViewModel.getFixedItemCount()) {
- 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(
- mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
- mUriForDefaultItem));
- }
- }
-
- public void onNothingSelected(AdapterView parent) {
- }
-
- private void playRingtone(int position, int delayMs) {
- mHandler.removeCallbacks(this);
- mRingtonePickerViewModel.setSampleItemPosition(position);
- mHandler.postDelayed(this, delayMs);
- }
-
- public void run() {
- mRingtonePickerViewModel.playRingtone(
- mRingtonePickerViewModel.itemPositionToRingtonePosition(
- mRingtonePickerViewModel.getSampleItemPosition()), mUriForDefaultItem,
- mAttributesFlags);
- }
-
@Override
protected void onStop() {
super.onStop();
@@ -476,155 +136,29 @@
mRingtonePickerViewModel.onPause(isChangingConfigurations());
}
- private void setSuccessResultWithRingtone(Uri ringtoneUri) {
- setResult(RESULT_OK,
- new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
- }
-
- private int getListPosition(int ringtoneManagerPos) {
-
- // If the manager position is -1 (for not found), return that
- if (ringtoneManagerPos < 0) return ringtoneManagerPos;
-
- return ringtoneManagerPos + mRingtonePickerViewModel.getFixedItemCount();
- }
-
- 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;
- }
- }
+ /**
+ * 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;
}
- /**
- * 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 = mRingtonePickerViewModel.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);
- }
+ 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/RingtonePickerViewModel.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
index f045dc2..914f16a 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
@@ -27,6 +27,7 @@
import android.net.Uri;
import android.provider.Settings;
+import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import com.android.internal.annotations.VisibleForTesting;
@@ -50,6 +51,7 @@
public final class RingtonePickerViewModel extends ViewModel {
static final int RINGTONE_TYPE_UNKNOWN = -1;
+
/**
* Keep the currently playing ringtone around when changing orientation, so that it
* can be stopped later, after the activity is recreated.
@@ -72,16 +74,87 @@
private int mSampleItemPosition = ITEM_POSITION_UNKNOWN;
/** The position in the list of the 'Default' item. */
private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
- /** The number of static items in the list. */
+ /** The number of fixed items in the list. */
private int mFixedItemCount;
private ListenableFuture<Uri> mAddCustomRingtoneFuture;
private RingtoneManager mRingtoneManager;
/**
+ * Stable ID for the ringtone that is currently selected (may be -1 if no ringtone is selected).
+ */
+ private long mSelectedItemId = -1;
+ private int mSelectedItemPosition = ITEM_POSITION_UNKNOWN;
+
+ /**
* The ringtone that's currently playing.
*/
private Ringtone mCurrentRingtone;
+ private PickerConfig mPickerConfig;
+
+ public enum PickerType {
+ RINGTONE_PICKER,
+ SOUND_PICKER,
+ VIBRATION_PICKER
+ }
+
+ /**
+ * Holds immutable info on the picker that should be displayed.
+ */
+ static final class PickerConfig {
+ public final String title;
+ /**
+ * Id of the user to which the ringtone picker should list the ringtones.
+ */
+ public final int userId;
+ /**
+ * Ringtone type.
+ */
+ public final int ringtoneType;
+ /**
+ * Whether this list has the 'Default' item.
+ */
+ public final boolean hasDefaultItem;
+ /**
+ * The Uri to play when the 'Default' item is clicked.
+ */
+ public final Uri uriForDefaultItem;
+ /**
+ * Whether this list has the 'Silent' item.
+ */
+ public final boolean hasSilentItem;
+ /**
+ * AudioAttributes flags.
+ */
+ public final int audioAttributesFlags;
+ /**
+ * The Uri to place a checkmark next to.
+ */
+ public final Uri existingUri;
+ /**
+ * In the buttonless (watch-only) version we don't show the OK/Cancel buttons.
+ */
+ public final boolean showOkCancelButtons;
+
+ public final PickerType mPickerType;
+
+ PickerConfig(String title, int userId, int ringtoneType,
+ boolean hasDefaultItem, Uri uriForDefaultItem, boolean hasSilentItem,
+ int audioAttributesFlags, Uri existingUri, boolean showOkCancelButtons,
+ PickerType pickerType) {
+ this.title = title;
+ this.userId = userId;
+ this.ringtoneType = ringtoneType;
+ this.hasDefaultItem = hasDefaultItem;
+ this.uriForDefaultItem = uriForDefaultItem;
+ this.hasSilentItem = hasSilentItem;
+ this.audioAttributesFlags = audioAttributesFlags;
+ this.existingUri = existingUri;
+ this.showOkCancelButtons = showOkCancelButtons;
+ this.mPickerType = pickerType;
+ }
+ }
+
@Inject
RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory,
RingtoneFactory ringtoneFactory,
@@ -91,6 +164,13 @@
mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor();
}
+ @NonNull
+ PickerConfig getPickerConfig() {
+ return requireNonNull(mPickerConfig,
+ "PickerConfig was never set. Did you forget to call "
+ + "RingtonePickerViewModel#init?");
+ }
+
@StringRes
static int getTitleByType(int ringtoneType) {
switch (ringtoneType) {
@@ -138,10 +218,11 @@
}
}
- void initRingtoneManager(int type) {
+ void init(@NonNull PickerConfig pickerConfig) {
mRingtoneManager = mRingtoneManagerFactory.create();
- if (type != RINGTONE_TYPE_UNKNOWN) {
- mRingtoneManager.setType(type);
+ mPickerConfig = pickerConfig;
+ if (pickerConfig.ringtoneType != RINGTONE_TYPE_UNKNOWN) {
+ mRingtoneManager.setType(pickerConfig.ringtoneType);
}
}
@@ -166,7 +247,7 @@
*/
void cancelPendingAsyncTasks() {
if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) {
- mAddCustomRingtoneFuture.cancel(/*mayInterruptIfRunning=*/true);
+ mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true);
}
}
@@ -180,36 +261,48 @@
return mRingtoneManager.getCursor();
}
- Uri getRingtoneUri(int ringtonePosition) {
+ Uri getRingtoneUri(int position) {
requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE);
- return mRingtoneManager.getRingtoneUri(ringtonePosition);
+ return mRingtoneManager.getRingtoneUri(mapListPositionToRingtonePosition(position));
}
int getRingtonePosition(Uri uri) {
requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE);
- return mRingtoneManager.getRingtonePosition(uri);
+ return mapRingtonePositionToListPosition(mRingtoneManager.getRingtonePosition(uri));
}
/**
- * Returns the position of the item in the list before header views were added.
+ * Maps the item position in the list, to its equivalent position in the RingtoneManager.
*
- * @param itemPosition the position of item in the list with any added headers.
- * @return position of the item in the list ignoring headers.
+ * @param itemPosition the position of item in the list.
+ * @return position of the item in the RingtoneManager.
*/
- int itemPositionToRingtonePosition(int itemPosition) {
+ private int mapListPositionToRingtonePosition(int itemPosition) {
+ // If the manager position is -1 (for not found), then return that.
+ if (itemPosition < 0) return itemPosition;
+
return itemPosition - mFixedItemCount;
}
- int getFixedItemCount() {
- return mFixedItemCount;
+ /**
+ * Maps the item position in the RingtoneManager, to its equivalent position in the list.
+ *
+ * @param itemPosition the position of the item in the RingtoneManager.
+ * @return position of the item in the list.
+ */
+ private int mapRingtonePositionToListPosition(int itemPosition) {
+ // If the manager position is -1 (for not found), then return that.
+ if (itemPosition < 0) return itemPosition;
+
+ return itemPosition + mFixedItemCount;
}
void resetFixedItemCount() {
mFixedItemCount = 0;
}
- void incrementFixedItemCount() {
- mFixedItemCount++;
+ int incrementAndGetFixedItemCount() {
+ return mFixedItemCount++;
}
void setDefaultItemPosition(int defaultItemPosition) {
@@ -232,6 +325,22 @@
mSampleItemPosition = sampleItemPosition;
}
+ public int getSelectedItemPosition() {
+ return mSelectedItemPosition;
+ }
+
+ public void setSelectedItemPosition(int selectedItemPosition) {
+ mSelectedItemPosition = selectedItemPosition;
+ }
+
+ public void setSelectedItemId(long selectedItemId) {
+ mSelectedItemId = selectedItemId;
+ }
+
+ public long getSelectedItemId() {
+ return mSelectedItemId;
+ }
+
void onPause(boolean isChangingConfigurations) {
if (!isChangingConfigurations) {
stopAnyPlayingRingtone();
@@ -247,19 +356,19 @@
}
@Nullable
- Uri getCurrentlySelectedRingtoneUri(int checkedItem, Uri defaultUri) {
- if (checkedItem == ITEM_POSITION_UNKNOWN) {
- // When the getCheckItem is POS_UNKNOWN, it is not the case we expected.
+ Uri getCurrentlySelectedRingtoneUri() {
+ if (mSelectedItemPosition == ITEM_POSITION_UNKNOWN) {
+ // When the selected item is POS_UNKNOWN, it is not the case we expected.
// We return null for this case.
return null;
- } else if (checkedItem == mDefaultItemPosition) {
+ } else if (mSelectedItemPosition == mDefaultItemPosition) {
// Use the default Uri that they originally gave us.
- return defaultUri;
- } else if (checkedItem == mSilentItemPosition) {
+ return mPickerConfig.uriForDefaultItem;
+ } else if (mSelectedItemPosition == mSilentItemPosition) {
// Use a null Uri for the 'Silent' item.
return null;
} else {
- return getRingtoneUri(itemPositionToRingtonePosition(checkedItem));
+ return getRingtoneUri(mSelectedItemPosition);
}
}
@@ -280,7 +389,8 @@
mCurrentRingtone.setStreamType(mRingtoneManager.inferStreamType());
}
} else {
- mCurrentRingtone = mRingtoneManager.getRingtone(position);
+ mCurrentRingtone = mRingtoneManager.getRingtone(
+ mapListPositionToRingtonePosition(position));
}
if (mCurrentRingtone != null) {
diff --git a/packages/SoundPicker/src/com/android/soundpicker/SoundPickerFragment.java b/packages/SoundPicker/src/com/android/soundpicker/SoundPickerFragment.java
new file mode 100644
index 0000000..e07d109
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/SoundPickerFragment.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2023 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.Activity;
+import android.content.ContentProvider;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * A fragment that will display a picker used to select sound or silent. It also includes the
+ * ability to add custom sounds.
+ */
+@AndroidEntryPoint(Fragment.class)
+public class SoundPickerFragment extends Hilt_SoundPickerFragment {
+
+ private static final String TAG = "SoundPickerFragment";
+ private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
+ private static final int POS_UNKNOWN = -1;
+
+ private RingtonePickerViewModel.PickerConfig mPickerConfig;
+ private boolean mIsManagedProfile;
+ private RingtonePickerViewModel mRingtonePickerViewModel;
+ private RingtoneAdapter mRingtoneAdapter;
+ private RecyclerView mSoundRecyclerView;
+
+ private final RingtoneAdapter.WorkRingtoneProvider mWorkRingtoneProvider =
+ new RingtoneAdapter.WorkRingtoneProvider() {
+ private Drawable mWorkIconDrawable;
+ @Override
+ public boolean isWorkRingtone(int position) {
+ if (mIsManagedProfile) {
+ /*
+ * Display the w ork 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 = mRingtonePickerViewModel.getRingtoneUri(position);
+ int uriUserId = ContentProvider.getUserIdFromUri(currentUri,
+ mPickerConfig.userId);
+ Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
+
+ return uriUserId == mPickerConfig.userId
+ && uriWithoutUserId.toString().startsWith(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString());
+ }
+
+ return false;
+ }
+
+ @Override
+ public Drawable getWorkIconDrawable() {
+ if (mWorkIconDrawable == null) {
+ mWorkIconDrawable = requireActivity().getPackageManager()
+ .getUserBadgeForDensityNoBackground(
+ UserHandle.of(mPickerConfig.userId), /* density= */ -1);
+ }
+
+ return mWorkIconDrawable;
+ }
+ };
+
+ private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() {
+ @Override
+ public void onSuccess(Uri ringtoneUri) {
+ requeryForAdapter();
+ }
+
+ @Override
+ public void onFailure(Throwable throwable) {
+ Log.e(TAG, "Failed to add custom ringtone.", throwable);
+ // Ringtone was not added, display error Toast
+ Toast.makeText(requireActivity().getApplicationContext(),
+ R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show();
+ }
+ };
+
+ ActivityResultLauncher<Intent> mActivityResultLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ new ActivityResultCallback<ActivityResult>() {
+ @Override
+ public void onActivityResult(ActivityResult result) {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ // There are no request codes
+ Intent data = result.getData();
+ mRingtonePickerViewModel.addRingtoneAsync(data.getData(),
+ mPickerConfig.ringtoneType,
+ mAddCustomRingtoneCallback,
+ // Causes the callback to be executed on the main thread.
+ ContextCompat.getMainExecutor(
+ requireActivity().getApplicationContext()));
+ }
+ }
+ });
+
+ private final RingtoneAdapter.RingtoneSelectionListener mRingtoneSelectionListener =
+ new RingtoneAdapter.RingtoneSelectionListener() {
+ @Override
+ public void onRingtoneSelected(int position) {
+ SoundPickerFragment.this.setSelectedItem(position);
+
+ // 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 (!mPickerConfig.showOkCancelButtons) {
+ setSuccessResultWithRingtone(
+ mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
+ }
+
+ // Play clip
+ playRingtone(position);
+ }
+
+ @Override
+ public void onAddRingtoneSelected() {
+ // The "Add new ringtone" item was clicked. Start a file picker intent to
+ // select only audio files (MIME type "audio/*")
+ final Intent chooseFile = getMediaFilePickerIntent();
+ mActivityResultLauncher.launch(chooseFile);
+ }
+ };
+
+ public SoundPickerFragment() {
+ super(R.layout.fragment_sound_picker);
+ }
+
+ @Override
+ public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
+ RingtonePickerViewModel.class);
+ mSoundRecyclerView = view.findViewById(R.id.recycler_view);
+ Objects.requireNonNull(mSoundRecyclerView);
+
+ mPickerConfig = mRingtonePickerViewModel.getPickerConfig();
+ mIsManagedProfile = UserManager.get(requireActivity()).isManagedProfile(
+ mPickerConfig.userId);
+
+ mRingtoneAdapter = createRingtoneAdapter();
+ mSoundRecyclerView.setHasFixedSize(true);
+ mSoundRecyclerView.setAdapter(mRingtoneAdapter);
+ mSoundRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
+ setSelectedItem(mRingtonePickerViewModel.getSelectedItemPosition());
+ prepareRecyclerView(mSoundRecyclerView);
+ }
+
+ private void prepareRecyclerView(@NonNull RecyclerView recyclerView) {
+ // Reset the static item count, as this method can be called multiple times
+ mRingtonePickerViewModel.resetFixedItemCount();
+
+ if (mPickerConfig.hasDefaultItem) {
+ int defaultItemPos = addDefaultRingtoneItem();
+
+ if (getSelectedItem() == POS_UNKNOWN
+ && RingtoneManager.isDefault(mPickerConfig.existingUri)) {
+ setSelectedItem(defaultItemPos);
+ }
+ }
+
+ if (mPickerConfig.hasSilentItem) {
+ int silentItemPos = addSilentItem();
+
+ // The 'Silent' item should use a null Uri
+ if (getSelectedItem() == POS_UNKNOWN && mPickerConfig.existingUri == null) {
+ setSelectedItem(silentItemPos);
+ }
+ }
+
+ if (getSelectedItem() == POS_UNKNOWN) {
+ setSelectedItem(
+ mRingtonePickerViewModel.getRingtonePosition(mPickerConfig.existingUri));
+ }
+
+ // 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 (!mPickerConfig.showOkCancelButtons) {
+ setSuccessResultWithRingtone(
+ mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
+ }
+ // If external storage is available, add a button to install sounds from storage.
+ if (resolvesMediaFilePicker()
+ && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ addNewSoundItem();
+ }
+
+ // Enable context menu in ringtone items
+ registerForContextMenu(recyclerView);
+ }
+
+ /**
+ * 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.
+ * <p>
+ * 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.
+ mRingtonePickerViewModel.init(mPickerConfig);
+ mRingtoneAdapter = createRingtoneAdapter();
+ mSoundRecyclerView.setAdapter(mRingtoneAdapter);
+ prepareRecyclerView(mSoundRecyclerView);
+
+ // Update selected item location.
+ int selectedPosition = POS_UNKNOWN;
+ for (int i = 0; i < mRingtoneAdapter.getItemCount(); i++) {
+ if (mRingtoneAdapter.getItemId(i) == mRingtonePickerViewModel.getSelectedItemId()) {
+ selectedPosition = i;
+ break;
+ }
+ }
+ if (mPickerConfig.hasSilentItem && selectedPosition == POS_UNKNOWN) {
+ selectedPosition = mRingtonePickerViewModel.getSilentItemPosition();
+ }
+ setSelectedItem(selectedPosition);
+ }
+
+ private void playRingtone(int position) {
+ mRingtonePickerViewModel.setSampleItemPosition(position);
+ mRingtonePickerViewModel.playRingtone(mRingtonePickerViewModel.getSampleItemPosition(),
+ mPickerConfig.uriForDefaultItem, mPickerConfig.audioAttributesFlags);
+ }
+
+ private boolean resolvesMediaFilePicker() {
+ return getMediaFilePickerIntent().resolveActivity(requireActivity().getPackageManager())
+ != null;
+ }
+
+ 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 void setSuccessResultWithRingtone(Uri ringtoneUri) {
+ requireActivity().setResult(Activity.RESULT_OK,
+ new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
+ }
+
+ private int getSelectedItem() {
+ return mRingtonePickerViewModel.getSelectedItemPosition();
+ }
+
+ private void setSelectedItem(int pos) {
+ Objects.requireNonNull(mRingtoneAdapter);
+ mRingtonePickerViewModel.setSelectedItemPosition(pos);
+ mRingtoneAdapter.setSelectedItem(pos);
+ mRingtonePickerViewModel.setSelectedItemId(mRingtoneAdapter.getItemId(pos));
+ mSoundRecyclerView.scrollToPosition(pos);
+ }
+
+ /**
+ * Adds a fixed item to the fixed items list . A fixed item is one that is not from
+ * the RingtoneManager.
+ *
+ * @param textResId The resource ID of the text for the item.
+ * @return The position of the inserted item.
+ */
+ private int addFixedItem(int textResId) {
+ mRingtoneAdapter.addTitleForFixedItem(textResId);
+ return mRingtonePickerViewModel.incrementAndGetFixedItemCount();
+ }
+
+ private int addDefaultRingtoneItem() {
+ int defaultRingtoneItemPos = addFixedItem(
+ RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
+ mPickerConfig.ringtoneType));
+ mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos);
+ return defaultRingtoneItemPos;
+ }
+
+ private int addSilentItem() {
+ int silentItemPos = addFixedItem(com.android.internal.R.string.ringtone_silent);
+ mRingtonePickerViewModel.setSilentItemPosition(silentItemPos);
+ return silentItemPos;
+ }
+
+ private void addNewSoundItem() {
+ mRingtoneAdapter.addTitleForAddRingtoneItem(
+ RingtonePickerViewModel.getAddNewItemTextByType(mPickerConfig.ringtoneType));
+ }
+
+ private RingtoneAdapter createRingtoneAdapter() {
+ LocalizedCursor cursor = new LocalizedCursor(
+ mRingtonePickerViewModel.getRingtoneCursor(), getResources(), COLUMN_LABEL);
+ return new RingtoneAdapter(cursor, mRingtoneSelectionListener, mWorkRingtoneProvider);
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/TabbedDialogFragment.java b/packages/SoundPicker/src/com/android/soundpicker/TabbedDialogFragment.java
new file mode 100644
index 0000000..63140d2
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/TabbedDialogFragment.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 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 static android.app.Activity.RESULT_CANCELED;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * A dialog fragment with a sound and/or vibration tab based on the picker type.
+ * <ul>
+ * <li> Ringtone Pickers will display both sound and vibration tabs.
+ * <li> Sound Pickers will only display the sound tab.
+ * <li> Vibration Pickers will only display the vibration tab.
+ * </ul>
+ */
+@AndroidEntryPoint(DialogFragment.class)
+public class TabbedDialogFragment extends Hilt_TabbedDialogFragment {
+
+ static final String TAG = "TabbedDialogFragment";
+
+ private RingtonePickerViewModel mRingtonePickerViewModel;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
+ RingtonePickerViewModel.class);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(),
+ android.R.style.ThemeOverlay_Material_Dialog)
+ .setTitle(mRingtonePickerViewModel.getPickerConfig().title);
+ // Do not show OK/Cancel buttons in the buttonless (watch-only) version.
+ if (mRingtonePickerViewModel.getPickerConfig().showOkCancelButtons) {
+ dialogBuilder
+ .setPositiveButton(getString(com.android.internal.R.string.ok),
+ (dialog, whichButton) -> {
+ setSuccessResultWithRingtone(
+ mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
+ requireActivity().finish();
+ })
+ .setNegativeButton(getString(com.android.internal.R.string.cancel),
+ (dialog, whichButton) -> {
+ requireActivity().setResult(RESULT_CANCELED);
+ requireActivity().finish();
+ });
+ }
+
+ View view = buildTabbedView(requireActivity().getLayoutInflater());
+ dialogBuilder.setView(view);
+
+ return dialogBuilder.create();
+ }
+
+ @Override
+ public void onCancel(@NonNull @NotNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ if (!requireActivity().isChangingConfigurations()) {
+ requireActivity().finish();
+ }
+ }
+
+ @Override
+ public void onDismiss(@NonNull @NotNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (!requireActivity().isChangingConfigurations()) {
+ requireActivity().finish();
+ }
+ }
+
+ private void setSuccessResultWithRingtone(Uri ringtoneUri) {
+ requireActivity().setResult(Activity.RESULT_OK,
+ new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
+ }
+
+ /**
+ * Inflates the tabbed layout view and adds the required fragments. If there's only one
+ * fragment to display, then the tab area is hidden.
+ * @param inflater The LayoutInflater that is used to inflate the tabbed view.
+ * @return The tabbed view.
+ */
+ private View buildTabbedView(@NonNull LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.fragment_tabbed_dialog, null, false);
+ TabLayout tabLayout = view.findViewById(R.id.tabLayout);
+ ViewPager2 viewPager = view.findViewById(R.id.masterViewPager);
+ Objects.requireNonNull(tabLayout);
+ Objects.requireNonNull(viewPager);
+
+ ViewPagerAdapter adapter = new ViewPagerAdapter(requireActivity());
+ addFragments(adapter);
+
+ if (adapter.getItemCount() == 1) {
+ // Hide the tab area since there's only one fragment to display.
+ tabLayout.setVisibility(View.GONE);
+ }
+
+ viewPager.setAdapter(adapter);
+ new TabLayoutMediator(tabLayout, viewPager,
+ (tab, position) -> tab.setText(adapter.getTitle(position))).attach();
+
+ return view;
+ }
+
+ /**
+ * Adds the appropriate fragments to the adapter based on the PickerType.
+ *
+ * @param adapter The adapter to add the fragments to.
+ */
+ private void addFragments(ViewPagerAdapter adapter) {
+ switch (mRingtonePickerViewModel.getPickerConfig().mPickerType) {
+ case RINGTONE_PICKER:
+ adapter.addFragment(getString(R.string.sound_page_title),
+ new SoundPickerFragment());
+ adapter.addFragment(getString(R.string.vibration_page_title),
+ new VibrationPickerFragment());
+ break;
+ case SOUND_PICKER:
+ adapter.addFragment(getString(R.string.sound_page_title),
+ new SoundPickerFragment());
+ break;
+ case VIBRATION_PICKER:
+ adapter.addFragment(getString(R.string.vibration_page_title),
+ new VibrationPickerFragment());
+ break;
+ default:
+ adapter.addFragment(getString(R.string.sound_page_title),
+ new SoundPickerFragment());
+ break;
+ }
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/VibrationPickerFragment.java b/packages/SoundPicker/src/com/android/soundpicker/VibrationPickerFragment.java
new file mode 100644
index 0000000..356b9ae
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/VibrationPickerFragment.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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 androidx.fragment.app.Fragment;
+
+/**
+ * A fragment that will display a picker used to select vibration.
+ */
+public class VibrationPickerFragment extends Fragment {
+
+ public VibrationPickerFragment() {
+ super(R.layout.fragment_vibration_picker);
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/ViewPagerAdapter.java b/packages/SoundPicker/src/com/android/soundpicker/ViewPagerAdapter.java
new file mode 100644
index 0000000..179068e
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/ViewPagerAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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 androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An adapter used to populate pages inside a ViewPager.
+ */
+public class ViewPagerAdapter extends FragmentStateAdapter {
+
+ private final List<Fragment> mFragments = new ArrayList<>();
+ private final List<String> mTitles = new ArrayList<>();
+
+ public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
+ super(fragmentActivity);
+ }
+
+ /**
+ * Adds a fragment and page title to the adapter.
+ * @param title the title of the page in the ViewPager.
+ * @param fragment the fragment that will be inflated on this page.
+ */
+ public void addFragment(String title, Fragment fragment) {
+ mTitles.add(title);
+ mFragments.add(fragment);
+ }
+
+ /**
+ * Returns the title of the requested page.
+ * @param position the position of the page in the Viewpager.
+ * @return The title of the requested page.
+ */
+ public String getTitle(int position) {
+ return mTitles.get(position);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ return Objects.requireNonNull(mFragments.get(position),
+ "Could not find a fragment using position: " + position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFragments.size();
+ }
+}
diff --git a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
index 9ef3aa3..659aae8 100644
--- a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
+++ b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
@@ -112,7 +112,7 @@
@Test
public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() {
- mViewModel.initRingtoneManager(RINGTONE_TYPE_UNKNOWN);
+ mViewModel.init(createPickerConfig(RINGTONE_TYPE_UNKNOWN));
verify(mMockRingtoneManagerFactory).create();
verify(mMockRingtoneManager, never()).setType(anyInt());
@@ -120,7 +120,7 @@
@Test
public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_NOTIFICATION);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION));
verify(mMockRingtoneManagerFactory).create();
verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
@@ -129,14 +129,14 @@
@Test
public void testGetStreamType_returnsTheCorrectStreamType() {
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM);
}
@Test
public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() {
when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
assertEquals(mViewModel.getRingtoneCursor(), mMockCursor);
}
@@ -144,14 +144,14 @@
public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() {
Uri expectedUri = DEFAULT_URI;
when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(expectedUri);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
Uri actualUri = mViewModel.getRingtoneUri(DEFAULT_RINGTONE_POSITION);
assertEquals(actualUri, expectedUri);
}
@Test
public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
@@ -161,7 +161,7 @@
@Test
public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -172,7 +172,7 @@
@Test
public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
Ringtone mockRingtone1 = createMockRingtone();
Ringtone mockRingtone2 = createMockRingtone();
@@ -186,7 +186,7 @@
verify(mockRingtone1, never()).stop();
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
mMockListeningExecutorServiceFactory);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -197,7 +197,7 @@
@Test
public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -208,7 +208,7 @@
@Test
public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -226,7 +226,7 @@
@Test
public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -237,21 +237,24 @@
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsUnknown_returnsNull() {
- Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(POS_UNKNOWN, DEFAULT_URI);
+ mViewModel.setSelectedItemPosition(POS_UNKNOWN);
+ Uri uri = mViewModel.getCurrentlySelectedRingtoneUri();
assertNull(uri);
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsDefaultPos_returnsDefaultUri() {
Uri expectedUri = DEFAULT_URI;
- Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(DEFAULT_RINGTONE_POSITION,
- expectedUri);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
+ mViewModel.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
+ Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri();
assertEquals(actualUri, expectedUri);
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsSilentPos_returnsNull() {
- Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(SILENT_RINGTONE_POSITION, DEFAULT_URI);
+ mViewModel.setSelectedItemPosition(SILENT_RINGTONE_POSITION);
+ Uri uri = mViewModel.getCurrentlySelectedRingtoneUri();
assertNull(uri);
}
@@ -266,7 +269,7 @@
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI);
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
mMockListeningExecutorServiceFactory);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
mMainThreadExecutor);
verify(mockCallback, never()).onFailure(any());
@@ -290,7 +293,7 @@
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI);
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
mMockListeningExecutorServiceFactory);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback1,
mMainThreadExecutor);
verify(mockCallback1, never()).onFailure(any());
@@ -312,7 +315,7 @@
when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI,
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
mMainThreadExecutor);
@@ -330,7 +333,7 @@
when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow(
IOException.class);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
mMainThreadExecutor);
@@ -342,8 +345,9 @@
public void testGetCurrentlySelectedRingtoneUri_checkedItemRingtonePos_returnsTheCorrectUri() {
Uri expectedUri = DEFAULT_URI;
when(mMockRingtoneManager.getRingtoneUri(RINGTONE_POSITION)).thenReturn(expectedUri);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
- Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(RINGTONE_POSITION, DEFAULT_URI);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
+ mViewModel.setSelectedItemPosition(RINGTONE_POSITION);
+ Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri();
verify(mMockRingtoneManager).getRingtoneUri(RINGTONE_POSITION);
assertEquals(actualUri, expectedUri);
@@ -353,7 +357,7 @@
public void testPlayRingtone_stopsPreviouslyRunningRingtone() {
// Start playing the first ringtone
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
@@ -369,7 +373,7 @@
@Test
public void testPlayRingtone_samplePosEqualToSilentPos_onlyStopPlayingRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
@@ -388,7 +392,7 @@
@Test
public void testPlayRingtone_samplePosEqualToDefaultPos_playDefaultRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
@@ -403,7 +407,7 @@
@Test
public void testPlayRingtone_samplePosNotEqualToDefaultPos_playRingtone() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
@@ -417,7 +421,7 @@
@Test
public void testPlayRingtone_withNoAttributeFlags_doNotUpdateRingtoneAttributesFlags() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
NO_ATTRIBUTES_FLAGS);
@@ -430,7 +434,7 @@
public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() {
int expectedPosition = 1;
when(mMockRingtoneManager.getRingtonePosition(any())).thenReturn(expectedPosition);
- mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
+ mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
int actualPosition = mViewModel.getRingtonePosition(DEFAULT_URI);
assertEquals(actualPosition, expectedPosition);
@@ -553,4 +557,13 @@
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
}
+
+ private RingtonePickerViewModel.PickerConfig createPickerConfig(int ringtoneType) {
+ return new RingtonePickerViewModel.PickerConfig("Phone ringtone", /* userId= */ 1,
+ ringtoneType, /* hasDefaultItem= */ true,
+ /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
+ /* audioAttributesFlags= */0, /* existingUri= */ Uri.parse(""),
+ /* showOkCancelButtons= */ true,
+ RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
+ }
}