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
diff --git a/packages/SoundPicker/Android.bp b/packages/SoundPicker/Android.bp
index a33b2be..c8999fb 100644
--- a/packages/SoundPicker/Android.bp
+++ b/packages/SoundPicker/Android.bp
@@ -17,6 +17,8 @@
],
static_libs: [
"androidx.appcompat_appcompat",
+ "hilt_android",
+ "guava",
],
}
diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml
index cdfe2421..1f99e75 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 0000000..4eecf89
--- /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 0000000..afdbf05
--- /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 6ee7c35..0a8a73b 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.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 a7da506..f08eb24 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java
@@ -19,15 +19,22 @@
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 359bd0d..f591aa5 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.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.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 @@
*
* @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 @@
private boolean mShowOkCancelButtons;
+ private AlertDialog mAlertDialog;
+
+ private int mCheckedItem = POS_UNKNOWN;
+
private final DialogInterface.OnClickListener mRingtoneClickListener =
new DialogInterface.OnClickListener() {
@@ -137,12 +148,28 @@
}
};
+ 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 @@
// 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 @@
// 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 @@
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());
- }
- }
-
- // 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);
+ mRingtonePickerViewModel.addRingtoneAsync(data.getData(),
+ mType,
+ mAddCustomRingtoneCallback,
+ // Causes the callback to be executed on the main thread.
+ ContextCompat.getMainExecutor(this.getApplicationContext()));
}
}
@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;
- }
+ public void onDismiss(DialogInterface dialog) {
+ if (!isChangingConfigurations()) {
+ finish();
}
}
- */
@Override
public void onDestroy() {
+ mRingtonePickerViewModel.cancelPendingAsyncTasks();
+ if (mAlertDialog != null && mAlertDialog.isShowing()) {
+ mAlertDialog.dismiss();
+ }
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
@@ -296,7 +282,7 @@
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 @@
checkedPosition = mRingtonePickerViewModel.getSilentItemPosition();
}
setCheckedItem(checkedPosition);
- setupAlert();
}
/**
@@ -374,7 +359,7 @@
* @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 @@
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 @@
}
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 @@
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 0000000..48fd4fe
--- /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 1649642..f045dc2 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.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 @@
@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 @@
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 @@
*/
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 @@
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 @@
}
}
- /**
- * 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 @@
}
}
+ /**
+ * 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 d6aea48..dcd7b98 100644
--- a/packages/SoundPicker/tests/Android.bp
+++ b/packages/SoundPicker/tests/Android.bp
@@ -28,6 +28,7 @@
"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 333a4e0..9ef3aa3 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.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 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.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 @@
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 @@
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 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 @@
@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 @@
@Test
public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
+ mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
Ringtone mockRingtone1 = createMockRingtone();
Ringtone mockRingtone2 = createMockRingtone();
@@ -136,7 +184,9 @@
// 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 @@
@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 @@
@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 @@
@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 @@
}
@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 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 @@
@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 @@
@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 @@
@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 @@
@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 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);