diff options
author | 2023-05-03 17:15:18 +0000 | |
---|---|---|
committer | 2023-05-18 14:03:01 +0000 | |
commit | fe6e60034c329d24e1789f1ea1bc75c3ed93dd74 (patch) | |
tree | e4aa70801fdffe4afea0176752ee79e8fb1f6c85 | |
parent | 0c6ab23963c5e3be6763b8b604a3d19754a13ebd (diff) |
Change RingtonePickerActivity to extend AppCompatActivity.
The application will continue to display an alert dialog as its main landing screen. We are also adding Hilt to the project to help us with the dependency injection.
Bug: 275540178
Test: com.android.soundpicker.RingtonePickerViewModelTest
Change-Id: Ie6a8fc5c4741d894166ed0c54b39e19d8a6a9161
11 files changed, 402 insertions, 128 deletions
diff --git a/packages/SoundPicker/Android.bp b/packages/SoundPicker/Android.bp index a33b2bee78d9..c8999fbcd271 100644 --- a/packages/SoundPicker/Android.bp +++ b/packages/SoundPicker/Android.bp @@ -17,6 +17,8 @@ android_library { ], static_libs: [ "androidx.appcompat_appcompat", + "hilt_android", + "guava", ], } diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml index cdfe2421fdb7..1f99e75ebc88 100644 --- a/packages/SoundPicker/AndroidManifest.xml +++ b/packages/SoundPicker/AndroidManifest.xml @@ -12,8 +12,10 @@ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <application + android:name=".RingtonePickerApplication" android:allowBackup="false" android:label="@string/app_label" + android:theme="@style/Theme.AppCompat" android:supportsRtl="true"> <receiver android:name="RingtoneReceiver" android:exported="true"> @@ -25,7 +27,7 @@ <service android:name="RingtoneOverlayService" /> <activity android:name="RingtonePickerActivity" - android:theme="@style/PickerDialogTheme" + android:theme="@style/Theme.AppCompat.Dialog" android:enabled="@*android:bool/config_defaultRingtonePickerEnabled" android:excludeFromRecents="true" android:exported="true"> diff --git a/packages/SoundPicker/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker/res/layout/activity_ringtone_picker.xml new file mode 100644 index 000000000000..4eecf89bb481 --- /dev/null +++ b/packages/SoundPicker/res/layout/activity_ringtone_picker.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" />
\ No newline at end of file diff --git a/packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.java b/packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.java new file mode 100644 index 000000000000..afdbf053ac22 --- /dev/null +++ b/packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.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 com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.Executors; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * A factory class used to create {@link ListeningExecutorService}. + */ +@Singleton +public class ListeningExecutorServiceFactory { + + @Inject + ListeningExecutorServiceFactory() { + } + + /** + * Returns a single thread {@link ListeningExecutorService}. + * + */ + public ListeningExecutorService createSingleThreadExecutor() { + return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + } +} diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java index 6ee7c357461d..0a8a73b27f26 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java @@ -21,15 +21,22 @@ import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import dagger.hilt.android.qualifiers.ApplicationContext; + +import javax.inject.Inject; +import javax.inject.Singleton; + /** * A factory class used to create {@link Ringtone}. */ +@Singleton public class RingtoneFactory { private final Context mApplicationContext; - RingtoneFactory(Context context) { - mApplicationContext = context.getApplicationContext(); + @Inject + RingtoneFactory(@ApplicationContext Context applicationContext) { + mApplicationContext = applicationContext; } /** diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java index a7da506581bb..f08eb24ec20d 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java @@ -19,15 +19,22 @@ package com.android.soundpicker; import android.content.Context; import android.media.RingtoneManager; +import dagger.hilt.android.qualifiers.ApplicationContext; + +import javax.inject.Inject; +import javax.inject.Singleton; + /** * A factory class used to create {@link RingtoneManager}. */ +@Singleton public class RingtoneManagerFactory { private final Context mApplicationContext; - RingtoneManagerFactory(Context context) { - mApplicationContext = context.getApplicationContext(); + @Inject + RingtoneManagerFactory(@ApplicationContext Context applicationContext) { + mApplicationContext = applicationContext; } /** diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java index 359bd0db4292..f591aa54a50e 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java @@ -26,7 +26,6 @@ import android.database.Cursor; import android.database.CursorWrapper; import android.media.RingtoneManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -45,8 +44,15 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import com.android.internal.app.AlertActivity; -import com.android.internal.app.AlertController; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; + +import com.google.common.util.concurrent.FutureCallback; + +import dagger.hilt.android.AndroidEntryPoint; import java.util.regex.Pattern; @@ -56,9 +62,10 @@ import java.util.regex.Pattern; * * @see RingtoneManager#ACTION_RINGTONE_PICKER */ -public final class RingtonePickerActivity extends AlertActivity implements +@AndroidEntryPoint(AppCompatActivity.class) +public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity implements AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener, - AlertController.AlertParams.OnPrepareListViewListener { + DialogInterface.OnDismissListener { private static final int POS_UNKNOWN = -1; @@ -106,6 +113,10 @@ public final class RingtonePickerActivity extends AlertActivity implements private boolean mShowOkCancelButtons; + private AlertDialog mAlertDialog; + + private int mCheckedItem = POS_UNKNOWN; + private final DialogInterface.OnClickListener mRingtoneClickListener = new DialogInterface.OnClickListener() { @@ -137,12 +148,28 @@ public final class RingtonePickerActivity extends AlertActivity implements } }; + private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() { + @Override + public void onSuccess(Uri ringtoneUri) { + requeryForAdapter(); + } + + @Override + public void onFailure(Throwable throwable) { + Log.e(TAG, "Failed to add custom ringtone.", throwable); + // Ringtone was not added, display error Toast + Toast.makeText(RingtonePickerActivity.this.getApplicationContext(), + R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show(); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mRingtonePickerViewModel = new RingtonePickerViewModel( - new RingtoneManagerFactory(this), new RingtoneFactory(this)); + setContentView(R.layout.activity_ringtone_picker); + + mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class); + mHandler = new Handler(); Intent intent = getIntent(); @@ -151,7 +178,7 @@ public final class RingtonePickerActivity extends AlertActivity implements // Get the types of ringtones to show mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN); - mRingtonePickerViewModel.setRingtoneType(mType); + mRingtonePickerViewModel.initRingtoneManager(mType); setupCursor(); /* @@ -183,36 +210,34 @@ public final class RingtonePickerActivity extends AlertActivity implements // Create the list of ringtones and hold on to it so we can update later. mAdapter = new BadgedRingtoneAdapter(this, mCursor, /* isManagedProfile = */ UserManager.get(this).isManagedProfile(mPickerUserId)); - if (savedInstanceState != null) { - setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN)); - } - final AlertController.AlertParams p = mAlertParams; - p.mAdapter = mAdapter; - p.mOnClickListener = mRingtoneClickListener; - p.mLabelColumn = COLUMN_LABEL; - p.mIsSingleChoice = true; - p.mOnItemSelectedListener = this; + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this, + android.R.style.ThemeOverlay_Material_Dialog); + alertDialogBuilder + .setSingleChoiceItems(mAdapter, getCheckedItem(), mRingtoneClickListener) + .setOnItemSelectedListener(this) + .setOnDismissListener(this); if (mShowOkCancelButtons) { - p.mPositiveButtonText = getString(com.android.internal.R.string.ok); - p.mPositiveButtonListener = this; - p.mNegativeButtonText = getString(com.android.internal.R.string.cancel); - p.mPositiveButtonListener = this; - } - p.mOnPrepareListViewListener = this; - p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); - if (p.mTitle == null) { - p.mTitle = getString(RingtonePickerViewModel.getTitleByType(mType)); + alertDialogBuilder + .setPositiveButton(getString(com.android.internal.R.string.ok), this) + .setNegativeButton(getString(com.android.internal.R.string.cancel), this); } - setupAlert(); + String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); + alertDialogBuilder.setTitle( + title != null ? title : getString(RingtonePickerViewModel.getTitleByType(mType))); - ListView listView = mAlert.getListView(); + mAlertDialog = alertDialogBuilder.show(); + ListView listView = mAlertDialog.getListView(); if (listView != null) { // List view needs to gain focus in order for RSB to work. if (!listView.requestFocus()) { Log.e(TAG, "Unable to gain focus! RSB may not work properly."); } + prepareListView(listView); + } + if (savedInstanceState != null) { + setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN)); } } @Override @@ -226,66 +251,27 @@ public final class RingtonePickerActivity extends AlertActivity implements super.onActivityResult(requestCode, resultCode, data); if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) { - // Add the custom ringtone in a separate thread - final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() { - @Override - protected Uri doInBackground(Uri... params) { - return mRingtonePickerViewModel.addRingtone(params[0], mType); - } - - @Override - protected void onPostExecute(Uri ringtoneUri) { - if (ringtoneUri != null) { - requeryForAdapter(); - } else { - // Ringtone was not added, display error Toast - Toast.makeText(RingtonePickerActivity.this, R.string.unable_to_add_ringtone, - Toast.LENGTH_SHORT).show(); - } - } - }; - installTask.execute(data.getData()); + mRingtonePickerViewModel.addRingtoneAsync(data.getData(), + mType, + mAddCustomRingtoneCallback, + // Causes the callback to be executed on the main thread. + ContextCompat.getMainExecutor(this.getApplicationContext())); } } - // Disabled because context menus aren't Material Design :( - /* @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - int position = ((AdapterContextMenuInfo) menuInfo).position; - - Ringtone ringtone = getRingtone(getRingtoneManagerPosition(position)); - if (ringtone != null && mRingtoneManager.isCustomRingtone(ringtone.getUri())) { - // It's a custom ringtone so we display the context menu - menu.setHeaderTitle(ringtone.getTitle(this)); - menu.add(Menu.NONE, Menu.FIRST, Menu.NONE, R.string.delete_ringtone_text); + public void onDismiss(DialogInterface dialog) { + if (!isChangingConfigurations()) { + finish(); } } @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case Menu.FIRST: { - int deletedRingtonePos = ((AdapterContextMenuInfo) item.getMenuInfo()).position; - Uri deletedRingtoneUri = getRingtone( - getRingtoneManagerPosition(deletedRingtonePos)).getUri(); - if(mRingtoneManager.deleteExternalRingtone(deletedRingtoneUri)) { - requeryForAdapter(); - } else { - Toast.makeText(this, R.string.unable_to_delete_ringtone, Toast.LENGTH_SHORT) - .show(); - } - return true; - } - default: { - return false; - } - } - } - */ - - @Override public void onDestroy() { + mRingtonePickerViewModel.cancelPendingAsyncTasks(); + if (mAlertDialog != null && mAlertDialog.isShowing()) { + mAlertDialog.dismiss(); + } if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } @@ -296,7 +282,7 @@ public final class RingtonePickerActivity extends AlertActivity implements super.onDestroy(); } - public void onPrepareListView(ListView listView) { + private void prepareListView(@NonNull ListView listView) { // Reset the static item count, as this method can be called multiple times mRingtonePickerViewModel.resetFixedItemCount(); @@ -363,7 +349,6 @@ public final class RingtonePickerActivity extends AlertActivity implements checkedPosition = mRingtonePickerViewModel.getSilentItemPosition(); } setCheckedItem(checkedPosition); - setupAlert(); } /** @@ -374,7 +359,7 @@ public final class RingtonePickerActivity extends AlertActivity implements * @param textResId The resource ID of the text for the item. * @return The position of the inserted item. */ - private int addStaticItem(ListView listView, int textResId) { + private int addStaticItem(@NonNull ListView listView, int textResId) { TextView textView = (TextView) getLayoutInflater().inflate( com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false); textView.setText(textResId); @@ -383,20 +368,20 @@ public final class RingtonePickerActivity extends AlertActivity implements return listView.getHeaderViewsCount() - 1; } - private int addDefaultRingtoneItem(ListView listView) { + private int addDefaultRingtoneItem(@NonNull ListView listView) { int defaultRingtoneItemPos = addStaticItem(listView, RingtonePickerViewModel.getDefaultRingtoneItemTextByType(mType)); mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos); return defaultRingtoneItemPos; } - private int addSilentItem(ListView listView) { + private int addSilentItem(@NonNull ListView listView) { int silentItemPos = addStaticItem(listView, com.android.internal.R.string.ringtone_silent); mRingtonePickerViewModel.setSilentItemPosition(silentItemPos); return silentItemPos; } - private void addNewSoundItem(ListView listView) { + private void addNewSoundItem(@NonNull ListView listView) { View view = getLayoutInflater().inflate(R.layout.add_new_sound_item, listView, false /* attachToRoot */); TextView text = (TextView)view.findViewById(R.id.add_new_sound_text); @@ -412,11 +397,16 @@ public final class RingtonePickerActivity extends AlertActivity implements } private int getCheckedItem() { - return mAlertParams.mCheckedItem; + return mCheckedItem; } private void setCheckedItem(int pos) { - mAlertParams.mCheckedItem = pos; + mCheckedItem = pos; + ListView listView = mAlertDialog.getListView(); + if (listView != null) { + listView.setItemChecked(pos, true); + listView.smoothScrollToPosition(pos); + } mCheckedItemId = mAdapter.getItemId( mRingtonePickerViewModel.itemPositionToRingtonePosition(pos)); } @@ -491,7 +481,6 @@ public final class RingtonePickerActivity extends AlertActivity implements new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri)); } - private int getListPosition(int ringtoneManagerPos) { // If the manager position is -1 (for not found), return that diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java new file mode 100644 index 000000000000..48fd4fe2f15e --- /dev/null +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.soundpicker; + +import android.app.Application; + +import dagger.hilt.android.HiltAndroidApp; + +/** + * The main application class for the project. + */ +@HiltAndroidApp(Application.class) +public class RingtonePickerApplication extends Hilt_RingtonePickerApplication { +} diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java index 164964277a4a..f045dc2f864c 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java @@ -16,6 +16,8 @@ package com.android.soundpicker; +import static java.util.Objects.requireNonNull; + import android.annotation.Nullable; import android.annotation.StringRes; import android.database.Cursor; @@ -24,16 +26,28 @@ import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.provider.Settings; -import android.util.Log; + +import androidx.lifecycle.ViewModel; import com.android.internal.annotations.VisibleForTesting; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; + +import dagger.hilt.android.lifecycle.HiltViewModel; + import java.io.IOException; +import java.util.concurrent.Executor; + +import javax.inject.Inject; /** * View model for {@link RingtonePickerActivity}. */ -public final class RingtonePickerViewModel { +@HiltViewModel +public final class RingtonePickerViewModel extends ViewModel { static final int RINGTONE_TYPE_UNKNOWN = -1; /** @@ -43,10 +57,14 @@ public final class RingtonePickerViewModel { @VisibleForTesting static Ringtone sPlayingRingtone; private static final String TAG = "RingtonePickerViewModel"; + private static final String RINGTONE_MANAGER_NULL_MESSAGE = + "RingtoneManager must not be null. Did you forget to call " + + "RingtonePickerViewModel#initRingtoneManager?"; private static final int ITEM_POSITION_UNKNOWN = -1; private final RingtoneManagerFactory mRingtoneManagerFactory; private final RingtoneFactory mRingtoneFactory; + private final ListeningExecutorService mListeningExecutorService; /** The position in the list of the 'Silent' item. */ private int mSilentItemPosition = ITEM_POSITION_UNKNOWN; @@ -56,7 +74,7 @@ public final class RingtonePickerViewModel { private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN; /** The number of static items in the list. */ private int mFixedItemCount; - + private ListenableFuture<Uri> mAddCustomRingtoneFuture; private RingtoneManager mRingtoneManager; /** @@ -64,11 +82,13 @@ public final class RingtonePickerViewModel { */ private Ringtone mCurrentRingtone; + @Inject RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory, - RingtoneFactory ringtoneFactory) { + RingtoneFactory ringtoneFactory, + ListeningExecutorServiceFactory listeningExecutorServiceFactory) { mRingtoneManagerFactory = ringtoneManagerFactory; mRingtoneFactory = ringtoneFactory; - initRingtoneManager(RINGTONE_TYPE_UNKNOWN); + mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor(); } @StringRes @@ -120,28 +140,53 @@ public final class RingtonePickerViewModel { void initRingtoneManager(int type) { mRingtoneManager = mRingtoneManagerFactory.create(); - setRingtoneType(type); - } - - void setRingtoneType(int type) { if (type != RINGTONE_TYPE_UNKNOWN) { mRingtoneManager.setType(type); } } + /** + * Adds an audio file to the list of ringtones asynchronously. + * Any previous async tasks are canceled before start the new one. + * + * @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. + * @param callback The callback to invoke when the task is completed. + * @param executor The executor to run the callback on when the task completes. + */ + void addRingtoneAsync(Uri uri, int type, FutureCallback<Uri> callback, Executor executor) { + // Cancel any currently running add ringtone tasks before starting a new one + cancelPendingAsyncTasks(); + mAddCustomRingtoneFuture = mListeningExecutorService.submit(() -> addRingtone(uri, type)); + Futures.addCallback(mAddCustomRingtoneFuture, callback, executor); + } + + /** + * Cancels all pending async tasks. + */ + void cancelPendingAsyncTasks() { + if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) { + mAddCustomRingtoneFuture.cancel(/*mayInterruptIfRunning=*/true); + } + } + int getRingtoneStreamType() { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); return mRingtoneManager.inferStreamType(); } Cursor getRingtoneCursor() { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); return mRingtoneManager.getCursor(); } Uri getRingtoneUri(int ringtonePosition) { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); return mRingtoneManager.getRingtoneUri(ringtonePosition); } int getRingtonePosition(Uri uri) { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); return mRingtoneManager.getRingtonePosition(uri); } @@ -218,24 +263,8 @@ public final class RingtonePickerViewModel { } } - /** - * 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) { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); stopAnyPlayingRingtone(); if (mSampleItemPosition == mSilentItemPosition) { return; @@ -263,6 +292,20 @@ public final class RingtonePickerViewModel { } } + /** + * 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 + private Uri addRingtone(Uri uri, int type) throws IOException { + requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE); + return mRingtoneManager.addCustomExternalRingtone(uri, type); + } + private void saveAnyPlayingRingtone() { if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { sPlayingRingtone = mCurrentRingtone; diff --git a/packages/SoundPicker/tests/Android.bp b/packages/SoundPicker/tests/Android.bp index d6aea48ece27..dcd7b98c0cb0 100644 --- a/packages/SoundPicker/tests/Android.bp +++ b/packages/SoundPicker/tests/Android.bp @@ -28,6 +28,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "mockito-target-minus-junit4", + "guava-android-testlib", "SoundPickerLib", ], srcs: [ diff --git a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java index 333a4e09627c..9ef3aa3b245f 100644 --- a/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java +++ b/packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java @@ -24,6 +24,7 @@ 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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.database.Cursor; @@ -36,6 +37,11 @@ import android.provider.Settings; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.testing.TestingExecutors; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,11 +49,13 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.IOException; +import java.util.concurrent.ExecutorService; @RunWith(AndroidJUnit4.class) public class RingtonePickerViewModelTest { private static final Uri DEFAULT_URI = Uri.parse("https://www.google.com/login.html"); + private static final int RINGTONE_TYPE_UNKNOWN = -1; private static final int POS_UNKNOWN = -1; private static final int NO_ATTRIBUTES_FLAGS = 0; private static final int SILENT_RINGTONE_POSITION = 0; @@ -62,7 +70,11 @@ public class RingtonePickerViewModelTest { private RingtoneManager mMockRingtoneManager; @Mock private Cursor mMockCursor; + @Mock + private ListeningExecutorServiceFactory mMockListeningExecutorServiceFactory; + private ExecutorService mMainThreadExecutor; + private ListeningExecutorService mBackgroundThreadExecutor; private Ringtone mMockDefaultRingtone; private Ringtone mMockRingtone; private RingtonePickerViewModel mViewModel; @@ -76,23 +88,55 @@ public class RingtonePickerViewModelTest { mMockRingtone = createMockRingtone(); when(mMockRingtoneFactory.create(DEFAULT_URI)).thenReturn(mMockDefaultRingtone); when(mMockRingtoneManager.getRingtone(anyInt())).thenReturn(mMockRingtone); + mMainThreadExecutor = TestingExecutors.sameThreadScheduledExecutor(); + mBackgroundThreadExecutor = TestingExecutors.sameThreadScheduledExecutor(); + when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( + mBackgroundThreadExecutor); - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory); - + mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, + mMockListeningExecutorServiceFactory); mViewModel.setSilentItemPosition(SILENT_RINGTONE_POSITION); mViewModel.setDefaultItemPosition(DEFAULT_RINGTONE_POSITION); mViewModel.setSampleItemPosition(RINGTONE_POSITION); } + @After + public void teardown() { + if (mMainThreadExecutor != null && !mMainThreadExecutor.isShutdown()) { + mMainThreadExecutor.shutdown(); + } + if (mBackgroundThreadExecutor != null && !mBackgroundThreadExecutor.isShutdown()) { + mBackgroundThreadExecutor.shutdown(); + } + } + + @Test + public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() { + mViewModel.initRingtoneManager(RINGTONE_TYPE_UNKNOWN); + + verify(mMockRingtoneManagerFactory).create(); + verify(mMockRingtoneManager, never()).setType(anyInt()); + } + + @Test + public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_NOTIFICATION); + + verify(mMockRingtoneManagerFactory).create(); + verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION); + } + @Test public void testGetStreamType_returnsTheCorrectStreamType() { when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM); } @Test public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() { when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); assertEquals(mViewModel.getRingtoneCursor(), mMockCursor); } @@ -100,12 +144,14 @@ public class RingtonePickerViewModelTest { public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() { Uri expectedUri = DEFAULT_URI; when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(expectedUri); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); Uri actualUri = mViewModel.getRingtoneUri(DEFAULT_RINGTONE_POSITION); assertEquals(actualUri, expectedUri); } @Test public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone); @@ -115,6 +161,7 @@ public class RingtonePickerViewModelTest { @Test public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -125,6 +172,7 @@ public class RingtonePickerViewModelTest { @Test public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(RINGTONE_POSITION); Ringtone mockRingtone1 = createMockRingtone(); Ringtone mockRingtone2 = createMockRingtone(); @@ -136,7 +184,9 @@ public class RingtonePickerViewModelTest { // This will result in a new view model getting created. mViewModel.onStop(/* isChangingConfigurations= */ true); verify(mockRingtone1, never()).stop(); - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory); + mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, + mMockListeningExecutorServiceFactory); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(RINGTONE_POSITION); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -147,6 +197,7 @@ public class RingtonePickerViewModelTest { @Test public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -157,6 +208,7 @@ public class RingtonePickerViewModelTest { @Test public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(RINGTONE_POSITION); mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -174,6 +226,7 @@ public class RingtonePickerViewModelTest { @Test public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() { + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -203,20 +256,93 @@ public class RingtonePickerViewModelTest { } @Test - public void testAddRingtone_returnsTheCorrectUri() throws IOException { + public void testCancelPendingAsyncTasks_correctlyCancelsPendingTasks() + throws IOException { + FutureCallback<Uri> mockCallback = mock(FutureCallback.class); + + when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( + TestingExecutors.noOpScheduledExecutor()); + when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI, + RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI); + mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, + mMockListeningExecutorServiceFactory); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); + mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback, + mMainThreadExecutor); + verify(mockCallback, never()).onFailure(any()); + // Calling cancelPendingAsyncTasks should cancel the pending task. Cancelling an async + // task invokes the onFailure method in the callable. + mViewModel.cancelPendingAsyncTasks(); + verify(mockCallback).onFailure(any()); + verify(mockCallback, never()).onSuccess(any()); + + } + + @Test + public void testAddRingtoneAsync_cancelPreviousTaskBeforeStartingNewOne() + throws IOException { + FutureCallback<Uri> mockCallback1 = mock(FutureCallback.class); + FutureCallback<Uri> mockCallback2 = mock(FutureCallback.class); + + when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( + TestingExecutors.noOpScheduledExecutor()); + when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI, + RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI); + mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, + mMockListeningExecutorServiceFactory); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); + mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback1, + mMainThreadExecutor); + verify(mockCallback1, never()).onFailure(any()); + // We call addRingtoneAsync again to cancel the previous task and start a new one. + // Cancelling an async task invokes the onFailure method in the callable. + mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback2, + mMainThreadExecutor); + verify(mockCallback1).onFailure(any()); + verify(mockCallback1, never()).onSuccess(any()); + verifyNoMoreInteractions(mockCallback2); + } + + @Test + public void testAddRingtoneAsync_whenAddRingtoneIsSuccessful_successCallbackIsInvoked() + throws IOException { Uri expectedUri = DEFAULT_URI; - when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenReturn( - expectedUri); - Uri actualUri = mViewModel.addRingtone(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION); + FutureCallback<Uri> mockCallback = mock(FutureCallback.class); + + when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI, + RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri); + + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); + mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback, + mMainThreadExecutor); + verify(mMockRingtoneManager).addCustomExternalRingtone(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION); - assertEquals(actualUri, expectedUri); + verify(mockCallback).onSuccess(expectedUri); + verify(mockCallback, never()).onFailure(any()); + } + + @Test + public void testAddRingtoneAsync_whenAddRingtoneFailed_failureCallbackIsInvoked() + throws IOException { + FutureCallback<Uri> mockCallback = mock(FutureCallback.class); + + when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow( + IOException.class); + + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); + mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback, + mMainThreadExecutor); + + verify(mockCallback).onFailure(any(IOException.class)); + verify(mockCallback, never()).onSuccess(any()); } @Test public void testGetCurrentlySelectedRingtoneUri_checkedItemRingtonePos_returnsTheCorrectUri() { Uri expectedUri = DEFAULT_URI; when(mMockRingtoneManager.getRingtoneUri(RINGTONE_POSITION)).thenReturn(expectedUri); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(RINGTONE_POSITION, DEFAULT_URI); verify(mMockRingtoneManager).getRingtoneUri(RINGTONE_POSITION); @@ -227,6 +353,7 @@ public class RingtonePickerViewModelTest { public void testPlayRingtone_stopsPreviouslyRunningRingtone() { // Start playing the first ringtone mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); @@ -242,6 +369,7 @@ public class RingtonePickerViewModelTest { @Test public void testPlayRingtone_samplePosEqualToSilentPos_onlyStopPlayingRingtone() { mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); @@ -260,6 +388,7 @@ public class RingtonePickerViewModelTest { @Test public void testPlayRingtone_samplePosEqualToDefaultPos_playDefaultRingtone() { mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM); @@ -274,6 +403,7 @@ public class RingtonePickerViewModelTest { @Test public void testPlayRingtone_samplePosNotEqualToDefaultPos_playRingtone() { mViewModel.setSampleItemPosition(RINGTONE_POSITION); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI, AudioAttributes.FLAG_AUDIBILITY_ENFORCED); @@ -287,6 +417,7 @@ public class RingtonePickerViewModelTest { @Test public void testPlayRingtone_withNoAttributeFlags_doNotUpdateRingtoneAttributesFlags() { mViewModel.setSampleItemPosition(RINGTONE_POSITION); + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI, NO_ATTRIBUTES_FLAGS); @@ -299,7 +430,7 @@ public class RingtonePickerViewModelTest { public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() { int expectedPosition = 1; when(mMockRingtoneManager.getRingtonePosition(any())).thenReturn(expectedPosition); - + mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE); int actualPosition = mViewModel.getRingtonePosition(DEFAULT_URI); assertEquals(actualPosition, expectedPosition); |