summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SoundPicker/Android.bp2
-rw-r--r--packages/SoundPicker/AndroidManifest.xml4
-rw-r--r--packages/SoundPicker/res/layout/activity_ringtone_picker.xml20
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/ListeningExecutorServiceFactory.java44
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java11
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtoneManagerFactory.java11
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java165
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtonePickerApplication.java28
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtonePickerViewModel.java95
-rw-r--r--packages/SoundPicker/tests/Android.bp1
-rw-r--r--packages/SoundPicker/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java149
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);