Merge "Move RingtoneManager out of the activity and into the ViewModel. And add unit tests to cover the new logic in the ViewModel"

This commit is contained in:
Ahmad Khalil
2023-04-28 20:19:57 +00:00
committed by Android (Google) Code Review
5 changed files with 648 additions and 174 deletions

View File

@@ -0,0 +1,44 @@
/*
* 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.Context;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
/**
* A factory class used to create {@link Ringtone}.
*/
public class RingtoneFactory {
private final Context mApplicationContext;
RingtoneFactory(Context context) {
mApplicationContext = context.getApplicationContext();
}
/**
* Returns a {@link Ringtone} based on the provided URI.
*
* @param uri The URI used to get the {@link Ringtone}
* @return a {@link Ringtone}
*/
public Ringtone create(Uri uri) {
return RingtoneManager.getRingtone(mApplicationContext, uri);
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.Context;
import android.media.RingtoneManager;
/**
* A factory class used to create {@link RingtoneManager}.
*/
public class RingtoneManagerFactory {
private final Context mApplicationContext;
RingtoneManagerFactory(Context context) {
mApplicationContext = context.getApplicationContext();
}
/**
* Creates a new {@link RingtoneManager} and returns it.
*
* @return a {@link RingtoneManager}
*/
public RingtoneManager create() {
return new RingtoneManager(mApplicationContext, /* includeParentRingtones */ true);
}
}

View File

@@ -24,8 +24,6 @@ import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
@@ -50,7 +48,6 @@ import android.widget.Toast;
import com.android.internal.app.AlertActivity;
import com.android.internal.app.AlertController;
import java.io.IOException;
import java.util.regex.Pattern;
/**
@@ -77,31 +74,20 @@ public final class RingtonePickerActivity extends AlertActivity implements
private static final int ADD_FILE_REQUEST_CODE = 300;
private RingtoneManager mRingtoneManager;
private RingtonePickerViewModel mRingtonePickerViewModel;
private int mType;
private Cursor mCursor;
private Handler mHandler;
private BadgedRingtoneAdapter mAdapter;
/** The position in the list of the 'Silent' item. */
private int mSilentPos = POS_UNKNOWN;
/** The position in the list of the 'Default' item. */
private int mDefaultRingtonePos = POS_UNKNOWN;
/** The position in the list of the ringtone to sample. */
private int mSampleRingtonePos = POS_UNKNOWN;
/** Whether this list has the 'Silent' item. */
private boolean mHasSilentItem;
/** The Uri to place a checkmark next to. */
private Uri mExistingUri;
/** The number of static items in the list. */
private int mStaticItemCount;
/** Whether this list has the 'Default' item. */
private boolean mHasDefaultItem;
@@ -111,22 +97,6 @@ public final class RingtonePickerActivity extends AlertActivity implements
/** Id of the user to which the ringtone picker should list the ringtones */
private int mPickerUserId;
/** Context of the user specified by mPickerUserId */
private Context mTargetContext;
/**
* A Ringtone for the default ringtone. In most cases, the RingtoneManager
* will stop the previous ringtone. However, the RingtoneManager doesn't
* manage the default ringtone for us, so we should stop this one manually.
*/
private Ringtone mDefaultRingtone;
/**
* The ringtone that's currently playing, unless the currently playing one is the default
* ringtone.
*/
private Ringtone mCurrentRingtone;
/**
* Stable ID for the ringtone that is currently checked (may be -1 if no ringtone is checked).
*/
@@ -136,20 +106,14 @@ public final class RingtonePickerActivity extends AlertActivity implements
private boolean mShowOkCancelButtons;
/**
* Keep the currently playing ringtone around when changing orientation, so that it
* can be stopped later, after the activity is recreated.
*/
private static Ringtone sPlayingRingtone;
private DialogInterface.OnClickListener mRingtoneClickListener =
private final DialogInterface.OnClickListener mRingtoneClickListener =
new DialogInterface.OnClickListener() {
/*
* On item clicked
*/
public void onClick(DialogInterface dialog, int which) {
if (which == mCursor.getCount() + mStaticItemCount) {
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();
@@ -163,7 +127,9 @@ public final class RingtonePickerActivity extends AlertActivity implements
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
setSuccessResultWithRingtone(
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
mUriForDefaultItem));
}
// Play clip
@@ -175,16 +141,18 @@ public final class RingtonePickerActivity extends AlertActivity implements
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRingtonePickerViewModel = new RingtonePickerViewModel(
new RingtoneManagerFactory(this), new RingtoneFactory(this));
mHandler = new Handler();
Intent intent = getIntent();
mPickerUserId = UserHandle.myUserId();
mTargetContext = this;
// Get the types of ringtones to show
mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1);
initRingtoneManager();
mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN);
mRingtonePickerViewModel.setRingtoneType(mType);
setupCursor();
/*
* Get whether to show the 'Default' item, and the URI to play when the
@@ -206,7 +174,7 @@ public final class RingtonePickerActivity extends AlertActivity implements
mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
// The volume keys will control the stream that we are choosing a ringtone for
setVolumeControlStream(mRingtoneManager.inferStreamType());
setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType());
// Get the URI whose list item should have a checkmark
mExistingUri = intent
@@ -262,12 +230,7 @@ public final class RingtonePickerActivity extends AlertActivity implements
final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() {
@Override
protected Uri doInBackground(Uri... params) {
try {
return mRingtoneManager.addCustomExternalRingtone(params[0], mType);
} catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Unable to add new ringtone", e);
}
return null;
return mRingtonePickerViewModel.addRingtone(params[0], mType);
}
@Override
@@ -335,33 +298,36 @@ public final class RingtonePickerActivity extends AlertActivity implements
public void onPrepareListView(ListView listView) {
// Reset the static item count, as this method can be called multiple times
mStaticItemCount = 0;
mRingtonePickerViewModel.resetFixedItemCount();
if (mHasDefaultItem) {
mDefaultRingtonePos = addDefaultRingtoneItem(listView);
int defaultItemPos = addDefaultRingtoneItem(listView);
if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
setCheckedItem(mDefaultRingtonePos);
setCheckedItem(defaultItemPos);
}
}
if (mHasSilentItem) {
mSilentPos = addSilentItem(listView);
int silentItemPos = addSilentItem(listView);
// The 'Silent' item should use a null Uri
if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) {
setCheckedItem(mSilentPos);
setCheckedItem(silentItemPos);
}
}
if (getCheckedItem() == POS_UNKNOWN) {
setCheckedItem(getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri)));
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(getCurrentlySelectedRingtoneUri());
setSuccessResultWithRingtone(
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
mUriForDefaultItem));
}
// If external storage is available, add a button to install sounds from storage.
if (resolvesMediaFilePicker()
@@ -381,7 +347,8 @@ public final class RingtonePickerActivity extends AlertActivity implements
*/
private void requeryForAdapter() {
// Refresh and set a new cursor, closing the old one.
initRingtoneManager();
mRingtonePickerViewModel.initRingtoneManager(mType);
setupCursor();
mAdapter.changeCursor(mCursor);
// Update checked item location.
@@ -393,7 +360,7 @@ public final class RingtonePickerActivity extends AlertActivity implements
}
}
if (mHasSilentItem && checkedPosition == POS_UNKNOWN) {
checkedPosition = mSilentPos;
checkedPosition = mRingtonePickerViewModel.getSilentItemPosition();
}
setCheckedItem(checkedPosition);
setupAlert();
@@ -412,17 +379,21 @@ public final class RingtonePickerActivity extends AlertActivity implements
com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false);
textView.setText(textResId);
listView.addHeaderView(textView);
mStaticItemCount++;
mRingtonePickerViewModel.incrementFixedItemCount();
return listView.getHeaderViewsCount() - 1;
}
private int addDefaultRingtoneItem(ListView listView) {
return addStaticItem(listView,
int defaultRingtoneItemPos = addStaticItem(listView,
RingtonePickerViewModel.getDefaultRingtoneItemTextByType(mType));
mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos);
return defaultRingtoneItemPos;
}
private int addSilentItem(ListView listView) {
return addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
int silentItemPos = addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
mRingtonePickerViewModel.setSilentItemPosition(silentItemPos);
return silentItemPos;
}
private void addNewSoundItem(ListView listView) {
@@ -435,21 +406,9 @@ public final class RingtonePickerActivity extends AlertActivity implements
listView.addFooterView(view);
}
private void initRingtoneManager() {
// Reinstantiate the RingtoneManager. Cursor.requery() was deprecated and calling it
// causes unexpected behavior.
mRingtoneManager = new RingtoneManager(mTargetContext, /* includeParentRingtones */ true);
if (mType != -1) {
mRingtoneManager.setType(mType);
}
mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL);
}
private Ringtone getRingtone(int ringtoneManagerPosition) {
if (ringtoneManagerPosition < 0) {
return null;
}
return mRingtoneManager.getRingtone(ringtoneManagerPosition);
private void setupCursor() {
mCursor = new LocalizedCursor(
mRingtonePickerViewModel.getRingtoneCursor(), getResources(), COLUMN_LABEL);
}
private int getCheckedItem() {
@@ -458,7 +417,8 @@ public final class RingtonePickerActivity extends AlertActivity implements
private void setCheckedItem(int pos) {
mAlertParams.mCheckedItem = pos;
mCheckedItemId = mAdapter.getItemId(getRingtoneManagerPosition(pos));
mCheckedItemId = mAdapter.getItemId(
mRingtonePickerViewModel.itemPositionToRingtonePosition(pos));
}
/*
@@ -467,11 +427,10 @@ public final class RingtonePickerActivity extends AlertActivity implements
public void onClick(DialogInterface dialog, int which) {
boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
// Stop playing the previous ringtone
mRingtoneManager.stopPreviousRingtone();
if (positiveResult) {
setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
setSuccessResultWithRingtone(
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
mUriForDefaultItem));
} else {
setResult(RESULT_CANCELED);
}
@@ -484,7 +443,7 @@ public final class RingtonePickerActivity extends AlertActivity implements
*/
public void onItemSelected(AdapterView parent, View view, int position, long id) {
// footer view
if (position >= mCursor.getCount() + mStaticItemCount) {
if (position >= mCursor.getCount() + mRingtonePickerViewModel.getFixedItemCount()) {
return;
}
@@ -493,7 +452,9 @@ public final class RingtonePickerActivity extends AlertActivity implements
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
setSuccessResultWithRingtone(
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
mUriForDefaultItem));
}
}
@@ -502,63 +463,27 @@ public final class RingtonePickerActivity extends AlertActivity implements
private void playRingtone(int position, int delayMs) {
mHandler.removeCallbacks(this);
mSampleRingtonePos = position;
mRingtonePickerViewModel.setSampleItemPosition(position);
mHandler.postDelayed(this, delayMs);
}
public void run() {
stopAnyPlayingRingtone();
if (mSampleRingtonePos == mSilentPos) {
return;
}
Ringtone ringtone;
if (mSampleRingtonePos == mDefaultRingtonePos) {
if (mDefaultRingtone == null) {
mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem);
}
/*
* Stream type of mDefaultRingtone is not set explicitly here.
* It should be set in accordance with mRingtoneManager of this Activity.
*/
if (mDefaultRingtone != null) {
mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType());
}
ringtone = mDefaultRingtone;
mCurrentRingtone = null;
} else {
ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos));
mCurrentRingtone = ringtone;
}
if (ringtone != null) {
if (mAttributesFlags != 0) {
ringtone.setAudioAttributes(
new AudioAttributes.Builder(ringtone.getAudioAttributes())
.setFlags(mAttributesFlags)
.build());
}
ringtone.play();
}
mRingtonePickerViewModel.playRingtone(
mRingtonePickerViewModel.itemPositionToRingtonePosition(
mRingtonePickerViewModel.getSampleItemPosition()), mUriForDefaultItem,
mAttributesFlags);
}
@Override
protected void onStop() {
super.onStop();
if (!isChangingConfigurations()) {
stopAnyPlayingRingtone();
} else {
saveAnyPlayingRingtone();
}
mRingtonePickerViewModel.onStop(isChangingConfigurations());
}
@Override
protected void onPause() {
super.onPause();
if (!isChangingConfigurations()) {
stopAnyPlayingRingtone();
}
mRingtonePickerViewModel.onPause(isChangingConfigurations());
}
private void setSuccessResultWithRingtone(Uri ringtoneUri) {
@@ -566,55 +491,13 @@ public final class RingtonePickerActivity extends AlertActivity implements
new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
}
private Uri getCurrentlySelectedRingtoneUri() {
if (getCheckedItem() == POS_UNKNOWN) {
// When the getCheckItem is POS_UNKNOWN, it is not the case we expected.
// We return null for this case.
return null;
} else if (getCheckedItem() == mDefaultRingtonePos) {
// Use the default Uri that they originally gave us.
return mUriForDefaultItem;
} else if (getCheckedItem() == mSilentPos) {
// Use a null Uri for the 'Silent' item.
return null;
} else {
return mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
}
}
private void saveAnyPlayingRingtone() {
if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
sPlayingRingtone = mDefaultRingtone;
} else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
sPlayingRingtone = mCurrentRingtone;
}
}
private void stopAnyPlayingRingtone() {
if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
sPlayingRingtone.stop();
}
sPlayingRingtone = null;
if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
mDefaultRingtone.stop();
}
if (mRingtoneManager != null) {
mRingtoneManager.stopPreviousRingtone();
}
}
private int getRingtoneManagerPosition(int listPos) {
return listPos - mStaticItemCount;
}
private int getListPosition(int ringtoneManagerPos) {
// If the manager position is -1 (for not found), return that
if (ringtoneManagerPos < 0) return ringtoneManagerPos;
return ringtoneManagerPos + mStaticItemCount;
return ringtoneManagerPos + mRingtonePickerViewModel.getFixedItemCount();
}
private Intent getMediaFilePickerIntent() {
@@ -735,7 +618,7 @@ public final class RingtonePickerActivity extends AlertActivity implements
* ringtone Uri is in external storage, and either the uri has no user id or has the
* id of the picker user
*/
Uri currentUri = mRingtoneManager.getRingtoneUri(cursor.getPosition());
Uri currentUri = mRingtonePickerViewModel.getRingtoneUri(cursor.getPosition());
int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId);
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);

