| /* |
| * Copyright (C) 2016 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.deskclock; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Activity; |
| import android.app.DialogFragment; |
| import android.app.FragmentManager; |
| import android.app.FragmentTransaction; |
| import android.app.LoaderManager; |
| import android.content.AsyncTaskLoader; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Loader; |
| import android.content.pm.PackageManager; |
| import android.database.Cursor; |
| import android.media.AudioManager; |
| import android.media.RingtoneManager; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.StringRes; |
| import android.support.v13.app.FragmentCompat; |
| import android.support.v4.content.ContextCompat; |
| import android.support.v7.app.AlertDialog; |
| import android.util.Pair; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.BaseAdapter; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import static android.Manifest.permission.READ_EXTERNAL_STORAGE; |
| |
| /** |
| * This ringtone picker offers some flexibility over the system ringtone picker. It can be themed |
| * and it allows control of the ringtones that are displayed and their labels. |
| */ |
| public final class RingtonePickerDialogFragment extends DialogFragment implements |
| DialogInterface.OnClickListener, |
| FragmentCompat.OnRequestPermissionsResultCallback, |
| LoaderManager.LoaderCallbacks<RingtoneManager> { |
| |
| private static final String ARGS_KEY_TITLE = "title"; |
| private static final String ARGS_KEY_DEFAULT_RINGTONE_TITLE = "default_ringtone_title"; |
| private static final String ARGS_KEY_DEFAULT_RINGTONE_URI = "default_ringtone_uri"; |
| private static final String ARGS_KEY_EXISTING_RINGTONE_URI = "existing_ringtone_uri"; |
| |
| private static final String STATE_KEY_REQUESTING_PERMISSION = "requesting_permission"; |
| private static final String STATE_KEY_SELECTED_RINGTONE_URI = "selected_ringtone_uri"; |
| |
| private boolean mRequestingPermission; |
| private Uri mSelectedRingtoneUri; |
| |
| private RingtoneAdapter mRingtoneAdapter; |
| private AlertDialog mDialog; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| // Restore saved instance state. |
| if (savedInstanceState != null) { |
| mRequestingPermission = savedInstanceState.getBoolean(STATE_KEY_REQUESTING_PERMISSION); |
| mSelectedRingtoneUri = |
| savedInstanceState.getParcelable(STATE_KEY_SELECTED_RINGTONE_URI); |
| } else { |
| // Initialize selection to the existing ringtone. |
| mSelectedRingtoneUri = getArguments().getParcelable(ARGS_KEY_EXISTING_RINGTONE_URI); |
| } |
| } |
| |
| @Override |
| public AlertDialog onCreateDialog(Bundle savedInstanceState) { |
| final AlertDialog.Builder builder = |
| new AlertDialog.Builder(getActivity(), R.style.DialogTheme); |
| final Bundle args = getArguments(); |
| |
| mRingtoneAdapter = new RingtoneAdapter(builder.getContext()) |
| .addStaticRingtone(R.string.silent_ringtone_title, Utils.RINGTONE_SILENT) |
| .addStaticRingtone(args.getInt(ARGS_KEY_DEFAULT_RINGTONE_TITLE), |
| (Uri) args.getParcelable(ARGS_KEY_DEFAULT_RINGTONE_URI)); |
| mDialog = builder.setTitle(args.getInt(ARGS_KEY_TITLE)) |
| .setSingleChoiceItems(mRingtoneAdapter, -1, this /* listener */) |
| .setPositiveButton(android.R.string.ok, this /* listener */) |
| .setNegativeButton(android.R.string.cancel, null /* listener */) |
| .create(); |
| |
| return mDialog; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| |
| if (savedInstanceState == null |
| && ContextCompat.checkSelfPermission(getActivity(), READ_EXTERNAL_STORAGE) |
| != PackageManager.PERMISSION_GRANTED) { |
| mRequestingPermission = true; |
| FragmentCompat.requestPermissions(this, new String[] { READ_EXTERNAL_STORAGE }, |
| 0 /* requestCode */); |
| } else if (!mRequestingPermission) { |
| getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */); |
| } |
| } |
| |
| @Override |
| public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, |
| @NonNull int[] grantResults) { |
| super.onRequestPermissionsResult(requestCode, permissions, grantResults); |
| |
| for (final String permission : permissions) { |
| if (READ_EXTERNAL_STORAGE.equals(permission)) { |
| mRequestingPermission = false; |
| |
| // Show the dialog now that we've prompted the user for permissions. |
| mDialog.show(); |
| |
| getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| |
| // Disable the positive button until we have a valid selection (Note: this is the first |
| // point in the fragment's lifecycle that the dialog *should* have all its views). |
| final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); |
| if (positiveButton != null) { |
| positiveButton.setEnabled(!mRingtoneAdapter.isEmpty() && mSelectedRingtoneUri != null); |
| } |
| |
| // Hide the dialog if we are currently requesting permissions. |
| if (mRequestingPermission) { |
| mDialog.hide(); |
| } |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| |
| // Allow the volume rocker to control the alarm stream volume while the picker is showing. |
| mDialog.setVolumeControlStream(AudioManager.STREAM_ALARM); |
| } |
| |
| @Override |
| public void onSaveInstanceState(@NonNull Bundle outState) { |
| super.onSaveInstanceState(outState); |
| |
| outState.putBoolean(STATE_KEY_REQUESTING_PERMISSION, mRequestingPermission); |
| outState.putParcelable(STATE_KEY_SELECTED_RINGTONE_URI, mSelectedRingtoneUri); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| |
| // Stop playing the preview unless we are currently undergoing a configuration change |
| // (e.g. orientation). |
| final Activity activity = getActivity(); |
| if (activity != null && !activity.isChangingConfigurations()) { |
| RingtonePreviewKlaxon.stop(activity); |
| } |
| } |
| |
| @Override |
| public Loader<RingtoneManager> onCreateLoader(int id, Bundle args) { |
| return new RingtoneManagerLoader(getActivity()); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<RingtoneManager> loader, RingtoneManager ringtoneManager) { |
| // Swap in the new ringtone manager. |
| mRingtoneAdapter.setRingtoneManager(ringtoneManager); |
| |
| // Preserve the selected ringtone. |
| final ListView listView = mDialog.getListView(); |
| final int checkedPosition = mRingtoneAdapter.getRingtonePosition(mSelectedRingtoneUri); |
| if (checkedPosition != ListView.INVALID_POSITION) { |
| listView.setItemChecked(checkedPosition, true); |
| |
| // Also scroll the list to the selected ringtone (this method is poorly named). |
| listView.setSelection(checkedPosition); |
| } else { |
| // Can't find the selected ringtone, clear the current selection. |
| mSelectedRingtoneUri = null; |
| listView.clearChoices(); |
| } |
| |
| // Enable the positive button if we have a valid selection (Note: the positive button may |
| // be null if this callback returns before onStart). |
| final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); |
| if (positiveButton != null) { |
| positiveButton.setEnabled(mSelectedRingtoneUri != null); |
| } |
| |
| // On M devices the checked view's drawable state isn't updated properly when it is first |
| // bound, so we must use a blunt approach to force it to refresh correctly. |
| if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { |
| listView.post(new Runnable() { |
| @Override |
| public void run() { |
| for (int i = listView.getChildCount() - 1; i >= 0; --i) { |
| listView.getChildAt(i).refreshDrawableState(); |
| } |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<RingtoneManager> loader) { |
| mRingtoneAdapter.setRingtoneManager(null); |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| if (which == DialogInterface.BUTTON_POSITIVE) { |
| if (mSelectedRingtoneUri != null) { |
| OnRingtoneSelectedListener listener = null; |
| if (getParentFragment() instanceof OnRingtoneSelectedListener) { |
| listener = (OnRingtoneSelectedListener) getParentFragment(); |
| } else if (getActivity() instanceof OnRingtoneSelectedListener) { |
| listener = (OnRingtoneSelectedListener) getActivity(); |
| } |
| |
| if (listener != null) { |
| listener.onRingtoneSelected(getTag(), mSelectedRingtoneUri); |
| } |
| } |
| } else if (which >= 0) { |
| // Update the selected ringtone, enabling the positive button if valid. |
| mSelectedRingtoneUri = mRingtoneAdapter.getItem(which); |
| |
| // Enable the positive button if we have a valid selection. |
| final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); |
| positiveButton.setEnabled(mSelectedRingtoneUri != null); |
| |
| // Play the preview for the clicked ringtone. |
| if (mSelectedRingtoneUri == null |
| || mSelectedRingtoneUri.equals(Utils.RINGTONE_SILENT)) { |
| RingtonePreviewKlaxon.stop(getActivity()); |
| } else { |
| RingtonePreviewKlaxon.start(getActivity(), mSelectedRingtoneUri); |
| } |
| } |
| } |
| |
| /** |
| * Callback interface for when a ringtone is selected via a picker. Typically implemented by |
| * the activity or fragment which launches the ringtone picker. |
| */ |
| public interface OnRingtoneSelectedListener { |
| /** |
| * Called when the ringtone picker dialog is confirmed and dismissed. |
| * |
| * @param tag the tag of the ringtone picker dialog fragment |
| * @param ringtoneUri the uri of the ringtone that was picked |
| */ |
| void onRingtoneSelected(String tag, Uri ringtoneUri); |
| } |
| |
| public static final class Builder { |
| |
| private final Bundle mArgs = new Bundle(); |
| |
| public Builder setTitle(@StringRes int titleId) { |
| mArgs.putInt(ARGS_KEY_TITLE, titleId); |
| return this; |
| } |
| |
| public Builder setDefaultRingtoneTitle(@StringRes int titleId) { |
| mArgs.putInt(ARGS_KEY_DEFAULT_RINGTONE_TITLE, titleId); |
| return this; |
| } |
| |
| public Builder setDefaultRingtoneUri(Uri ringtoneUri) { |
| mArgs.putParcelable(ARGS_KEY_DEFAULT_RINGTONE_URI, ringtoneUri); |
| return this; |
| } |
| |
| public Builder setExistingRingtoneUri(Uri ringtoneUri) { |
| mArgs.putParcelable(ARGS_KEY_EXISTING_RINGTONE_URI, ringtoneUri); |
| return this; |
| } |
| |
| public void show(FragmentManager fragmentManager, String tag) { |
| final DialogFragment fragment = new RingtonePickerDialogFragment(); |
| fragment.setArguments(mArgs); |
| fragment.show(fragmentManager, tag); |
| } |
| |
| public void show(FragmentTransaction fragmentTransaction, String tag) { |
| final DialogFragment fragment = new RingtonePickerDialogFragment(); |
| fragment.setArguments(mArgs); |
| fragment.show(fragmentTransaction, tag); |
| } |
| } |
| |
| private static class RingtoneManagerLoader extends AsyncTaskLoader<RingtoneManager> { |
| |
| private RingtoneManager mRingtoneManager; |
| private Cursor mRingtoneCursor; |
| |
| public RingtoneManagerLoader(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public RingtoneManager loadInBackground() { |
| final RingtoneManager ringtoneManager = new RingtoneManager(getContext()); |
| ringtoneManager.setType(AudioManager.STREAM_ALARM); |
| |
| // Force the ringtone manager to load its ringtones. The cursor will be cached |
| // internally by the ringtone manager. |
| try { |
| ringtoneManager.getCursor(); |
| } catch (Exception e) { |
| LogUtils.e("Error getting Ringtone Manager cursor", e); |
| } |
| |
| return ringtoneManager; |
| } |
| |
| @Override |
| public void deliverResult(RingtoneManager ringtoneManager) { |
| if (mRingtoneManager != ringtoneManager) { |
| if (mRingtoneCursor != null && !mRingtoneCursor.isClosed()) { |
| mRingtoneCursor.close(); |
| } |
| mRingtoneManager = ringtoneManager; |
| try { |
| mRingtoneCursor = mRingtoneManager.getCursor(); |
| } catch (Exception e) { |
| LogUtils.e("Error getting Ringtone Manager cursor", e); |
| } |
| } |
| super.deliverResult(ringtoneManager); |
| } |
| |
| @Override |
| protected void onReset() { |
| super.onReset(); |
| |
| if (mRingtoneCursor != null && !mRingtoneCursor.isClosed()) { |
| mRingtoneCursor.close(); |
| mRingtoneCursor = null; |
| } |
| mRingtoneManager = null; |
| } |
| |
| @Override |
| protected void onStartLoading() { |
| super.onStartLoading(); |
| |
| if (mRingtoneManager != null) { |
| deliverResult(mRingtoneManager); |
| } else { |
| forceLoad(); |
| } |
| } |
| } |
| |
| private static class RingtoneAdapter extends BaseAdapter { |
| |
| private final List<Pair<Integer, Uri>> mStaticRingtones; |
| private final LayoutInflater mLayoutInflater; |
| |
| private RingtoneManager mRingtoneManager; |
| private Cursor mRingtoneCursor; |
| |
| public RingtoneAdapter(Context context) { |
| mStaticRingtones = new ArrayList<>(2 /* magic */); |
| mLayoutInflater = LayoutInflater.from(context); |
| } |
| |
| /** |
| * Add a static ringtone item to display before the system ones. |
| * |
| * @param title the title to display for the ringtone |
| * @param ringtoneUri the {@link Uri} for the ringtone |
| * @return this object so method calls may be chained |
| */ |
| public RingtoneAdapter addStaticRingtone(@StringRes int title, Uri ringtoneUri) { |
| if (title != 0 && ringtoneUri != null) { |
| mStaticRingtones.add(Pair.create(title, ringtoneUri)); |
| notifyDataSetChanged(); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Set the {@link RingtoneManager} to query for system ringtones. |
| * |
| * @param ringtoneManager the {@link RingtoneManager} to query for system ringtones |
| * @return this object so method calls may be chained |
| */ |
| public RingtoneAdapter setRingtoneManager(RingtoneManager ringtoneManager) { |
| mRingtoneManager = ringtoneManager; |
| try { |
| mRingtoneCursor = ringtoneManager == null ? null : ringtoneManager.getCursor(); |
| } catch (Exception e) { |
| LogUtils.e("Error getting Ringtone Manager cursor", e); |
| } |
| notifyDataSetChanged(); |
| |
| return this; |
| } |
| |
| /** |
| * Returns the position of the given ringtone uri. |
| * |
| * @param ringtoneUri the {@link Uri} to retrieve the position of |
| * @return the ringtones position in the adapter |
| */ |
| public int getRingtonePosition(Uri ringtoneUri) { |
| if (ringtoneUri == null) { |
| return ListView.INVALID_POSITION; |
| } |
| |
| final int staticRingtoneCount = mStaticRingtones.size(); |
| for (int position = 0; position < staticRingtoneCount; ++position) { |
| if (ringtoneUri.equals(mStaticRingtones.get(position).second)) { |
| return position; |
| } |
| } |
| |
| final int position = mRingtoneManager.getRingtonePosition(ringtoneUri); |
| if (position != -1) { |
| return position + staticRingtoneCount; |
| } |
| return ListView.INVALID_POSITION; |
| } |
| |
| @Override |
| public int getCount() { |
| if (mRingtoneCursor == null) { |
| return 0; |
| } |
| return mStaticRingtones.size() + mRingtoneCursor.getCount(); |
| } |
| |
| @Override |
| public Uri getItem(int position) { |
| final int staticRingtoneCount = mStaticRingtones.size(); |
| if (position < staticRingtoneCount) { |
| return mStaticRingtones.get(position).second; |
| } |
| return mRingtoneManager.getRingtoneUri(position - staticRingtoneCount); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| @SuppressLint("PrivateResource") |
| public View getView(int position, View view, ViewGroup parent) { |
| if (view == null) { |
| // Use AlertDialog's singleChoiceItemLayout directly here, if this breaks in the |
| // future just copy the layout to DeskClock's res/. |
| view = mLayoutInflater.inflate(R.layout.select_dialog_singlechoice_material, |
| parent, false /* attachToRoot */); |
| } |
| |
| final TextView textView = (TextView) view.findViewById(android.R.id.text1); |
| final int staticRingtoneCount = mStaticRingtones.size(); |
| if (position < staticRingtoneCount) { |
| textView.setText(mStaticRingtones.get(position).first); |
| } else { |
| mRingtoneCursor.moveToPosition(position - staticRingtoneCount); |
| textView.setText(mRingtoneCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX)); |
| } |
| |
| return view; |
| } |
| } |
| } |