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