View File

@@ -16,16 +16,61 @@
package com.android.soundpicker;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.database.Cursor;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.provider.Settings;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
/**
* View model for {@link RingtonePickerActivity}.
*/
public final class RingtonePickerViewModel {
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.
*/
@VisibleForTesting
static Ringtone sPlayingRingtone;
private static final String TAG = "RingtonePickerViewModel";
private static final int ITEM_POSITION_UNKNOWN = -1;
private final RingtoneManagerFactory mRingtoneManagerFactory;
private final RingtoneFactory mRingtoneFactory;
/** The position in the list of the 'Silent' item. */
private int mSilentItemPosition = ITEM_POSITION_UNKNOWN;
/** The position in the list of the ringtone to sample. */
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. */
private int mFixedItemCount;
private RingtoneManager mRingtoneManager;
/**
* The ringtone that's currently playing.
*/
private Ringtone mCurrentRingtone;
RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory,
RingtoneFactory ringtoneFactory) {
mRingtoneManagerFactory = ringtoneManagerFactory;
mRingtoneFactory = ringtoneFactory;
initRingtoneManager(RINGTONE_TYPE_UNKNOWN);
}
@StringRes
static int getTitleByType(int ringtoneType) {
switch (ringtoneType) {
@@ -72,4 +117,168 @@ public final class RingtonePickerViewModel {
return R.string.ringtone_default;
}
}
void initRingtoneManager(int type) {
mRingtoneManager = mRingtoneManagerFactory.create();
setRingtoneType(type);
}
void setRingtoneType(int type) {
if (type != RINGTONE_TYPE_UNKNOWN) {
mRingtoneManager.setType(type);
}
}
int getRingtoneStreamType() {
return mRingtoneManager.inferStreamType();
}
Cursor getRingtoneCursor() {
return mRingtoneManager.getCursor();
}
Uri getRingtoneUri(int ringtonePosition) {
return mRingtoneManager.getRingtoneUri(ringtonePosition);
}
int getRingtonePosition(Uri uri) {
return mRingtoneManager.getRingtonePosition(uri);
}
/**
* Returns the position of the item in the list before header views were added.
*
* @param itemPosition the position of item in the list with any added headers.
* @return position of the item in the list ignoring headers.
*/
int itemPositionToRingtonePosition(int itemPosition) {
return itemPosition - mFixedItemCount;
}
int getFixedItemCount() {
return mFixedItemCount;
}
void resetFixedItemCount() {
mFixedItemCount = 0;
}
void incrementFixedItemCount() {
mFixedItemCount++;
}
void setDefaultItemPosition(int defaultItemPosition) {
mDefaultItemPosition = defaultItemPosition;
}
int getSilentItemPosition() {
return mSilentItemPosition;
}
void setSilentItemPosition(int silentItemPosition) {
mSilentItemPosition = silentItemPosition;
}
public int getSampleItemPosition() {
return mSampleItemPosition;
}
public void setSampleItemPosition(int sampleItemPosition) {
mSampleItemPosition = sampleItemPosition;
}
void onPause(boolean isChangingConfigurations) {
if (!isChangingConfigurations) {
stopAnyPlayingRingtone();
}
}
void onStop(boolean isChangingConfigurations) {
if (isChangingConfigurations) {
saveAnyPlayingRingtone();
} else {
stopAnyPlayingRingtone();
}
}
@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.
// We return null for this case.
return null;
} else if (checkedItem == mDefaultItemPosition) {
// Use the default Uri that they originally gave us.
return defaultUri;
} else if (checkedItem == mSilentItemPosition) {
// Use a null Uri for the 'Silent' item.
return null;
} else {
return getRingtoneUri(itemPositionToRingtonePosition(checkedItem));
}
}
/**
* Adds an audio file to the list of ringtones.
* @param uri Uri of the file to be added as ringtone. Must be a media file.
* @param type The type of the ringtone to be added.
* @return The Uri of the installed ringtone, which may be the {@code uri} if it
* is already in ringtone storage. Or null if it failed to add the audio file.
*/
@Nullable
Uri addRingtone(Uri uri, int type) {
try {
return mRingtoneManager.addCustomExternalRingtone(uri, type);
} catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Unable to add new ringtone", e);
}
return null;
}
void playRingtone(int position, Uri uriForDefaultItem, int attributesFlags) {
stopAnyPlayingRingtone();
if (mSampleItemPosition == mSilentItemPosition) {
return;
}
if (mSampleItemPosition == mDefaultItemPosition) {
mCurrentRingtone = mRingtoneFactory.create(uriForDefaultItem);
/*
* Stream type of mDefaultRingtone is not set explicitly here. It should be set in
* accordance with mRingtoneManager of this Activity.
*/
if (mCurrentRingtone != null) {
mCurrentRingtone.setStreamType(mRingtoneManager.inferStreamType());
}
} else {
mCurrentRingtone = mRingtoneManager.getRingtone(position);
}
if (mCurrentRingtone != null) {
if (attributesFlags != 0) {
mCurrentRingtone.setAudioAttributes(new AudioAttributes.Builder(
mCurrentRingtone.getAudioAttributes()).setFlags(attributesFlags).build());
}
mCurrentRingtone.play();
}
}
private void saveAnyPlayingRingtone() {
if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
sPlayingRingtone = mCurrentRingtone;
}
mCurrentRingtone = null;
}
private void stopAnyPlayingRingtone() {
if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
sPlayingRingtone.stop();
}
sPlayingRingtone = null;
if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
mCurrentRingtone.stop();
}
mCurrentRingtone = null;
}
}

