Add two tabs to the ringtone picker pop-up view.
We're converting the AlertDialog to a DialogFragment with a ViewPage and two tabs (Sound and Vibration). The Vibration tab will be empty for now, while the Sound tab will include a recyclerview with all available sounds. Fix: 275541592 Test: com.android.soundpicker.RingtonePickerViewModelTest Change-Id: Idda019d2ef460785d2b2fd9df35858d4650829fc
This commit is contained in:
@@ -19,6 +19,10 @@ android_library {
|
||||
"androidx.appcompat_appcompat",
|
||||
"hilt_android",
|
||||
"guava",
|
||||
"androidx.recyclerview_recyclerview",
|
||||
"androidx-constraintlayout_constraintlayout",
|
||||
"androidx.viewpager2_viewpager2",
|
||||
"com.google.android.material_material",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RINGTONE_PICKER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.RINGTONE_PICKER_SOUND" />
|
||||
<category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" />
|
||||
<category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?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");
|
||||
@@ -15,6 +16,6 @@
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
android:layout_height="match_parent"/>
|
||||
@@ -16,12 +16,14 @@
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
|
||||
<ImageView
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentRight="true"
|
||||
@@ -29,9 +31,9 @@
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:src="@drawable/ic_add" />
|
||||
android:src="@drawable/ic_add"/>
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<TextView
|
||||
android:id="@+id/add_new_sound_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -43,5 +45,5 @@
|
||||
android:gravity="center_vertical"
|
||||
android:paddingEnd="?android:attr/dialogPreferredPadding"
|
||||
android:drawablePadding="20dp"
|
||||
android:ellipsize="marquee" />
|
||||
android:ellipsize="marquee"/>
|
||||
</LinearLayout>
|
||||
23
packages/SoundPicker/res/layout/fragment_sound_picker.xml
Normal file
23
packages/SoundPicker/res/layout/fragment_sound_picker.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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.
|
||||
-->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
31
packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml
Normal file
31
packages/SoundPicker/res/layout/fragment_tabbed_dialog.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/masterViewPager"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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.
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/empty_list"
|
||||
android:textColor="?android:attr/colorAccent"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:gravity="center"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -14,12 +14,14 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<com.android.soundpicker.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.android.soundpicker.CheckedListItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
>
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/checked_text_view"
|
||||
@@ -35,7 +37,7 @@
|
||||
android:drawablePadding="20dp"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_toLeftOf="@+id/work_icon"
|
||||
android:maxLines="3" />
|
||||
android:maxLines="3"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@id/work_icon"
|
||||
@@ -44,5 +46,5 @@
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_marginRight="20dp" />
|
||||
android:layout_marginRight="20dp"/>
|
||||
</com.android.soundpicker.CheckedListItem>
|
||||
|
||||
@@ -40,4 +40,8 @@
|
||||
|
||||
<!-- Text for the name of the app. [CHAR LIMIT=12] -->
|
||||
<string name="app_label">Sounds</string>
|
||||
|
||||
<string name="empty_list">The list is empty</string>
|
||||
<string name="sound_page_title">Sound</string>
|
||||
<string name="vibration_page_title">Vibration</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.soundpicker;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cursor wrapper class mainly used to guarantee getting a ringtone title
|
||||
*/
|
||||
final class LocalizedCursor extends CursorWrapper {
|
||||
|
||||
private static final String TAG = "LocalizedCursor";
|
||||
private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
|
||||
|
||||
private final int mTitleIndex;
|
||||
private final Resources mResources;
|
||||
private final Pattern mSanitizePattern;
|
||||
private final String mNamePrefix;
|
||||
|
||||
LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
|
||||
super(cursor);
|
||||
mTitleIndex = mCursor.getColumnIndex(columnLabel);
|
||||
mResources = resources;
|
||||
mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
|
||||
if (mTitleIndex == -1) {
|
||||
Log.e(TAG, "No index for column " + columnLabel);
|
||||
mNamePrefix = null;
|
||||
} else {
|
||||
mNamePrefix = buildNamePrefix(mResources);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the prefix for the name of the resource to look up.
|
||||
* The format is: "ResourcePackageName::ResourceTypeName/" (the type name is expected to be
|
||||
* "string" but let's not hardcode it).
|
||||
* Here we use an existing resource "notification_sound_default" which is always expected to be
|
||||
* found.
|
||||
*
|
||||
* @param resources Application's resources
|
||||
* @return the built name prefix, or null if failed to build.
|
||||
*/
|
||||
@Nullable
|
||||
private static String buildNamePrefix(Resources resources) {
|
||||
try {
|
||||
return String.format("%s:%s/%s",
|
||||
resources.getResourcePackageName(R.string.notification_sound_default),
|
||||
resources.getResourceTypeName(R.string.notification_sound_default),
|
||||
SOUND_NAME_RES_PREFIX);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
Log.e(TAG, "Failed to build the prefix for the name of the resource.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process resource name to generate a valid resource name.
|
||||
*
|
||||
* @return a non-null String
|
||||
*/
|
||||
private String sanitize(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int columnIndex) {
|
||||
final String defaultName = mCursor.getString(columnIndex);
|
||||
if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
|
||||
return defaultName;
|
||||
}
|
||||
TypedValue value = new TypedValue();
|
||||
try {
|
||||
// the name currently in the database is used to derive a name to match
|
||||
// against resource names in this package
|
||||
mResources.getValue(mNamePrefix + sanitize(defaultName), value,
|
||||
/* resolveRefs= */ false);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
Log.d(TAG, "Failed to get localized string. Using default string instead.", e);
|
||||
return defaultName;
|
||||
}
|
||||
if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
|
||||
Log.d(TAG, String.format("Replacing name %s with %s",
|
||||
defaultName, value.string.toString()));
|
||||
return value.string.toString();
|
||||
} else {
|
||||
Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
|
||||
return defaultName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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 static com.android.internal.widget.RecyclerView.NO_ID;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.RingtoneManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* The adapter presents a list of ringtones which may include fixed item in the list and an action
|
||||
* button at the end.
|
||||
*
|
||||
* The adapter handles three different types of items:
|
||||
* <ul>
|
||||
* <li>FIXED: Fixed items are items added to the top of the list. These items can not be modified
|
||||
* and their position will never change.
|
||||
* <li>DYNAMIC: Dynamic items are items from the ringtone manager. These items can be modified
|
||||
* and their position can change.
|
||||
* <li>FOOTER: A footer item is an added button to the end of the list. This item can be clicked
|
||||
* but not selected and its position will never change.
|
||||
* </ul>
|
||||
*/
|
||||
final class RingtoneAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int VIEW_TYPE_FIXED_ITEM = 0;
|
||||
private static final int VIEW_TYPE_DYNAMIC_ITEM = 1;
|
||||
private static final int VIEW_TYPE_ADD_RINGTONE_ITEM = 2;
|
||||
private final Cursor mCursor;
|
||||
private final List<Integer> mFixedItemTitles;
|
||||
private final WorkRingtoneProvider mWorkRingtoneProvider;
|
||||
private final RingtoneSelectionListener mRingtoneSelectionListener;
|
||||
private final int mRowIDColumn;
|
||||
private int mSelectedItem = -1;
|
||||
@StringRes private Integer mAddRingtoneItemTitle;
|
||||
|
||||
/** Listener for ringtone selections. */
|
||||
interface RingtoneSelectionListener {
|
||||
void onRingtoneSelected(int position);
|
||||
void onAddRingtoneSelected();
|
||||
}
|
||||
/** Provides a mean to detect work ringtones. */
|
||||
interface WorkRingtoneProvider {
|
||||
boolean isWorkRingtone(int position);
|
||||
|
||||
Drawable getWorkIconDrawable();
|
||||
}
|
||||
|
||||
RingtoneAdapter(Cursor cursor, RingtoneSelectionListener ringtoneSelectionListener,
|
||||
WorkRingtoneProvider workRingtoneProvider) {
|
||||
mCursor = cursor;
|
||||
mRingtoneSelectionListener = ringtoneSelectionListener;
|
||||
mWorkRingtoneProvider = workRingtoneProvider;
|
||||
mFixedItemTitles = new ArrayList<>();
|
||||
mRowIDColumn = mCursor != null ? mCursor.getColumnIndex("_id") : -1;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
void setSelectedItem(int position) {
|
||||
notifyItemChanged(mSelectedItem);
|
||||
mSelectedItem = position;
|
||||
notifyItemChanged(mSelectedItem);
|
||||
}
|
||||
|
||||
void addTitleForFixedItem(@StringRes int textResId) {
|
||||
mFixedItemTitles.add(textResId);
|
||||
notifyItemInserted(mFixedItemTitles.size() - 1);
|
||||
}
|
||||
|
||||
void addTitleForAddRingtoneItem(@StringRes int textResId) {
|
||||
mAddRingtoneItemTitle = textResId;
|
||||
notifyItemInserted(getItemCount() - 1);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
if (viewType == VIEW_TYPE_FIXED_ITEM) {
|
||||
View fixedItemView = inflater.inflate(
|
||||
com.android.internal.R.layout.select_dialog_singlechoice_material, parent,
|
||||
false);
|
||||
|
||||
return new FixedItemViewHolder(fixedItemView, mRingtoneSelectionListener);
|
||||
}
|
||||
|
||||
if (viewType == VIEW_TYPE_ADD_RINGTONE_ITEM) {
|
||||
View addRingtoneItemView = inflater.inflate(R.layout.add_new_sound_item, parent, false);
|
||||
|
||||
return new AddRingtoneItemViewHolder(addRingtoneItemView, mRingtoneSelectionListener);
|
||||
}
|
||||
|
||||
View view = inflater.inflate(R.layout.radio_with_work_badge, parent, false);
|
||||
|
||||
return new DynamicItemViewHolder(view, mRingtoneSelectionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NotNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof FixedItemViewHolder) {
|
||||
FixedItemViewHolder viewHolder = (FixedItemViewHolder) holder;
|
||||
|
||||
viewHolder.onBind(mFixedItemTitles.get(position),
|
||||
/* isChecked= */ position == mSelectedItem);
|
||||
return;
|
||||
}
|
||||
if (holder instanceof AddRingtoneItemViewHolder) {
|
||||
AddRingtoneItemViewHolder viewHolder = (AddRingtoneItemViewHolder) holder;
|
||||
|
||||
viewHolder.onBind(mAddRingtoneItemTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(holder instanceof DynamicItemViewHolder)) {
|
||||
throw new IllegalArgumentException("holder type is not supported");
|
||||
}
|
||||
|
||||
DynamicItemViewHolder viewHolder = (DynamicItemViewHolder) holder;
|
||||
int pos = position - mFixedItemTitles.size();
|
||||
if (!mCursor.moveToPosition(pos)) {
|
||||
throw new IllegalStateException("Could not move cursor to position: " + pos);
|
||||
}
|
||||
|
||||
Drawable workIcon = (mWorkRingtoneProvider != null)
|
||||
&& mWorkRingtoneProvider.isWorkRingtone(position)
|
||||
? mWorkRingtoneProvider.getWorkIconDrawable() : null;
|
||||
|
||||
viewHolder.onBind(mCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX),
|
||||
/* isChecked= */ position == mSelectedItem, workIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (!mFixedItemTitles.isEmpty() && position < mFixedItemTitles.size()) {
|
||||
return VIEW_TYPE_FIXED_ITEM;
|
||||
}
|
||||
if (mAddRingtoneItemTitle != null && position == getItemCount() - 1) {
|
||||
return VIEW_TYPE_ADD_RINGTONE_ITEM;
|
||||
}
|
||||
|
||||
return VIEW_TYPE_DYNAMIC_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int itemCount = mFixedItemTitles.size() + mCursor.getCount();
|
||||
|
||||
if (mAddRingtoneItemTitle != null) {
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
int itemViewType = getItemViewType(position);
|
||||
if (itemViewType == VIEW_TYPE_FIXED_ITEM) {
|
||||
// Since the item is a fixed item, then we can use the position as a stable ID
|
||||
// since the order of the fixed items should never change.
|
||||
return position;
|
||||
}
|
||||
if (itemViewType == VIEW_TYPE_DYNAMIC_ITEM && mCursor != null
|
||||
&& mCursor.moveToPosition(position - mFixedItemTitles.size())
|
||||
&& mRowIDColumn != -1) {
|
||||
return mCursor.getLong(mRowIDColumn) + mFixedItemTitles.size();
|
||||
}
|
||||
|
||||
// The position is either invalid or the item is the add ringtone item view, so no stable
|
||||
// ID is returned. Add ringtone item view cannot be selected and only include an action
|
||||
// buttons.
|
||||
return NO_ID;
|
||||
}
|
||||
|
||||
private static class DynamicItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private final CheckedTextView mTitleTextView;
|
||||
private final ImageView mWorkIcon;
|
||||
|
||||
DynamicItemViewHolder(View itemView, RingtoneSelectionListener listener) {
|
||||
super(itemView);
|
||||
|
||||
mTitleTextView = itemView.findViewById(R.id.checked_text_view);
|
||||
mWorkIcon = itemView.findViewById(R.id.work_icon);
|
||||
itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
|
||||
}
|
||||
|
||||
void onBind(String title, boolean isChecked, Drawable workIcon) {
|
||||
Objects.requireNonNull(mTitleTextView);
|
||||
Objects.requireNonNull(mWorkIcon);
|
||||
|
||||
mTitleTextView.setText(title);
|
||||
mTitleTextView.setChecked(isChecked);
|
||||
|
||||
if (workIcon == null) {
|
||||
mWorkIcon.setVisibility(View.GONE);
|
||||
} else {
|
||||
mWorkIcon.setImageDrawable(workIcon);
|
||||
mWorkIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class FixedItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private final CheckedTextView mTitleTextView;
|
||||
|
||||
FixedItemViewHolder(View itemView, RingtoneSelectionListener listener) {
|
||||
super(itemView);
|
||||
|
||||
mTitleTextView = (CheckedTextView) itemView;
|
||||
itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
|
||||
}
|
||||
|
||||
void onBind(@StringRes int title, boolean isChecked) {
|
||||
Objects.requireNonNull(mTitleTextView);
|
||||
|
||||
mTitleTextView.setText(title);
|
||||
mTitleTextView.setChecked(isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddRingtoneItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView mTitleTextView;
|
||||
|
||||
AddRingtoneItemViewHolder(View itemView, RingtoneSelectionListener listener) {
|
||||
super(itemView);
|
||||
|
||||
mTitleTextView = itemView.findViewById(R.id.add_new_sound_text);
|
||||
itemView.setOnClickListener(v -> listener.onAddRingtoneSelected());
|
||||
}
|
||||
|
||||
void onBind(@StringRes int title) {
|
||||
Objects.requireNonNull(mTitleTextView);
|
||||
|
||||
mTitleTextView.setText(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,46 +16,20 @@
|
||||
|
||||
package com.android.soundpicker;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.NotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* The {@link RingtonePickerActivity} allows the user to choose one from all of the
|
||||
* available ringtones. The chosen ringtone's URI will be persisted as a string.
|
||||
@@ -63,106 +37,17 @@ import java.util.regex.Pattern;
|
||||
* @see RingtoneManager#ACTION_RINGTONE_PICKER
|
||||
*/
|
||||
@AndroidEntryPoint(AppCompatActivity.class)
|
||||
public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity implements
|
||||
AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener,
|
||||
DialogInterface.OnDismissListener {
|
||||
|
||||
private static final int POS_UNKNOWN = -1;
|
||||
public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity {
|
||||
|
||||
private static final String TAG = "RingtonePickerActivity";
|
||||
|
||||
private static final int DELAY_MS_SELECTION_PLAYED = 300;
|
||||
|
||||
private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
|
||||
|
||||
private static final String SAVE_CLICKED_POS = "clicked_pos";
|
||||
|
||||
private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
|
||||
|
||||
private static final int ADD_FILE_REQUEST_CODE = 300;
|
||||
// TODO: Use the extra key from RingtoneManager once it's added.
|
||||
private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY";
|
||||
private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false;
|
||||
|
||||
private RingtonePickerViewModel mRingtonePickerViewModel;
|
||||
|
||||
private int mType;
|
||||
|
||||
private Cursor mCursor;
|
||||
private Handler mHandler;
|
||||
private BadgedRingtoneAdapter mAdapter;
|
||||
|
||||
/** Whether this list has the 'Silent' item. */
|
||||
private boolean mHasSilentItem;
|
||||
|
||||
/** The Uri to place a checkmark next to. */
|
||||
private Uri mExistingUri;
|
||||
|
||||
/** Whether this list has the 'Default' item. */
|
||||
private boolean mHasDefaultItem;
|
||||
|
||||
/** The Uri to play when the 'Default' item is clicked. */
|
||||
private Uri mUriForDefaultItem;
|
||||
|
||||
/** Id of the user to which the ringtone picker should list the ringtones */
|
||||
private int mPickerUserId;
|
||||
|
||||
/**
|
||||
* Stable ID for the ringtone that is currently checked (may be -1 if no ringtone is checked).
|
||||
*/
|
||||
private long mCheckedItemId = -1;
|
||||
|
||||
private int mAttributesFlags;
|
||||
|
||||
private boolean mShowOkCancelButtons;
|
||||
|
||||
private AlertDialog mAlertDialog;
|
||||
|
||||
private int mCheckedItem = POS_UNKNOWN;
|
||||
|
||||
private final DialogInterface.OnClickListener mRingtoneClickListener =
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
/*
|
||||
* On item clicked
|
||||
*/
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (which == mCursor.getCount() + mRingtonePickerViewModel.getFixedItemCount()) {
|
||||
// The "Add new ringtone" item was clicked. Start a file picker intent to select
|
||||
// only audio files (MIME type "audio/*")
|
||||
final Intent chooseFile = getMediaFilePickerIntent();
|
||||
startActivityForResult(chooseFile, ADD_FILE_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the position of most recently clicked item
|
||||
setCheckedItem(which);
|
||||
|
||||
// In the buttonless (watch-only) version, preemptively set our result since we won't
|
||||
// have another chance to do so before the activity closes.
|
||||
if (!mShowOkCancelButtons) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
|
||||
mUriForDefaultItem));
|
||||
}
|
||||
|
||||
// Play clip
|
||||
playRingtone(which, 0);
|
||||
}
|
||||
|
||||
};
|
||||
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);
|
||||
@@ -170,300 +55,75 @@ public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity im
|
||||
|
||||
mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class);
|
||||
|
||||
mHandler = new Handler();
|
||||
|
||||
Intent intent = getIntent();
|
||||
mPickerUserId = UserHandle.myUserId();
|
||||
/**
|
||||
* Id of the user to which the ringtone picker should list the ringtones
|
||||
*/
|
||||
int pickerUserId = UserHandle.myUserId();
|
||||
|
||||
// Get the types of ringtones to show
|
||||
mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
|
||||
int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
|
||||
RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN);
|
||||
mRingtonePickerViewModel.initRingtoneManager(mType);
|
||||
setupCursor();
|
||||
|
||||
/*
|
||||
* Get whether to show the 'Default' item, and the URI to play when the
|
||||
* default is clicked
|
||||
*/
|
||||
mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
|
||||
if (mUriForDefaultItem == null) {
|
||||
mUriForDefaultItem = RingtonePickerViewModel.getDefaultItemUriByType(mType);
|
||||
// Get whether to show the 'Default' item, and the URI to play when the default is clicked
|
||||
boolean hasDefaultItem =
|
||||
intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
// The Uri to play when the 'Default' item is clicked.
|
||||
Uri uriForDefaultItem =
|
||||
intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
|
||||
if (uriForDefaultItem == null) {
|
||||
uriForDefaultItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType);
|
||||
}
|
||||
|
||||
// Get whether to show the 'Silent' item
|
||||
mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
// Get whether this list has the 'Silent' item.
|
||||
boolean hasSilentItem =
|
||||
intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
// AudioAttributes flags
|
||||
mAttributesFlags |= intent.getIntExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
|
||||
0 /*defaultValue == no flags*/);
|
||||
|
||||
mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
|
||||
boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
|
||||
|
||||
// Get the URI whose list item should have a checkmark
|
||||
Uri existingUri = intent
|
||||
.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
|
||||
|
||||
String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
|
||||
if (title == null) {
|
||||
title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType));
|
||||
}
|
||||
String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY);
|
||||
RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType(
|
||||
ringtonePickerCategory);
|
||||
|
||||
mRingtonePickerViewModel.init(new RingtonePickerViewModel.PickerConfig(title, pickerUserId,
|
||||
ringtoneType, hasDefaultItem, uriForDefaultItem, hasSilentItem,
|
||||
mAttributesFlags, existingUri, showOkCancelButtons, pickerType));
|
||||
|
||||
// The volume keys will control the stream that we are choosing a ringtone for
|
||||
setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType());
|
||||
|
||||
// Get the URI whose list item should have a checkmark
|
||||
mExistingUri = intent
|
||||
.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
|
||||
if (savedInstanceState == null) {
|
||||
TabbedDialogFragment dialogFragment = new TabbedDialogFragment();
|
||||
|
||||
// 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));
|
||||
|
||||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this,
|
||||
android.R.style.ThemeOverlay_Material_Dialog);
|
||||
alertDialogBuilder
|
||||
.setSingleChoiceItems(mAdapter, getCheckedItem(), mRingtoneClickListener)
|
||||
.setOnItemSelectedListener(this)
|
||||
.setOnDismissListener(this);
|
||||
if (mShowOkCancelButtons) {
|
||||
alertDialogBuilder
|
||||
.setPositiveButton(getString(com.android.internal.R.string.ok), this)
|
||||
.setNegativeButton(getString(com.android.internal.R.string.cancel), this);
|
||||
}
|
||||
|
||||
String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
|
||||
alertDialogBuilder.setTitle(
|
||||
title != null ? title : getString(RingtonePickerViewModel.getTitleByType(mType)));
|
||||
|
||||
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.");
|
||||
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
||||
Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG);
|
||||
if (prev != null) {
|
||||
ft.remove(prev);
|
||||
}
|
||||
prepareListView(listView);
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(SAVE_CLICKED_POS, getCheckedItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
mRingtonePickerViewModel.addRingtoneAsync(data.getData(),
|
||||
mType,
|
||||
mAddCustomRingtoneCallback,
|
||||
// Causes the callback to be executed on the main thread.
|
||||
ContextCompat.getMainExecutor(this.getApplicationContext()));
|
||||
ft.addToBackStack(null);
|
||||
dialogFragment.show(ft, TabbedDialogFragment.TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void prepareListView(@NonNull ListView listView) {
|
||||
// Reset the static item count, as this method can be called multiple times
|
||||
mRingtonePickerViewModel.resetFixedItemCount();
|
||||
|
||||
if (mHasDefaultItem) {
|
||||
int defaultItemPos = addDefaultRingtoneItem(listView);
|
||||
|
||||
if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
|
||||
setCheckedItem(defaultItemPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (mHasSilentItem) {
|
||||
int silentItemPos = addSilentItem(listView);
|
||||
|
||||
// The 'Silent' item should use a null Uri
|
||||
if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) {
|
||||
setCheckedItem(silentItemPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (getCheckedItem() == POS_UNKNOWN) {
|
||||
setCheckedItem(
|
||||
getListPosition(mRingtonePickerViewModel.getRingtonePosition(mExistingUri)));
|
||||
}
|
||||
|
||||
// In the buttonless (watch-only) version, preemptively set our result since we won't
|
||||
// have another chance to do so before the activity closes.
|
||||
if (!mShowOkCancelButtons) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
|
||||
mUriForDefaultItem));
|
||||
}
|
||||
// If external storage is available, add a button to install sounds from storage.
|
||||
if (resolvesMediaFilePicker()
|
||||
&& Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
addNewSoundItem(listView);
|
||||
}
|
||||
|
||||
// Enable context menu in ringtone items
|
||||
registerForContextMenu(listView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-query RingtoneManager for the most recent set of installed ringtones. May move the
|
||||
* selected item position to match the new position of the chosen sound.
|
||||
*
|
||||
* This should only need to happen after adding or removing a ringtone.
|
||||
*/
|
||||
private void requeryForAdapter() {
|
||||
// Refresh and set a new cursor, closing the old one.
|
||||
mRingtonePickerViewModel.initRingtoneManager(mType);
|
||||
setupCursor();
|
||||
mAdapter.changeCursor(mCursor);
|
||||
|
||||
// Update checked item location.
|
||||
int checkedPosition = POS_UNKNOWN;
|
||||
for (int i = 0; i < mAdapter.getCount(); i++) {
|
||||
if (mAdapter.getItemId(i) == mCheckedItemId) {
|
||||
checkedPosition = getListPosition(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mHasSilentItem && checkedPosition == POS_UNKNOWN) {
|
||||
checkedPosition = mRingtonePickerViewModel.getSilentItemPosition();
|
||||
}
|
||||
setCheckedItem(checkedPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a static item to the top of the list. A static item is one that is not from the
|
||||
* RingtoneManager.
|
||||
*
|
||||
* @param listView The ListView to add to.
|
||||
* @param textResId The resource ID of the text for the item.
|
||||
* @return The position of the inserted item.
|
||||
*/
|
||||
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);
|
||||
listView.addHeaderView(textView);
|
||||
mRingtonePickerViewModel.incrementFixedItemCount();
|
||||
return listView.getHeaderViewsCount() - 1;
|
||||
}
|
||||
|
||||
private int addDefaultRingtoneItem(@NonNull ListView listView) {
|
||||
int defaultRingtoneItemPos = addStaticItem(listView,
|
||||
RingtonePickerViewModel.getDefaultRingtoneItemTextByType(mType));
|
||||
mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos);
|
||||
return defaultRingtoneItemPos;
|
||||
}
|
||||
|
||||
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(@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);
|
||||
|
||||
text.setText(RingtonePickerViewModel.getAddNewItemTextByType(mType));
|
||||
|
||||
listView.addFooterView(view);
|
||||
}
|
||||
|
||||
private void setupCursor() {
|
||||
mCursor = new LocalizedCursor(
|
||||
mRingtonePickerViewModel.getRingtoneCursor(), getResources(), COLUMN_LABEL);
|
||||
}
|
||||
|
||||
private int getCheckedItem() {
|
||||
return mCheckedItem;
|
||||
}
|
||||
|
||||
private void setCheckedItem(int pos) {
|
||||
mCheckedItem = pos;
|
||||
ListView listView = mAlertDialog.getListView();
|
||||
if (listView != null) {
|
||||
listView.setItemChecked(pos, true);
|
||||
listView.smoothScrollToPosition(pos);
|
||||
}
|
||||
mCheckedItemId = mAdapter.getItemId(
|
||||
mRingtonePickerViewModel.itemPositionToRingtonePosition(pos));
|
||||
}
|
||||
|
||||
/*
|
||||
* On click of Ok/Cancel buttons
|
||||
*/
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
|
||||
|
||||
if (positiveResult) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
|
||||
mUriForDefaultItem));
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
/*
|
||||
* On item selected via keys
|
||||
*/
|
||||
public void onItemSelected(AdapterView parent, View view, int position, long id) {
|
||||
// footer view
|
||||
if (position >= mCursor.getCount() + mRingtonePickerViewModel.getFixedItemCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
playRingtone(position, DELAY_MS_SELECTION_PLAYED);
|
||||
|
||||
// In the buttonless (watch-only) version, preemptively set our result since we won't
|
||||
// have another chance to do so before the activity closes.
|
||||
if (!mShowOkCancelButtons) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri(getCheckedItem(),
|
||||
mUriForDefaultItem));
|
||||
}
|
||||
}
|
||||
|
||||
public void onNothingSelected(AdapterView parent) {
|
||||
}
|
||||
|
||||
private void playRingtone(int position, int delayMs) {
|
||||
mHandler.removeCallbacks(this);
|
||||
mRingtonePickerViewModel.setSampleItemPosition(position);
|
||||
mHandler.postDelayed(this, delayMs);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
mRingtonePickerViewModel.playRingtone(
|
||||
mRingtonePickerViewModel.itemPositionToRingtonePosition(
|
||||
mRingtonePickerViewModel.getSampleItemPosition()), mUriForDefaultItem,
|
||||
mAttributesFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
@@ -476,155 +136,29 @@ public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity im
|
||||
mRingtonePickerViewModel.onPause(isChangingConfigurations());
|
||||
}
|
||||
|
||||
private void setSuccessResultWithRingtone(Uri ringtoneUri) {
|
||||
setResult(RESULT_OK,
|
||||
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
|
||||
if (ringtoneManagerPos < 0) return ringtoneManagerPos;
|
||||
|
||||
return ringtoneManagerPos + mRingtonePickerViewModel.getFixedItemCount();
|
||||
}
|
||||
|
||||
private Intent getMediaFilePickerIntent() {
|
||||
final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
chooseFile.setType("audio/*");
|
||||
chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
|
||||
new String[] { "audio/*", "application/ogg" });
|
||||
return chooseFile;
|
||||
}
|
||||
|
||||
private boolean resolvesMediaFilePicker() {
|
||||
return getMediaFilePickerIntent().resolveActivity(getPackageManager()) != null;
|
||||
}
|
||||
|
||||
private static class LocalizedCursor extends CursorWrapper {
|
||||
|
||||
final int mTitleIndex;
|
||||
final Resources mResources;
|
||||
String mNamePrefix;
|
||||
final Pattern mSanitizePattern;
|
||||
|
||||
LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
|
||||
super(cursor);
|
||||
mTitleIndex = mCursor.getColumnIndex(columnLabel);
|
||||
mResources = resources;
|
||||
mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
|
||||
if (mTitleIndex == -1) {
|
||||
Log.e(TAG, "No index for column " + columnLabel);
|
||||
mNamePrefix = null;
|
||||
} else {
|
||||
try {
|
||||
// Build the prefix for the name of the resource to look up
|
||||
// format is: "ResourcePackageName::ResourceTypeName/"
|
||||
// (the type name is expected to be "string" but let's not hardcode it).
|
||||
// Here we use an existing resource "notification_sound_default" which is
|
||||
// always expected to be found.
|
||||
mNamePrefix = String.format("%s:%s/%s",
|
||||
mResources.getResourcePackageName(R.string.notification_sound_default),
|
||||
mResources.getResourceTypeName(R.string.notification_sound_default),
|
||||
SOUND_NAME_RES_PREFIX);
|
||||
} catch (NotFoundException e) {
|
||||
mNamePrefix = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Maps the ringtone picker category to the appropriate PickerType.
|
||||
* If the category is null or the feature is still not released, then it defaults to sound
|
||||
* picker.
|
||||
*
|
||||
* @param category the ringtone picker category.
|
||||
* @return the corresponding picker type.
|
||||
*/
|
||||
private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) {
|
||||
if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) {
|
||||
return RingtonePickerViewModel.PickerType.SOUND_PICKER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process resource name to generate a valid resource name.
|
||||
* @param input
|
||||
* @return a non-null String
|
||||
*/
|
||||
private String sanitize(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int columnIndex) {
|
||||
final String defaultName = mCursor.getString(columnIndex);
|
||||
if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
|
||||
return defaultName;
|
||||
}
|
||||
TypedValue value = new TypedValue();
|
||||
try {
|
||||
// the name currently in the database is used to derive a name to match
|
||||
// against resource names in this package
|
||||
mResources.getValue(mNamePrefix + sanitize(defaultName), value, false);
|
||||
} catch (NotFoundException e) {
|
||||
// no localized string, use the default string
|
||||
return defaultName;
|
||||
}
|
||||
if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
|
||||
Log.d(TAG, String.format("Replacing name %s with %s",
|
||||
defaultName, value.string.toString()));
|
||||
return value.string.toString();
|
||||
} else {
|
||||
Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
|
||||
return defaultName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BadgedRingtoneAdapter extends CursorAdapter {
|
||||
private final boolean mIsManagedProfile;
|
||||
|
||||
public BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile) {
|
||||
super(context, cursor);
|
||||
mIsManagedProfile = isManagedProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (position < 0) {
|
||||
return position;
|
||||
}
|
||||
return super.getItemId(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
return inflater.inflate(R.layout.radio_with_work_badge, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
// Set text as the title of the ringtone
|
||||
((TextView) view.findViewById(R.id.checked_text_view))
|
||||
.setText(cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX));
|
||||
|
||||
boolean isWorkRingtone = false;
|
||||
if (mIsManagedProfile) {
|
||||
/*
|
||||
* Display the work icon if the ringtone belongs to a work profile. We can tell that
|
||||
* a ringtone belongs to a work profile if the picker user is a managed profile, the
|
||||
* ringtone Uri is in external storage, and either the uri has no user id or has the
|
||||
* id of the picker user
|
||||
*/
|
||||
Uri currentUri = mRingtonePickerViewModel.getRingtoneUri(cursor.getPosition());
|
||||
int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId);
|
||||
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
|
||||
|
||||
if (uriUserId == mPickerUserId && uriWithoutUserId.toString()
|
||||
.startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
|
||||
isWorkRingtone = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImageView workIcon = (ImageView) view.findViewById(R.id.work_icon);
|
||||
if(isWorkRingtone) {
|
||||
workIcon.setImageDrawable(getPackageManager().getUserBadgeForDensityNoBackground(
|
||||
UserHandle.of(mPickerUserId), -1 /* density */));
|
||||
workIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
workIcon.setVisibility(View.GONE);
|
||||
}
|
||||
switch (category) {
|
||||
case "android.intent.category.RINGTONE_PICKER_RINGTONE":
|
||||
return RingtonePickerViewModel.PickerType.RINGTONE_PICKER;
|
||||
case "android.intent.category.RINGTONE_PICKER_SOUND":
|
||||
return RingtonePickerViewModel.PickerType.SOUND_PICKER;
|
||||
case "android.intent.category.RINGTONE_PICKER_VIBRATION":
|
||||
return RingtonePickerViewModel.PickerType.VIBRATION_PICKER;
|
||||
default:
|
||||
Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker.");
|
||||
return RingtonePickerViewModel.PickerType.SOUND_PICKER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
@@ -50,6 +51,7 @@ import javax.inject.Inject;
|
||||
public final class RingtonePickerViewModel extends ViewModel {
|
||||
|
||||
static final int RINGTONE_TYPE_UNKNOWN = -1;
|
||||
|
||||
/**
|
||||
* Keep the currently playing ringtone around when changing orientation, so that it
|
||||
* can be stopped later, after the activity is recreated.
|
||||
@@ -72,16 +74,87 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
private int mSampleItemPosition = ITEM_POSITION_UNKNOWN;
|
||||
/** The position in the list of the 'Default' item. */
|
||||
private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
|
||||
/** The number of static items in the list. */
|
||||
/** The number of fixed items in the list. */
|
||||
private int mFixedItemCount;
|
||||
private ListenableFuture<Uri> mAddCustomRingtoneFuture;
|
||||
private RingtoneManager mRingtoneManager;
|
||||
|
||||
/**
|
||||
* Stable ID for the ringtone that is currently selected (may be -1 if no ringtone is selected).
|
||||
*/
|
||||
private long mSelectedItemId = -1;
|
||||
private int mSelectedItemPosition = ITEM_POSITION_UNKNOWN;
|
||||
|
||||
/**
|
||||
* The ringtone that's currently playing.
|
||||
*/
|
||||
private Ringtone mCurrentRingtone;
|
||||
|
||||
private PickerConfig mPickerConfig;
|
||||
|
||||
public enum PickerType {
|
||||
RINGTONE_PICKER,
|
||||
SOUND_PICKER,
|
||||
VIBRATION_PICKER
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds immutable info on the picker that should be displayed.
|
||||
*/
|
||||
static final class PickerConfig {
|
||||
public final String title;
|
||||
/**
|
||||
* Id of the user to which the ringtone picker should list the ringtones.
|
||||
*/
|
||||
public final int userId;
|
||||
/**
|
||||
* Ringtone type.
|
||||
*/
|
||||
public final int ringtoneType;
|
||||
/**
|
||||
* Whether this list has the 'Default' item.
|
||||
*/
|
||||
public final boolean hasDefaultItem;
|
||||
/**
|
||||
* The Uri to play when the 'Default' item is clicked.
|
||||
*/
|
||||
public final Uri uriForDefaultItem;
|
||||
/**
|
||||
* Whether this list has the 'Silent' item.
|
||||
*/
|
||||
public final boolean hasSilentItem;
|
||||
/**
|
||||
* AudioAttributes flags.
|
||||
*/
|
||||
public final int audioAttributesFlags;
|
||||
/**
|
||||
* The Uri to place a checkmark next to.
|
||||
*/
|
||||
public final Uri existingUri;
|
||||
/**
|
||||
* In the buttonless (watch-only) version we don't show the OK/Cancel buttons.
|
||||
*/
|
||||
public final boolean showOkCancelButtons;
|
||||
|
||||
public final PickerType mPickerType;
|
||||
|
||||
PickerConfig(String title, int userId, int ringtoneType,
|
||||
boolean hasDefaultItem, Uri uriForDefaultItem, boolean hasSilentItem,
|
||||
int audioAttributesFlags, Uri existingUri, boolean showOkCancelButtons,
|
||||
PickerType pickerType) {
|
||||
this.title = title;
|
||||
this.userId = userId;
|
||||
this.ringtoneType = ringtoneType;
|
||||
this.hasDefaultItem = hasDefaultItem;
|
||||
this.uriForDefaultItem = uriForDefaultItem;
|
||||
this.hasSilentItem = hasSilentItem;
|
||||
this.audioAttributesFlags = audioAttributesFlags;
|
||||
this.existingUri = existingUri;
|
||||
this.showOkCancelButtons = showOkCancelButtons;
|
||||
this.mPickerType = pickerType;
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory,
|
||||
RingtoneFactory ringtoneFactory,
|
||||
@@ -91,6 +164,13 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
PickerConfig getPickerConfig() {
|
||||
return requireNonNull(mPickerConfig,
|
||||
"PickerConfig was never set. Did you forget to call "
|
||||
+ "RingtonePickerViewModel#init?");
|
||||
}
|
||||
|
||||
@StringRes
|
||||
static int getTitleByType(int ringtoneType) {
|
||||
switch (ringtoneType) {
|
||||
@@ -138,10 +218,11 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
void initRingtoneManager(int type) {
|
||||
void init(@NonNull PickerConfig pickerConfig) {
|
||||
mRingtoneManager = mRingtoneManagerFactory.create();
|
||||
if (type != RINGTONE_TYPE_UNKNOWN) {
|
||||
mRingtoneManager.setType(type);
|
||||
mPickerConfig = pickerConfig;
|
||||
if (pickerConfig.ringtoneType != RINGTONE_TYPE_UNKNOWN) {
|
||||
mRingtoneManager.setType(pickerConfig.ringtoneType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +247,7 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
*/
|
||||
void cancelPendingAsyncTasks() {
|
||||
if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) {
|
||||
mAddCustomRingtoneFuture.cancel(/*mayInterruptIfRunning=*/true);
|
||||
mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,36 +261,48 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
return mRingtoneManager.getCursor();
|
||||
}
|
||||
|
||||
Uri getRingtoneUri(int ringtonePosition) {
|
||||
Uri getRingtoneUri(int position) {
|
||||
requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE);
|
||||
return mRingtoneManager.getRingtoneUri(ringtonePosition);
|
||||
return mRingtoneManager.getRingtoneUri(mapListPositionToRingtonePosition(position));
|
||||
}
|
||||
|
||||
int getRingtonePosition(Uri uri) {
|
||||
requireNonNull(mRingtoneManager, RINGTONE_MANAGER_NULL_MESSAGE);
|
||||
return mRingtoneManager.getRingtonePosition(uri);
|
||||
return mapRingtonePositionToListPosition(mRingtoneManager.getRingtonePosition(uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the item in the list before header views were added.
|
||||
* Maps the item position in the list, to its equivalent position in the RingtoneManager.
|
||||
*
|
||||
* @param itemPosition the position of item in the list with any added headers.
|
||||
* @return position of the item in the list ignoring headers.
|
||||
* @param itemPosition the position of item in the list.
|
||||
* @return position of the item in the RingtoneManager.
|
||||
*/
|
||||
int itemPositionToRingtonePosition(int itemPosition) {
|
||||
private int mapListPositionToRingtonePosition(int itemPosition) {
|
||||
// If the manager position is -1 (for not found), then return that.
|
||||
if (itemPosition < 0) return itemPosition;
|
||||
|
||||
return itemPosition - mFixedItemCount;
|
||||
}
|
||||
|
||||
int getFixedItemCount() {
|
||||
return mFixedItemCount;
|
||||
/**
|
||||
* Maps the item position in the RingtoneManager, to its equivalent position in the list.
|
||||
*
|
||||
* @param itemPosition the position of the item in the RingtoneManager.
|
||||
* @return position of the item in the list.
|
||||
*/
|
||||
private int mapRingtonePositionToListPosition(int itemPosition) {
|
||||
// If the manager position is -1 (for not found), then return that.
|
||||
if (itemPosition < 0) return itemPosition;
|
||||
|
||||
return itemPosition + mFixedItemCount;
|
||||
}
|
||||
|
||||
void resetFixedItemCount() {
|
||||
mFixedItemCount = 0;
|
||||
}
|
||||
|
||||
void incrementFixedItemCount() {
|
||||
mFixedItemCount++;
|
||||
int incrementAndGetFixedItemCount() {
|
||||
return mFixedItemCount++;
|
||||
}
|
||||
|
||||
void setDefaultItemPosition(int defaultItemPosition) {
|
||||
@@ -232,6 +325,22 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
mSampleItemPosition = sampleItemPosition;
|
||||
}
|
||||
|
||||
public int getSelectedItemPosition() {
|
||||
return mSelectedItemPosition;
|
||||
}
|
||||
|
||||
public void setSelectedItemPosition(int selectedItemPosition) {
|
||||
mSelectedItemPosition = selectedItemPosition;
|
||||
}
|
||||
|
||||
public void setSelectedItemId(long selectedItemId) {
|
||||
mSelectedItemId = selectedItemId;
|
||||
}
|
||||
|
||||
public long getSelectedItemId() {
|
||||
return mSelectedItemId;
|
||||
}
|
||||
|
||||
void onPause(boolean isChangingConfigurations) {
|
||||
if (!isChangingConfigurations) {
|
||||
stopAnyPlayingRingtone();
|
||||
@@ -247,19 +356,19 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Uri getCurrentlySelectedRingtoneUri(int checkedItem, Uri defaultUri) {
|
||||
if (checkedItem == ITEM_POSITION_UNKNOWN) {
|
||||
// When the getCheckItem is POS_UNKNOWN, it is not the case we expected.
|
||||
Uri getCurrentlySelectedRingtoneUri() {
|
||||
if (mSelectedItemPosition == ITEM_POSITION_UNKNOWN) {
|
||||
// When the selected item is POS_UNKNOWN, it is not the case we expected.
|
||||
// We return null for this case.
|
||||
return null;
|
||||
} else if (checkedItem == mDefaultItemPosition) {
|
||||
} else if (mSelectedItemPosition == mDefaultItemPosition) {
|
||||
// Use the default Uri that they originally gave us.
|
||||
return defaultUri;
|
||||
} else if (checkedItem == mSilentItemPosition) {
|
||||
return mPickerConfig.uriForDefaultItem;
|
||||
} else if (mSelectedItemPosition == mSilentItemPosition) {
|
||||
// Use a null Uri for the 'Silent' item.
|
||||
return null;
|
||||
} else {
|
||||
return getRingtoneUri(itemPositionToRingtonePosition(checkedItem));
|
||||
return getRingtoneUri(mSelectedItemPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +389,8 @@ public final class RingtonePickerViewModel extends ViewModel {
|
||||
mCurrentRingtone.setStreamType(mRingtoneManager.inferStreamType());
|
||||
}
|
||||
} else {
|
||||
mCurrentRingtone = mRingtoneManager.getRingtone(position);
|
||||
mCurrentRingtone = mRingtoneManager.getRingtone(
|
||||
mapListPositionToRingtonePosition(position));
|
||||
}
|
||||
|
||||
if (mCurrentRingtone != null) {
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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.Activity;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A fragment that will display a picker used to select sound or silent. It also includes the
|
||||
* ability to add custom sounds.
|
||||
*/
|
||||
@AndroidEntryPoint(Fragment.class)
|
||||
public class SoundPickerFragment extends Hilt_SoundPickerFragment {
|
||||
|
||||
private static final String TAG = "SoundPickerFragment";
|
||||
private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
|
||||
private static final int POS_UNKNOWN = -1;
|
||||
|
||||
private RingtonePickerViewModel.PickerConfig mPickerConfig;
|
||||
private boolean mIsManagedProfile;
|
||||
private RingtonePickerViewModel mRingtonePickerViewModel;
|
||||
private RingtoneAdapter mRingtoneAdapter;
|
||||
private RecyclerView mSoundRecyclerView;
|
||||
|
||||
private final RingtoneAdapter.WorkRingtoneProvider mWorkRingtoneProvider =
|
||||
new RingtoneAdapter.WorkRingtoneProvider() {
|
||||
private Drawable mWorkIconDrawable;
|
||||
@Override
|
||||
public boolean isWorkRingtone(int position) {
|
||||
if (mIsManagedProfile) {
|
||||
/*
|
||||
* Display the w ork icon if the ringtone belongs to a work profile. We
|
||||
* can tell that a ringtone belongs to a work profile if the picker user
|
||||
* is a managed profile, the ringtone Uri is in external storage, and
|
||||
* either the uri has no user id or has the id of the picker user
|
||||
*/
|
||||
Uri currentUri = mRingtonePickerViewModel.getRingtoneUri(position);
|
||||
int uriUserId = ContentProvider.getUserIdFromUri(currentUri,
|
||||
mPickerConfig.userId);
|
||||
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
|
||||
|
||||
return uriUserId == mPickerConfig.userId
|
||||
&& uriWithoutUserId.toString().startsWith(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getWorkIconDrawable() {
|
||||
if (mWorkIconDrawable == null) {
|
||||
mWorkIconDrawable = requireActivity().getPackageManager()
|
||||
.getUserBadgeForDensityNoBackground(
|
||||
UserHandle.of(mPickerConfig.userId), /* density= */ -1);
|
||||
}
|
||||
|
||||
return mWorkIconDrawable;
|
||||
}
|
||||
};
|
||||
|
||||
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(requireActivity().getApplicationContext(),
|
||||
R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
|
||||
ActivityResultLauncher<Intent> mActivityResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
new ActivityResultCallback<ActivityResult>() {
|
||||
@Override
|
||||
public void onActivityResult(ActivityResult result) {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
// There are no request codes
|
||||
Intent data = result.getData();
|
||||
mRingtonePickerViewModel.addRingtoneAsync(data.getData(),
|
||||
mPickerConfig.ringtoneType,
|
||||
mAddCustomRingtoneCallback,
|
||||
// Causes the callback to be executed on the main thread.
|
||||
ContextCompat.getMainExecutor(
|
||||
requireActivity().getApplicationContext()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private final RingtoneAdapter.RingtoneSelectionListener mRingtoneSelectionListener =
|
||||
new RingtoneAdapter.RingtoneSelectionListener() {
|
||||
@Override
|
||||
public void onRingtoneSelected(int position) {
|
||||
SoundPickerFragment.this.setSelectedItem(position);
|
||||
|
||||
// In the buttonless (watch-only) version, preemptively set our result since
|
||||
// we won't have another chance to do so before the activity closes.
|
||||
if (!mPickerConfig.showOkCancelButtons) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
|
||||
}
|
||||
|
||||
// Play clip
|
||||
playRingtone(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddRingtoneSelected() {
|
||||
// The "Add new ringtone" item was clicked. Start a file picker intent to
|
||||
// select only audio files (MIME type "audio/*")
|
||||
final Intent chooseFile = getMediaFilePickerIntent();
|
||||
mActivityResultLauncher.launch(chooseFile);
|
||||
}
|
||||
};
|
||||
|
||||
public SoundPickerFragment() {
|
||||
super(R.layout.fragment_sound_picker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
|
||||
RingtonePickerViewModel.class);
|
||||
mSoundRecyclerView = view.findViewById(R.id.recycler_view);
|
||||
Objects.requireNonNull(mSoundRecyclerView);
|
||||
|
||||
mPickerConfig = mRingtonePickerViewModel.getPickerConfig();
|
||||
mIsManagedProfile = UserManager.get(requireActivity()).isManagedProfile(
|
||||
mPickerConfig.userId);
|
||||
|
||||
mRingtoneAdapter = createRingtoneAdapter();
|
||||
mSoundRecyclerView.setHasFixedSize(true);
|
||||
mSoundRecyclerView.setAdapter(mRingtoneAdapter);
|
||||
mSoundRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
||||
setSelectedItem(mRingtonePickerViewModel.getSelectedItemPosition());
|
||||
prepareRecyclerView(mSoundRecyclerView);
|
||||
}
|
||||
|
||||
private void prepareRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
// Reset the static item count, as this method can be called multiple times
|
||||
mRingtonePickerViewModel.resetFixedItemCount();
|
||||
|
||||
if (mPickerConfig.hasDefaultItem) {
|
||||
int defaultItemPos = addDefaultRingtoneItem();
|
||||
|
||||
if (getSelectedItem() == POS_UNKNOWN
|
||||
&& RingtoneManager.isDefault(mPickerConfig.existingUri)) {
|
||||
setSelectedItem(defaultItemPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (mPickerConfig.hasSilentItem) {
|
||||
int silentItemPos = addSilentItem();
|
||||
|
||||
// The 'Silent' item should use a null Uri
|
||||
if (getSelectedItem() == POS_UNKNOWN && mPickerConfig.existingUri == null) {
|
||||
setSelectedItem(silentItemPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (getSelectedItem() == POS_UNKNOWN) {
|
||||
setSelectedItem(
|
||||
mRingtonePickerViewModel.getRingtonePosition(mPickerConfig.existingUri));
|
||||
}
|
||||
|
||||
// In the buttonless (watch-only) version, preemptively set our result since we won't
|
||||
// have another chance to do so before the activity closes.
|
||||
if (!mPickerConfig.showOkCancelButtons) {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
|
||||
}
|
||||
// If external storage is available, add a button to install sounds from storage.
|
||||
if (resolvesMediaFilePicker()
|
||||
&& Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
addNewSoundItem();
|
||||
}
|
||||
|
||||
// Enable context menu in ringtone items
|
||||
registerForContextMenu(recyclerView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-query RingtoneManager for the most recent set of installed ringtones. May move the
|
||||
* selected item position to match the new position of the chosen sound.
|
||||
* <p>
|
||||
* This should only need to happen after adding or removing a ringtone.
|
||||
*/
|
||||
private void requeryForAdapter() {
|
||||
// Refresh and set a new cursor, closing the old one.
|
||||
mRingtonePickerViewModel.init(mPickerConfig);
|
||||
mRingtoneAdapter = createRingtoneAdapter();
|
||||
mSoundRecyclerView.setAdapter(mRingtoneAdapter);
|
||||
prepareRecyclerView(mSoundRecyclerView);
|
||||
|
||||
// Update selected item location.
|
||||
int selectedPosition = POS_UNKNOWN;
|
||||
for (int i = 0; i < mRingtoneAdapter.getItemCount(); i++) {
|
||||
if (mRingtoneAdapter.getItemId(i) == mRingtonePickerViewModel.getSelectedItemId()) {
|
||||
selectedPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mPickerConfig.hasSilentItem && selectedPosition == POS_UNKNOWN) {
|
||||
selectedPosition = mRingtonePickerViewModel.getSilentItemPosition();
|
||||
}
|
||||
setSelectedItem(selectedPosition);
|
||||
}
|
||||
|
||||
private void playRingtone(int position) {
|
||||
mRingtonePickerViewModel.setSampleItemPosition(position);
|
||||
mRingtonePickerViewModel.playRingtone(mRingtonePickerViewModel.getSampleItemPosition(),
|
||||
mPickerConfig.uriForDefaultItem, mPickerConfig.audioAttributesFlags);
|
||||
}
|
||||
|
||||
private boolean resolvesMediaFilePicker() {
|
||||
return getMediaFilePickerIntent().resolveActivity(requireActivity().getPackageManager())
|
||||
!= null;
|
||||
}
|
||||
|
||||
private Intent getMediaFilePickerIntent() {
|
||||
final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
chooseFile.setType("audio/*");
|
||||
chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
|
||||
new String[]{"audio/*", "application/ogg"});
|
||||
return chooseFile;
|
||||
}
|
||||
|
||||
private void setSuccessResultWithRingtone(Uri ringtoneUri) {
|
||||
requireActivity().setResult(Activity.RESULT_OK,
|
||||
new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
|
||||
}
|
||||
|
||||
private int getSelectedItem() {
|
||||
return mRingtonePickerViewModel.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
private void setSelectedItem(int pos) {
|
||||
Objects.requireNonNull(mRingtoneAdapter);
|
||||
mRingtonePickerViewModel.setSelectedItemPosition(pos);
|
||||
mRingtoneAdapter.setSelectedItem(pos);
|
||||
mRingtonePickerViewModel.setSelectedItemId(mRingtoneAdapter.getItemId(pos));
|
||||
mSoundRecyclerView.scrollToPosition(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fixed item to the fixed items list . A fixed item is one that is not from
|
||||
* the RingtoneManager.
|
||||
*
|
||||
* @param textResId The resource ID of the text for the item.
|
||||
* @return The position of the inserted item.
|
||||
*/
|
||||
private int addFixedItem(int textResId) {
|
||||
mRingtoneAdapter.addTitleForFixedItem(textResId);
|
||||
return mRingtonePickerViewModel.incrementAndGetFixedItemCount();
|
||||
}
|
||||
|
||||
private int addDefaultRingtoneItem() {
|
||||
int defaultRingtoneItemPos = addFixedItem(
|
||||
RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
|
||||
mPickerConfig.ringtoneType));
|
||||
mRingtonePickerViewModel.setDefaultItemPosition(defaultRingtoneItemPos);
|
||||
return defaultRingtoneItemPos;
|
||||
}
|
||||
|
||||
private int addSilentItem() {
|
||||
int silentItemPos = addFixedItem(com.android.internal.R.string.ringtone_silent);
|
||||
mRingtonePickerViewModel.setSilentItemPosition(silentItemPos);
|
||||
return silentItemPos;
|
||||
}
|
||||
|
||||
private void addNewSoundItem() {
|
||||
mRingtoneAdapter.addTitleForAddRingtoneItem(
|
||||
RingtonePickerViewModel.getAddNewItemTextByType(mPickerConfig.ringtoneType));
|
||||
}
|
||||
|
||||
private RingtoneAdapter createRingtoneAdapter() {
|
||||
LocalizedCursor cursor = new LocalizedCursor(
|
||||
mRingtonePickerViewModel.getRingtoneCursor(), getResources(), COLUMN_LABEL);
|
||||
return new RingtoneAdapter(cursor, mRingtoneSelectionListener, mWorkRingtoneProvider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 static android.app.Activity.RESULT_CANCELED;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A dialog fragment with a sound and/or vibration tab based on the picker type.
|
||||
* <ul>
|
||||
* <li> Ringtone Pickers will display both sound and vibration tabs.
|
||||
* <li> Sound Pickers will only display the sound tab.
|
||||
* <li> Vibration Pickers will only display the vibration tab.
|
||||
* </ul>
|
||||
*/
|
||||
@AndroidEntryPoint(DialogFragment.class)
|
||||
public class TabbedDialogFragment extends Hilt_TabbedDialogFragment {
|
||||
|
||||
static final String TAG = "TabbedDialogFragment";
|
||||
|
||||
private RingtonePickerViewModel mRingtonePickerViewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
|
||||
RingtonePickerViewModel.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(),
|
||||
android.R.style.ThemeOverlay_Material_Dialog)
|
||||
.setTitle(mRingtonePickerViewModel.getPickerConfig().title);
|
||||
// Do not show OK/Cancel buttons in the buttonless (watch-only) version.
|
||||
if (mRingtonePickerViewModel.getPickerConfig().showOkCancelButtons) {
|
||||
dialogBuilder
|
||||
.setPositiveButton(getString(com.android.internal.R.string.ok),
|
||||
(dialog, whichButton) -> {
|
||||
setSuccessResultWithRingtone(
|
||||
mRingtonePickerViewModel.getCurrentlySelectedRingtoneUri());
|
||||
requireActivity().finish();
|
||||
})
|
||||
.setNegativeButton(getString(com.android.internal.R.string.cancel),
|
||||
(dialog, whichButton) -> {
|
||||
requireActivity().setResult(RESULT_CANCELED);
|
||||
requireActivity().finish();
|
||||
});
|
||||
}
|
||||
|
||||
View view = buildTabbedView(requireActivity().getLayoutInflater());
|
||||
dialogBuilder.setView(view);
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull @NotNull DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
if (!requireActivity().isChangingConfigurations()) {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull @NotNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (!requireActivity().isChangingConfigurations()) {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void setSuccessResultWithRingtone(Uri ringtoneUri) {
|
||||
requireActivity().setResult(Activity.RESULT_OK,
|
||||
new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the tabbed layout view and adds the required fragments. If there's only one
|
||||
* fragment to display, then the tab area is hidden.
|
||||
* @param inflater The LayoutInflater that is used to inflate the tabbed view.
|
||||
* @return The tabbed view.
|
||||
*/
|
||||
private View buildTabbedView(@NonNull LayoutInflater inflater) {
|
||||
View view = inflater.inflate(R.layout.fragment_tabbed_dialog, null, false);
|
||||
TabLayout tabLayout = view.findViewById(R.id.tabLayout);
|
||||
ViewPager2 viewPager = view.findViewById(R.id.masterViewPager);
|
||||
Objects.requireNonNull(tabLayout);
|
||||
Objects.requireNonNull(viewPager);
|
||||
|
||||
ViewPagerAdapter adapter = new ViewPagerAdapter(requireActivity());
|
||||
addFragments(adapter);
|
||||
|
||||
if (adapter.getItemCount() == 1) {
|
||||
// Hide the tab area since there's only one fragment to display.
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
viewPager.setAdapter(adapter);
|
||||
new TabLayoutMediator(tabLayout, viewPager,
|
||||
(tab, position) -> tab.setText(adapter.getTitle(position))).attach();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appropriate fragments to the adapter based on the PickerType.
|
||||
*
|
||||
* @param adapter The adapter to add the fragments to.
|
||||
*/
|
||||
private void addFragments(ViewPagerAdapter adapter) {
|
||||
switch (mRingtonePickerViewModel.getPickerConfig().mPickerType) {
|
||||
case RINGTONE_PICKER:
|
||||
adapter.addFragment(getString(R.string.sound_page_title),
|
||||
new SoundPickerFragment());
|
||||
adapter.addFragment(getString(R.string.vibration_page_title),
|
||||
new VibrationPickerFragment());
|
||||
break;
|
||||
case SOUND_PICKER:
|
||||
adapter.addFragment(getString(R.string.sound_page_title),
|
||||
new SoundPickerFragment());
|
||||
break;
|
||||
case VIBRATION_PICKER:
|
||||
adapter.addFragment(getString(R.string.vibration_page_title),
|
||||
new VibrationPickerFragment());
|
||||
break;
|
||||
default:
|
||||
adapter.addFragment(getString(R.string.sound_page_title),
|
||||
new SoundPickerFragment());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 androidx.fragment.app.Fragment;
|
||||
|
||||
/**
|
||||
* A fragment that will display a picker used to select vibration.
|
||||
*/
|
||||
public class VibrationPickerFragment extends Fragment {
|
||||
|
||||
public VibrationPickerFragment() {
|
||||
super(R.layout.fragment_vibration_picker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An adapter used to populate pages inside a ViewPager.
|
||||
*/
|
||||
public class ViewPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
private final List<Fragment> mFragments = new ArrayList<>();
|
||||
private final List<String> mTitles = new ArrayList<>();
|
||||
|
||||
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fragment and page title to the adapter.
|
||||
* @param title the title of the page in the ViewPager.
|
||||
* @param fragment the fragment that will be inflated on this page.
|
||||
*/
|
||||
public void addFragment(String title, Fragment fragment) {
|
||||
mTitles.add(title);
|
||||
mFragments.add(fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title of the requested page.
|
||||
* @param position the position of the page in the Viewpager.
|
||||
* @return The title of the requested page.
|
||||
*/
|
||||
public String getTitle(int position) {
|
||||
return mTitles.get(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
return Objects.requireNonNull(mFragments.get(position),
|
||||
"Could not find a fragment using position: " + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mFragments.size();
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() {
|
||||
mViewModel.initRingtoneManager(RINGTONE_TYPE_UNKNOWN);
|
||||
mViewModel.init(createPickerConfig(RINGTONE_TYPE_UNKNOWN));
|
||||
|
||||
verify(mMockRingtoneManagerFactory).create();
|
||||
verify(mMockRingtoneManager, never()).setType(anyInt());
|
||||
@@ -120,7 +120,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_NOTIFICATION);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION));
|
||||
|
||||
verify(mMockRingtoneManagerFactory).create();
|
||||
verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
|
||||
@@ -129,14 +129,14 @@ public class RingtonePickerViewModelTest {
|
||||
@Test
|
||||
public void testGetStreamType_returnsTheCorrectStreamType() {
|
||||
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() {
|
||||
when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
assertEquals(mViewModel.getRingtoneCursor(), mMockCursor);
|
||||
}
|
||||
|
||||
@@ -144,14 +144,14 @@ public class RingtonePickerViewModelTest {
|
||||
public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() {
|
||||
Uri expectedUri = DEFAULT_URI;
|
||||
when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(expectedUri);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(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.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
|
||||
@@ -161,7 +161,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -172,7 +172,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
|
||||
Ringtone mockRingtone1 = createMockRingtone();
|
||||
Ringtone mockRingtone2 = createMockRingtone();
|
||||
@@ -186,7 +186,7 @@ public class RingtonePickerViewModelTest {
|
||||
verify(mockRingtone1, never()).stop();
|
||||
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
|
||||
mMockListeningExecutorServiceFactory);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -197,7 +197,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -208,7 +208,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
|
||||
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -226,7 +226,7 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() {
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -237,21 +237,24 @@ public class RingtonePickerViewModelTest {
|
||||
|
||||
@Test
|
||||
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsUnknown_returnsNull() {
|
||||
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(POS_UNKNOWN, DEFAULT_URI);
|
||||
mViewModel.setSelectedItemPosition(POS_UNKNOWN);
|
||||
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri();
|
||||
assertNull(uri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsDefaultPos_returnsDefaultUri() {
|
||||
Uri expectedUri = DEFAULT_URI;
|
||||
Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri(DEFAULT_RINGTONE_POSITION,
|
||||
expectedUri);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri();
|
||||
assertEquals(actualUri, expectedUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCurrentlySelectedRingtoneUri_checkedItemIsSilentPos_returnsNull() {
|
||||
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri(SILENT_RINGTONE_POSITION, DEFAULT_URI);
|
||||
mViewModel.setSelectedItemPosition(SILENT_RINGTONE_POSITION);
|
||||
Uri uri = mViewModel.getCurrentlySelectedRingtoneUri();
|
||||
assertNull(uri);
|
||||
}
|
||||
|
||||
@@ -266,7 +269,7 @@ public class RingtonePickerViewModelTest {
|
||||
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI);
|
||||
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
|
||||
mMockListeningExecutorServiceFactory);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
|
||||
mMainThreadExecutor);
|
||||
verify(mockCallback, never()).onFailure(any());
|
||||
@@ -290,7 +293,7 @@ public class RingtonePickerViewModelTest {
|
||||
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(DEFAULT_URI);
|
||||
mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
|
||||
mMockListeningExecutorServiceFactory);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback1,
|
||||
mMainThreadExecutor);
|
||||
verify(mockCallback1, never()).onFailure(any());
|
||||
@@ -312,7 +315,7 @@ public class RingtonePickerViewModelTest {
|
||||
when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI,
|
||||
RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri);
|
||||
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
|
||||
mMainThreadExecutor);
|
||||
|
||||
@@ -330,7 +333,7 @@ public class RingtonePickerViewModelTest {
|
||||
when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow(
|
||||
IOException.class);
|
||||
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.addRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, mockCallback,
|
||||
mMainThreadExecutor);
|
||||
|
||||
@@ -342,8 +345,9 @@ public class RingtonePickerViewModelTest {
|
||||
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);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.setSelectedItemPosition(RINGTONE_POSITION);
|
||||
Uri actualUri = mViewModel.getCurrentlySelectedRingtoneUri();
|
||||
|
||||
verify(mMockRingtoneManager).getRingtoneUri(RINGTONE_POSITION);
|
||||
assertEquals(actualUri, expectedUri);
|
||||
@@ -353,7 +357,7 @@ public class RingtonePickerViewModelTest {
|
||||
public void testPlayRingtone_stopsPreviouslyRunningRingtone() {
|
||||
// Start playing the first ringtone
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
|
||||
@@ -369,7 +373,7 @@ public class RingtonePickerViewModelTest {
|
||||
@Test
|
||||
public void testPlayRingtone_samplePosEqualToSilentPos_onlyStopPlayingRingtone() {
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
mViewModel.playRingtone(DEFAULT_RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
|
||||
@@ -388,7 +392,7 @@ public class RingtonePickerViewModelTest {
|
||||
@Test
|
||||
public void testPlayRingtone_samplePosEqualToDefaultPos_playDefaultRingtone() {
|
||||
mViewModel.setSampleItemPosition(DEFAULT_RINGTONE_POSITION);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
|
||||
when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
|
||||
|
||||
@@ -403,7 +407,7 @@ public class RingtonePickerViewModelTest {
|
||||
@Test
|
||||
public void testPlayRingtone_samplePosNotEqualToDefaultPos_playRingtone() {
|
||||
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
|
||||
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
|
||||
AudioAttributes.FLAG_AUDIBILITY_ENFORCED);
|
||||
@@ -417,7 +421,7 @@ public class RingtonePickerViewModelTest {
|
||||
@Test
|
||||
public void testPlayRingtone_withNoAttributeFlags_doNotUpdateRingtoneAttributesFlags() {
|
||||
mViewModel.setSampleItemPosition(RINGTONE_POSITION);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
|
||||
mViewModel.playRingtone(RINGTONE_POSITION, DEFAULT_URI,
|
||||
NO_ATTRIBUTES_FLAGS);
|
||||
@@ -430,7 +434,7 @@ public class RingtonePickerViewModelTest {
|
||||
public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() {
|
||||
int expectedPosition = 1;
|
||||
when(mMockRingtoneManager.getRingtonePosition(any())).thenReturn(expectedPosition);
|
||||
mViewModel.initRingtoneManager(RingtoneManager.TYPE_RINGTONE);
|
||||
mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE));
|
||||
int actualPosition = mViewModel.getRingtonePosition(DEFAULT_URI);
|
||||
|
||||
assertEquals(actualPosition, expectedPosition);
|
||||
@@ -553,4 +557,13 @@ public class RingtonePickerViewModelTest {
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build();
|
||||
}
|
||||
|
||||
private RingtonePickerViewModel.PickerConfig createPickerConfig(int ringtoneType) {
|
||||
return new RingtonePickerViewModel.PickerConfig("Phone ringtone", /* userId= */ 1,
|
||||
ringtoneType, /* hasDefaultItem= */ true,
|
||||
/* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
|
||||
/* audioAttributesFlags= */0, /* existingUri= */ Uri.parse(""),
|
||||
/* showOkCancelButtons= */ true,
|
||||
RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user