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
This commit is contained in:
Ahmad Khalil
2023-05-26 13:57:08 +00:00
parent 855cd90c78
commit 6211a67919
18 changed files with 1349 additions and 601 deletions

View File

@@ -19,6 +19,10 @@ android_library {
"androidx.appcompat_appcompat",
"hilt_android",
"guava",
"androidx.recyclerview_recyclerview",
"androidx-constraintlayout_constraintlayout",
"androidx.viewpager2_viewpager2",
"com.google.android.material_material",
],
}

View File

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

View File

@@ -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" />
android:layout_height="match_parent"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 @@ import java.util.regex.Pattern;
* @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 @@ public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity im
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 @@ public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity im
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;
}
}
}

View File

@@ -27,6 +27,7 @@ import android.media.RingtoneManager;
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 @@ import javax.inject.Inject;
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 @@ public final class RingtonePickerViewModel extends ViewModel {
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 @@ public final class RingtonePickerViewModel extends ViewModel {
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 @@ public final class RingtonePickerViewModel extends ViewModel {
}
}
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 @@ public final class RingtonePickerViewModel extends ViewModel {
*/
void cancelPendingAsyncTasks() {
if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) {
mAddCustomRingtoneFuture.cancel(/*mayInterruptIfRunning=*/true);
mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true);
}
}
@@ -180,36 +261,48 @@ public final class RingtonePickerViewModel extends ViewModel {
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 @@ public final class RingtonePickerViewModel extends ViewModel {
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 @@ public final class RingtonePickerViewModel extends ViewModel {
}
@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 @@ public final class RingtonePickerViewModel extends ViewModel {
mCurrentRingtone.setStreamType(mRingtoneManager.inferStreamType());
}
} else {
mCurrentRingtone = mRingtoneManager.getRingtone(position);
mCurrentRingtone = mRingtoneManager.getRingtone(
mapListPositionToRingtonePosition(position));
}
if (mCurrentRingtone != null) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,7 +112,7 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
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 class RingtonePickerViewModelTest {
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 class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 @@ public class RingtonePickerViewModelTest {
@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 class RingtonePickerViewModelTest {
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 @@ public class RingtonePickerViewModelTest {
.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);
}
}