diff options
| author | 2023-04-21 10:04:59 +0000 | |
|---|---|---|
| committer | 2023-04-28 15:52:13 +0000 | |
| commit | 73438d9115d1b4f61cea11f9a7243af3de67ff4c (patch) | |
| tree | cb39c224011791cd941fe2324554e0601731f562 | |
| parent | 726d9a0d241c69647822ed3ed3daaf8847df1e79 (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
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(); + } } |