View File

@@ -17,19 +17,294 @@
package com.android.soundpicker;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.database.Cursor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.provider.Settings;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
public class RingtonePickerViewModelTest {
private static final Uri DEFAULT_URI = Uri.parse("https://www.google.com/login.html");
private static final int POS_UNKNOWN = -1;
private static final int NO_ATTRIBUTES_FLAGS = 0;
private static final int SILENT_RINGTONE_POSITION = 0;
private static final int DEFAULT_RINGTONE_POSITION = 1;
private static final int RINGTONE_POSITION = 2;
@Mock
private RingtoneManagerFactory mMockRingtoneManagerFactory;
@Mock
private RingtoneFactory mMockRingtoneFactory;
@Mock
private RingtoneManager mMockRingtoneManager;
@Mock
private Cursor mMockCursor;
private Ringtone mMockDefaultRingtone;
private Ringtone mMockRingtone;
private RingtonePickerViewModel mViewModel;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mMockRingtoneManagerFactory.create()).thenReturn(mMockRingtoneManager);
mMockDefaultRingtone = createMockRingtone();
mMockRingtone = createMockRingtone();
when(mMockRingtoneFactory.create(DEFAULT_URI)).thenReturn(mMockDefaultRingtone);
when(mMockRingtoneManager.getRingtone(anyInt())).thenReturn(mMockRingtone);
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory);
mViewModel.setSilentItemPosition(SILENT_RINGTONE_POSITION);
mViewModel.setDefaultItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
}
@Test
public void testGetStreamType_returnsTheCorrectStreamType() {
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM);
}
@Test
public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() {
when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor);
assertEquals(mViewModel.getRingtoneCursor(), mMockCursor);
}
@Test
public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() {
Uri expectedUri = DEFAULT_URI;
when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(expectedUri);
Uri actualUri = mViewModel.getRingtoneUri(DEFAULT_RINGTONE_POSITION);
assertEquals(actualUri, expectedUri);
}
@Test
public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() {
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
mViewModel.onPause(/* isChangingConfigurations= */ true);
verify(mMockRingtone, never()).stop();
}
@Test
public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
mViewModel.onPause(/* isChangingConfigurations= */ false);
verify(mMockDefaultRingtone).stop();
}
@Test
public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
Ringtone mockRingtone1 = createMockRingtone();
Ringtone mockRingtone2 = createMockRingtone();
when(mMockRingtoneManager.getRingtone(anyInt())).thenReturn(mockRingtone1, mockRingtone2);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mockRingtone1);
// Fake a scenario where the activity is destroyed and recreated due to a config change.
// This will result in a new view model getting created.
mViewModel.onStop(/* isChangingConfigurations= */ true);
verify(mockRingtone1, never()).stop();
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory);
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mockRingtone2);
verify(mockRingtone1).stop();
verify(mockRingtone2, never()).stop();
}
@Test
public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
mViewModel.onStop(/* isChangingConfigurations= */ true);
assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone);
}
@Test
public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
mViewModel.onStop(/* isChangingConfigurations= */ true);
assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockRingtone);
}
@Test
public void testOnStop_withChangingConfigurationTrueAndNoPlayingRingtone_saveNothing() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.onStop(/* isChangingConfigurations= */ true);
assertNull(RingtonePickerViewModel.sPlayingRingtone);
}
@Test
public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
mViewModel.onStop(/* isChangingConfigurations= */ false);
verify(mMockDefaultRingtone).stop();
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsUnknown_returnsNull() {
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(POS_UNKNOWN, DEFAULT_URI);
assertNull(uri);
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsDefaultPos_returnsDefaultUri() {
Uri expectedUri = DEFAULT_URI;
Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(DEFAULT_RINGTONE_POSITION,
expectedUri);
assertEquals(actualUri, expectedUri);
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsSilentPos_returnsNull() {
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(SILENT_RINGTONE_POSITION, DEFAULT_URI);
assertNull(uri);
}
@Test
public void testAddRingtone_returnsTheCorrectUri() throws IOException {
Uri expectedUri = DEFAULT_URI;
when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenReturn(
expectedUri);
Uri actualUri = mViewModel.addRingtone(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION);
verify(mMockRingtoneManager).addCustomExternalRingtone(DEFAULT_URI,
RingtoneManager.TYPE_NOTIFICATION);
assertEquals(actualUri, expectedUri);
}
@Test
public void testGetCurrentlySelectedRingtoneUri_checkedItemRingtonePos_returnsTheCorrectUri() {
Uri expectedUri = DEFAULT_URI;
when(mMockRingtoneManager.getRingtoneUri(RINGTONE_POSITION)).thenReturn(expectedUri);
Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(RINGTONE_POSITION, DEFAULT_URI);
verify(mMockRingtoneManager).getRingtoneUri(RINGTONE_POSITION);
assertEquals(actualUri, expectedUri);
}
@Test
public void testPlayRingtone_stopsPreviouslyRunningRingtone() {
// Start playing the first ringtone
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
// Start playing the second ringtone
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
verify(mMockDefaultRingtone).stop();
}
@Test
public void testPlayRingtone_samplePosEqualToSilentPos_onlyStopPlayingRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
mViewModel.setSampleItemPosition(SILENT_RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verify(mMockDefaultRingtone).stop();
// This will be invoked on the first ringtone we play, but not on the second one.
verify(mMockRingtoneFactory).create(any());
verify(mMockRingtoneManager, never()).getRingtone(anyInt());
verify(mMockRingtone, never()).play();
}
@Test
public void testPlayRingtone_samplePosEqualToDefaultPos_playDefaultRingtone() {
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
verify(mMockDefaultRingtone).setStreamType(AudioManager.STREAM_ALARM);
verify(mMockDefaultRingtone).play();
}
@Test
public void testPlayRingtone_samplePosNotEqualToDefaultPos_playRingtone() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
verify(mMockRingtone).setAudioAttributes(
audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE,
AudioAttributes.FLAG_AUDIBILITY_ENFORCED));
verify(mMockRingtone).play();
}
@Test
public void testPlayRingtone_withNoAttributeFlags_doNotUpdateRingtoneAttributesFlags() {
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
NO_ATTRIBUTES_FLAGS);
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
verify(mMockRingtone, never()).setAudioAttributes(any());
verify(mMockRingtone).play();
}
@Test
public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() {
int expectedPosition = 1;
when(mMockRingtoneManager.getRingtonePosition(any())).thenReturn(expectedPosition);
int actualPosition = mViewModel.getRingtonePosition(DEFAULT_URI);
assertEquals(actualPosition, expectedPosition);
}
@Test
public void testDefaultItemUri_withNotificationIntent_returnDefaultNotificationUri() {
Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(
@@ -69,7 +344,7 @@ public class RingtonePickerViewModelTest {
@Test
public void testTitle_withInvalidRingtoneType_returnDefaultRingtoneTitle() {
int title = RingtonePickerViewModel.getTitleByType(-1);
int title = RingtonePickerViewModel.getTitleByType(/*ringtoneType= */ -1);
assertEquals(com.android.internal.R.string.ringtone_picker_title, title);
}
@@ -126,4 +401,25 @@ public class RingtonePickerViewModelTest {
int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(-1);
assertEquals(R.string.ringtone_default, defaultRingtoneItemText);
}
private Ringtone createMockRingtone() {
Ringtone mockRingtone = mock(Ringtone.class);
when(mockRingtone.getAudioAttributes()).thenReturn(
audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, 0));
return mockRingtone;
}
private void verifyRingtonePlayCalledAndMockPlayingState(Ringtone ringtone) {
verify(ringtone).play();
when(ringtone.isPlaying()).thenReturn(true);
}
private static AudioAttributes audioAttributes(int audioUsage, int flags) {
return new AudioAttributes.Builder()
.setUsage(audioUsage)
.setFlags(flags)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
}
}