summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ahmad Khalil <khalilahmad@google.com> 2023-04-21 10:04:59 +0000
committer Ahmad Khalil <khalilahmad@google.com> 2023-04-28 15:52:13 +0000
commit73438d9115d1b4f61cea11f9a7243af3de67ff4c (patch)
treecb39c224011791cd941fe2324554e0601731f562
parent726d9a0d241c69647822ed3ed3daaf8847df1e79 (diff)
Move RingtoneManager out of the activity and into the ViewModel.
And add unit tests to cover the new logic in the ViewModel Bug: 275540178 Test: com.android.soundpicker.RingtonePickerViewModelTest Change-Id: Ib160c82e3661eb4b901c4eea3274037f9f93e25a
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java44
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java42
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java229
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java209
-rw-r--r--packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java298
5 files changed, 648 insertions, 174 deletions
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java
new file mode 100644
index 000000000000..6ee7c357461d
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java
@@ -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);
+ }
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java
new file mode 100644
index 000000000000..a7da506581bb
--- /dev/null
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java
@@ -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);
+ }
+}
+
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
index e090c165fc7b..359bd0db4292 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@@ -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);
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
index 9bc33abf7c6d..164964277a4a 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java
@@ -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;
+ }
}
diff --git a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
index a5340022ab84..333a4e09627c 100644
--- a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
+++ b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
@@ -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();
+ }
}