diff --git a/res/xml/preferences_notifications.xml b/res/xml/preferences_notifications.xml index df3ffef344..04ef9563f5 100644 --- a/res/xml/preferences_notifications.xml +++ b/res/xml/preferences_notifications.xml @@ -9,11 +9,11 @@ android:title="@string/preferences__notifications" android:defaultValue="true" /> - - () { @@ -383,6 +391,27 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi } } + private class RingtoneClickedListener implements Preference.OnPreferenceClickListener { + + @Override + public boolean onPreferenceClick(Preference preference) { + Uri uri = recipient.getRingtone(); + + if (uri == null) uri = Settings.System.DEFAULT_NOTIFICATION_URI; + else if (uri.toString().isEmpty()) uri = null; + + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri); + + startActivityForResult(intent, 1); + + return true; + } + } + private class VibrateChangeListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { diff --git a/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java index 195f760e9a..4d2bb4feda 100644 --- a/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java @@ -9,6 +9,7 @@ import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.util.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.RecipientDatabase; @@ -19,6 +20,9 @@ import org.thoughtcrime.securesms.util.Util; public abstract class AbstractNotificationBuilder extends NotificationCompat.Builder { + @SuppressWarnings("unused") + private static final String TAG = AbstractNotificationBuilder.class.getSimpleName(); + protected Context context; protected NotificationPrivacyPreference privacy; diff --git a/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index ec9bf41d91..848af5490d 100644 --- a/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -10,8 +10,6 @@ import android.view.View; import org.thoughtcrime.securesms.components.CustomDefaultPreference; import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference; import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreferenceDialogFragmentCompat; -import org.thoughtcrime.securesms.preferences.widgets.RingtonePreference; -import org.thoughtcrime.securesms.preferences.widgets.RingtonePreferenceDialogFragmentCompat; public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat { @@ -32,9 +30,7 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp public void onDisplayPreferenceDialog(Preference preference) { DialogFragment dialogFragment = null; - if (preference instanceof RingtonePreference) { - dialogFragment = RingtonePreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } else if (preference instanceof ColorPickerPreference) { + if (preference instanceof ColorPickerPreference) { dialogFragment = ColorPickerPreferenceDialogFragmentCompat.newInstance(preference.getKey()); } else if (preference instanceof CustomDefaultPreference) { dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey()); diff --git a/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index bcaccf4f9c..7b374b59f4 100644 --- a/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.preferences; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.media.Ringtone; import android.media.RingtoneManager; @@ -17,11 +19,13 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.preferences.widgets.AdvancedRingtonePreference; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import static android.app.Activity.RESULT_OK; + public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment { + @SuppressWarnings("unused") private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); private MasterSecret masterSecret; @@ -44,12 +48,27 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme this.findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF) .setOnPreferenceChangeListener(new ListSummaryListener()); + this.findPreference(TextSecurePreferences.RINGTONE_PREF) + .setOnPreferenceClickListener(preference -> { + String current = TextSecurePreferences.getNotificationRingtone(getContext()); + + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current == null ? null : Uri.parse(current)); + + startActivityForResult(intent, 1); + + return true; + }); + initializeListSummary((ListPreference) findPreference(TextSecurePreferences.LED_COLOR_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.LED_BLINK_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.REPEAT_ALERTS_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)); - initializeRingtoneSummary((AdvancedRingtonePreference) findPreference(TextSecurePreferences.RINGTONE_PREF)); + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); } @Override @@ -63,6 +82,16 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__notifications); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == 1 && resultCode == RESULT_OK && data != null) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + TextSecurePreferences.setNotificationRingtone(getContext(), uri != null ? uri.toString() : null); + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); + } + } + private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { @@ -72,6 +101,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme preference.setSummary(R.string.preferences__silent); } else { Ringtone tone = RingtoneManager.getRingtone(getActivity(), value); + if (tone != null) { preference.setSummary(tone.getTitle(getActivity())); } @@ -81,7 +111,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme } } - private void initializeRingtoneSummary(AdvancedRingtonePreference pref) { + private void initializeRingtoneSummary(Preference pref) { RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); String encodedUri = sharedPreferences.getString(pref.getKey(), null); @@ -98,6 +128,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme } private class NotificationPrivacyListener extends ListSummaryListener { + @SuppressLint("StaticFieldLeak") @Override public boolean onPreferenceChange(Preference preference, Object value) { new AsyncTask() { diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/AdvancedRingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/widgets/AdvancedRingtonePreference.java deleted file mode 100644 index d11fc60f39..0000000000 --- a/src/org/thoughtcrime/securesms/preferences/widgets/AdvancedRingtonePreference.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.support.annotation.RequiresApi; -import android.util.AttributeSet; - - -public class AdvancedRingtonePreference extends RingtonePreference { - - private Uri currentRingtone; - - public AdvancedRingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - public AdvancedRingtonePreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - public AdvancedRingtonePreference(Context context) { - super(context); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public AdvancedRingtonePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - protected Uri onRestoreRingtone() { - if (currentRingtone == null) return super.onRestoreRingtone(); - else return currentRingtone; - } - - public void setCurrentRingtone(Uri uri) { - currentRingtone = uri; - } - - -} diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreference.java deleted file mode 100644 index a62f59ce28..0000000000 --- a/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreference.java +++ /dev/null @@ -1,462 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.provider.MediaStore; -import android.support.annotation.IntDef; -import android.support.v4.content.res.TypedArrayUtils; -import android.support.v7.preference.DialogPreference; -import android.support.v7.preference.Preference; -import android.text.TextUtils; -import android.util.AttributeSet; - -import org.thoughtcrime.securesms.R; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * A {@link Preference} that displays a ringtone picker as a dialog. - *

- * This preference will save the picked ringtone's URI as a string into the SharedPreferences. The - * saved URI can be fed directly into {@link RingtoneManager#getRingtone(Context, Uri)} to get the - * {@link Ringtone} instance that can be played. - * - * @see RingtoneManager - * @see Ringtone - */ -@SuppressWarnings("WeakerAccess,unused") -public class RingtonePreference extends DialogPreference { - private static final int CUSTOM_RINGTONE_REQUEST_CODE = 0x9000; - private static final int WRITE_FILES_PERMISSION_REQUEST_CODE = 0x9001; - - private int ringtoneType; - private boolean showDefault; - private boolean showSilent; - private boolean showAdd; - - private Uri ringtoneUri; - -// private CharSequence summaryHasRingtone; -// private CharSequence summary; - - private int miscCustomRingtoneRequestCode = CUSTOM_RINGTONE_REQUEST_CODE; - private int miscPermissionRequestCode = WRITE_FILES_PERMISSION_REQUEST_CODE; - - @IntDef({ - RingtoneManager.TYPE_ALL, - RingtoneManager.TYPE_ALARM, - RingtoneManager.TYPE_NOTIFICATION, - RingtoneManager.TYPE_RINGTONE - }) - @Retention(RetentionPolicy.SOURCE) - protected @interface RingtoneType { - } - -// static { -// PreferenceFragmentCompat.addDialogPreference(RingtonePreference.class, RingtonePreferenceDialogFragmentCompat.class); -// } - - public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - - android.preference.RingtonePreference proxyPreference; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - proxyPreference = new android.preference.RingtonePreference(context, attrs, defStyleAttr, defStyleRes); - } else { - proxyPreference = new android.preference.RingtonePreference(context, attrs, defStyleAttr); - } - - ringtoneType = proxyPreference.getRingtoneType(); - showDefault = proxyPreference.getShowDefault(); - showSilent = proxyPreference.getShowSilent(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RingtonePreference, defStyleAttr, 0); - showAdd = a.getBoolean(R.styleable.RingtonePreference_showAdd, true); -// summaryHasRingtone = a.getText(R.styleable.RingtonePreference_summaryHasRingtone); - a.recycle(); - -// summary = super.getSummary(); - } - - public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - @SuppressLint("RestrictedApi") - public RingtonePreference(Context context, AttributeSet attrs) { - this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle, - android.R.attr.dialogPreferenceStyle)); - } - - public RingtonePreference(Context context) { - this(context, null); - } - - /** - * Returns the sound type(s) that are shown in the picker. - * - * @return The sound type(s) that are shown in the picker. - * @see #setRingtoneType(int) - */ - @RingtoneType - public int getRingtoneType() { - return ringtoneType; - } - - /** - * Sets the sound type(s) that are shown in the picker. See {@link RingtoneManager} for the - * possible values. - * - * @param ringtoneType The sound type(s) that are shown in the picker. - */ - public void setRingtoneType(@RingtoneType int ringtoneType) { - this.ringtoneType = ringtoneType; - } - - /** - * Returns whether to a show an item for the default sound/ringtone. - * - * @return Whether to show an item for the default sound/ringtone. - */ - public boolean getShowDefault() { - return showDefault; - } - - /** - * Sets whether to show an item for the default sound/ringtone. The default - * to use will be deduced from the sound type(s) being shown. - * - * @param showDefault Whether to show the default or not. - */ - public void setShowDefault(boolean showDefault) { - this.showDefault = showDefault; - } - - /** - * Returns whether to a show an item for 'None'. - * - * @return Whether to show an item for 'None'. - */ - public boolean getShowSilent() { - return showSilent; - } - - /** - * Sets whether to show an item for 'None'. - * - * @param showSilent Whether to show 'None'. - */ - public void setShowSilent(boolean showSilent) { - this.showSilent = showSilent; - } - - /** - * Returns whether to a show an item for 'Add new ringtone'. - *

- * Note that this requires {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE}. If it's - * not supplied in the manifest, the item won't be displayed. - * - * @return Whether to show an item for 'Add new ringtone'. - */ - public boolean getShowAdd() { - return showAdd; - } - - boolean shouldShowAdd() { - if (showAdd) { - try { - PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), PackageManager.GET_PERMISSIONS); - String[] permissions = pInfo.requestedPermissions; - for (String permission : permissions) { - if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission)) { - return true; - } - } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - } - - return false; - } - - /** - * Sets whether to show an item for 'Add new ringtone'. - *

- * Note that this requires {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE}. If it's - * not supplied in the manifest, the item won't be displayed. - * - * @param showAdd Whether to show 'Add new ringtone'. - */ - public void setShowAdd(boolean showAdd) { - this.showAdd = showAdd; - } - - /** - * This request code will be used to start the file picker activity that the user can use - * to add new ringtones. The new ringtone will be delivered to - * {@link RingtonePreferenceDialogFragmentCompat#onActivityResult(int, int, Intent)}. - *

- * The default value equals to {@link #CUSTOM_RINGTONE_REQUEST_CODE} - * ({@value #CUSTOM_RINGTONE_REQUEST_CODE}). - */ - public int getCustomRingtoneRequestCode() { - return miscCustomRingtoneRequestCode; - } - - /** - * Sets the request code that will be used to start the file picker activity that the user can - * use to add new ringtones. The new ringtone will be delivered to - * {@link RingtonePreferenceDialogFragmentCompat#onActivityResult(int, int, Intent)}. - *

- * The default value equals to {@link #CUSTOM_RINGTONE_REQUEST_CODE} - * ({@value #CUSTOM_RINGTONE_REQUEST_CODE}). - * - * @param customRingtoneRequestCode the request code for the file picker - */ - public void setCustomRingtoneRequestCode(int customRingtoneRequestCode) { - this.miscCustomRingtoneRequestCode = customRingtoneRequestCode; - } - - /** - * This request code will be used to ask for user permission to save (write) new ringtone - * to one of the public external storage directories (only applies to API 23+). The result will - * be delivered to - * {@link RingtonePreferenceDialogFragmentCompat#onRequestPermissionsResult(int, String[], int[])}. - *

- * The default value equals to {@link #WRITE_FILES_PERMISSION_REQUEST_CODE} - * ({@value #WRITE_FILES_PERMISSION_REQUEST_CODE}). - */ - public int getPermissionRequestCode() { - return miscPermissionRequestCode; - } - - /** - * Sets the request code that will be used to ask for user permission to save (write) new - * ringtone to one of the public external storage directories (only applies to API 23+). The - * result will be delivered to - * {@link RingtonePreferenceDialogFragmentCompat#onRequestPermissionsResult(int, String[], int[])}. - *

- * The default value equals to {@link #WRITE_FILES_PERMISSION_REQUEST_CODE} - * ({@value #WRITE_FILES_PERMISSION_REQUEST_CODE}). - * - * @param permissionRequestCode the request code for the file picker - */ - public void setPermissionRequestCode(int permissionRequestCode) { - this.miscPermissionRequestCode = permissionRequestCode; - } - - public Uri getRingtone() { - return onRestoreRingtone(); - } - - public void setRingtone(Uri uri) { - setInternalRingtone(uri, false); - } - - private void setInternalRingtone(Uri uri, boolean force) { - Uri oldUri = onRestoreRingtone(); - - final boolean changed = (oldUri != null && !oldUri.equals(uri)) || (uri != null && !uri.equals(oldUri)); - - if (changed || force) { - final boolean wasBlocking = shouldDisableDependents(); - - ringtoneUri = uri; - onSaveRingtone(uri); - - final boolean isBlocking = shouldDisableDependents(); - - notifyChanged(); - - if (isBlocking != wasBlocking) { - notifyDependencyChange(isBlocking); - } - } - } - - /** - * Called when a ringtone is chosen. - *

- * By default, this saves the ringtone URI to the persistent storage as a - * string. - * - * @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null. - */ - protected void onSaveRingtone(Uri ringtoneUri) { - persistString(ringtoneUri != null ? ringtoneUri.toString() : ""); - } - - /** - * Called when the chooser is about to be shown and the current ringtone - * should be marked. Can return null to not mark any ringtone. - *

- * By default, this restores the previous ringtone URI from the persistent - * storage. - * - * @return The ringtone to be marked as the current ringtone. - */ - protected Uri onRestoreRingtone() { - final String uriString = getPersistedString(ringtoneUri == null ? null : ringtoneUri.toString()); - return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null; - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getString(index); - } - - @Override - protected void onSetInitialValue(boolean restoreValue, Object defaultValueObj) { - final String defaultValue = (String) defaultValueObj; - setInternalRingtone(restoreValue ? onRestoreRingtone() : (!TextUtils.isEmpty(defaultValue) ? Uri.parse(defaultValue) : null), true); - } - - @Override - public boolean shouldDisableDependents() { - return super.shouldDisableDependents() || onRestoreRingtone() == null; - } - -// /** -// * Returns the summary of this Preference. If no {@code summaryHasRingtone} is set, this will be -// * displayed if no ringtone is selected; otherwise the ringtone title will be used. -// * -// * @return The summary. -// */ -// @Override -// public CharSequence getSummary() { -// if (ringtoneUri == null) { -// return summary; -// } else { -// String ringtoneTitle = getRingtoneTitle(); -// if (summaryHasRingtone != null && ringtoneTitle != null) { -// return String.format(summaryHasRingtone.toString(), ringtoneTitle); -// } else if (ringtoneTitle != null) { -// return ringtoneTitle; -// } else { -// return summary; -// } -// } -// } - -// /** -// * Sets the summary for this Preference with a CharSequence. If no {@code summaryHasRingtone} is -// * set, this will be displayed if no ringtone is selected; otherwise the ringtone title will be -// * used. -// * -// * @param summary The summary for the preference. -// */ -// @Override -// public void setSummary(CharSequence summary) { -// super.setSummary(summary); -// if (summary == null && this.summary != null) { -// this.summary = null; -// } else if (summary != null && !summary.equals(this.summary)) { -// this.summary = summary.toString(); -// } -// } - -// /** -// * Returns the picked summary for this Preference. This will be displayed if the preference -// * has a persisted value or the default value is set. If the summary -// * has a {@linkplain java.lang.String#format String formatting} -// * marker in it (i.e. "%s" or "%1$s"), then the current ringtone's title -// * will be substituted in its place. -// * -// * @return The picked summary. -// */ -// @Nullable -// public CharSequence getSummaryHasRingtone() { -// return summaryHasRingtone; -// } - -// /** -// * Sets the picked summary for this Preference with a resource ID. This will be displayed if the -// * preference has a persisted value or the default value is set. If the summary -// * has a {@linkplain java.lang.String#format String formatting} -// * marker in it (i.e. "%s" or "%1$s"), then the current ringtone's title -// * will be substituted in its place. -// * -// * @param resId The summary as a resource. -// * @see #setSummaryHasRingtone(CharSequence) -// */ -// public void setSummaryHasRingtone(@StringRes int resId) { -// setSummaryHasRingtone(getContext().getString(resId)); -// } - -// /** -// * Sets the picked summary for this Preference with a CharSequence. This will be displayed if -// * the preference has a persisted value or the default value is set. If the summary -// * has a {@linkplain java.lang.String#format String formatting} -// * marker in it (i.e. "%s" or "%1$s"), then the current ringtone's title -// * will be substituted in its place. -// * -// * @param summaryHasRingtone The summary for the preference. -// */ -// public void setSummaryHasRingtone(@Nullable CharSequence summaryHasRingtone) { -// if (summaryHasRingtone == null && this.summaryHasRingtone != null) { -// this.summaryHasRingtone = null; -// } else if (summaryHasRingtone != null && !summaryHasRingtone.equals(this.summaryHasRingtone)) { -// this.summaryHasRingtone = summaryHasRingtone.toString(); -// } -// -// notifyChanged(); -// } - - /** - * Returns the selected ringtone's title, or {@code null} if no ringtone is picked. - * - * @return The selected ringtone's title, or {@code null} if no ringtone is picked. - */ - public String getRingtoneTitle() { - Context context = getContext(); - ContentResolver cr = context.getContentResolver(); - String[] projection = {MediaStore.MediaColumns.TITLE}; - - String ringtoneTitle = null; - - if (ringtoneUri != null) { - int type = RingtoneManager.getDefaultType(ringtoneUri); - - switch (type) { - case RingtoneManager.TYPE_ALL: - case RingtoneManager.TYPE_RINGTONE: - ringtoneTitle = context.getString(R.string.RingtonePreference_ringtone_default); - break; - case RingtoneManager.TYPE_ALARM: - ringtoneTitle = context.getString(R.string.RingtonePreference_alarm_sound_default); - break; - case RingtoneManager.TYPE_NOTIFICATION: - ringtoneTitle = context.getString(R.string.RingtonePreference_notification_sound_default); - break; - default: - try { - Cursor cursor = cr.query(ringtoneUri, projection, null, null, null); - if (cursor != null) { - if (cursor.moveToFirst()) { - ringtoneTitle = cursor.getString(0); - } - - cursor.close(); - } - } catch (Exception ignore) { - } - } - } - - return ringtoneTitle; - } -} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreferenceDialogFragmentCompat.java b/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreferenceDialogFragmentCompat.java deleted file mode 100644 index f9d9ef5a70..0000000000 --- a/src/org/thoughtcrime/securesms/preferences/widgets/RingtonePreferenceDialogFragmentCompat.java +++ /dev/null @@ -1,593 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.media.MediaScannerConnection; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.provider.OpenableColumns; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.preference.PreferenceDialogFragmentCompat; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.webkit.MimeTypeMap; -import android.widget.ArrayAdapter; -import android.widget.CursorAdapter; -import android.widget.HeaderViewListAdapter; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.Toast; - -import org.thoughtcrime.securesms.R; - -import java.io.Closeable; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.SecureRandom; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; - -import static android.app.Activity.RESULT_OK; - -public class RingtonePreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat { - private static final String TAG = "RingtonePrefDialog"; - - private static final String CURSOR_DEFAULT_ID = "-2"; - private static final String CURSOR_NONE_ID = "-1"; - - private int selectedIndex = -1; - private String[] data; - - private RingtoneManager ringtoneManager; - private Ringtone defaultRingtone; - - public static RingtonePreferenceDialogFragmentCompat newInstance(String key) { - RingtonePreferenceDialogFragmentCompat fragment = new RingtonePreferenceDialogFragmentCompat(); - Bundle b = new Bundle(1); - b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key); - fragment.setArguments(b); - return fragment; - } - - private RingtonePreference getRingtonePreference() { - return (RingtonePreference) getPreference(); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - } - - @Override - public void onPause() { - super.onPause(); - - stopPlaying(); - } - - private void stopPlaying() { - if (defaultRingtone != null && defaultRingtone.isPlaying()) { - defaultRingtone.stop(); - } - - if (ringtoneManager != null) { - ringtoneManager.stopPreviousRingtone(); - } - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - super.onPrepareDialogBuilder(builder); - - RingtonePreference ringtonePreference = getRingtonePreference(); - - createRingtoneList(ringtonePreference.getRingtone()); - - final Context context = getContext(); - - final int ringtoneType = ringtonePreference.getRingtoneType(); - final boolean showDefault = ringtonePreference.getShowDefault(); - final boolean showSilent = ringtonePreference.getShowSilent(); - final Uri defaultUri; - - if (showDefault) { - defaultUri = RingtoneManager.getDefaultUri(ringtoneType); - } else { - defaultUri = null; - } - - builder - .setSingleChoiceItems(data, selectedIndex, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i < data.length) { - selectedIndex = i; - - int realIdx = i - (showDefault ? 1 : 0) - (showSilent ? 1 : 0); - - stopPlaying(); - - if (showDefault && i == 0) { - if (defaultRingtone != null) { - defaultRingtone.play(); - } else { - defaultRingtone = RingtoneManager.getRingtone(context, defaultUri); - if (defaultRingtone != null) { - defaultRingtone.play(); - } - } - } else if (((showDefault && i == 1) || (!showDefault && i == 0)) && showSilent) { - ringtoneManager.stopPreviousRingtone(); // "playing" silence - } else { - Ringtone ringtone = ringtoneManager.getRingtone(realIdx); - ringtone.play(); - } - } else { - newRingtone(); - } - } - }) - .setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialogInterface) { - if (defaultRingtone != null) { - defaultRingtone.stop(); - } - - RingtonePreferenceDialogFragmentCompat.this.onDismiss(dialogInterface); - } - }) - .setNegativeButton(android.R.string.cancel, this) - .setPositiveButton(android.R.string.ok, this); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog dialog = (AlertDialog) super.onCreateDialog(savedInstanceState); - - if (getRingtonePreference().shouldShowAdd()) { - ListView listView = dialog.getListView(); - View addRingtoneView = LayoutInflater.from(listView.getContext()).inflate(R.layout.add_ringtone_item, listView, false); - listView.addFooterView(addRingtoneView); - } - - return dialog; - } - - @Override - public void onDialogClosed(boolean positiveResult) { - stopPlaying(); - - defaultRingtone = null; - - final RingtonePreference preference = getRingtonePreference(); - final boolean showDefault = preference.getShowDefault(); - final boolean showSilent = preference.getShowSilent(); - - if (positiveResult && selectedIndex >= 0) { - final Uri uri; - - if (showDefault && selectedIndex == 0) { - uri = RingtoneManager.getDefaultUri(preference.getRingtoneType()); - } else if (((showDefault && selectedIndex == 1) || (!showDefault && selectedIndex == 0)) && showSilent) { - uri = null; - } else { - uri = ringtoneManager.getRingtoneUri(selectedIndex - (showDefault ? 1 : 0) - (showSilent ? 1 : 0)); - } - - if (preference.callChangeListener(uri)) { - preference.setRingtone(uri); - } - } - } - - @NonNull - private String[] createRingtoneList(Uri ringtoneUri) { - RingtonePreference ringtonePreference = getRingtonePreference(); - List results = new LinkedList<>(); - - ringtoneManager = new RingtoneManager(getContext()); - ringtoneManager.setType(ringtonePreference.getRingtoneType()); - ringtoneManager.setStopPreviousRingtone(true); - - Cursor ringtoneCursor = ringtoneManager.getCursor(); - - String colId = ringtoneCursor.getColumnName(RingtoneManager.ID_COLUMN_INDEX); - String colTitle = ringtoneCursor.getColumnName(RingtoneManager.TITLE_COLUMN_INDEX); - - MatrixCursor extras = new MatrixCursor(new String[]{colId, colTitle}); - - final int ringtoneType = ringtonePreference.getRingtoneType(); - final boolean showDefault = ringtonePreference.getShowDefault(); - final boolean showSilent = ringtonePreference.getShowSilent(); - - if (showDefault) { - switch (ringtoneType) { - case RingtoneManager.TYPE_ALARM: - extras.addRow(new String[]{CURSOR_DEFAULT_ID, getString(R.string.RingtonePreference_alarm_sound_default)}); - break; - case RingtoneManager.TYPE_NOTIFICATION: - extras.addRow(new String[]{CURSOR_DEFAULT_ID, getString(R.string.RingtonePreference_notification_sound_default)}); - break; - case RingtoneManager.TYPE_RINGTONE: - case RingtoneManager.TYPE_ALL: - default: - extras.addRow(new String[]{CURSOR_DEFAULT_ID, getString(R.string.RingtonePreference_ringtone_default)}); - break; - } - } - - if (showSilent) { - extras.addRow(new String[]{CURSOR_NONE_ID, getString(R.string.RingtonePreference_ringtone_silent)}); - } - - selectedIndex = ringtoneManager.getRingtonePosition(ringtoneUri); - if (selectedIndex >= 0) { - selectedIndex += (showDefault ? 1 : 0) + (showSilent ? 1 : 0); - } - - if (selectedIndex < 0 && showDefault) { - if (RingtoneManager.getDefaultType(ringtoneUri) != -1) { - selectedIndex = 0; - } - } - - if (selectedIndex < 0 && showSilent) { - selectedIndex = showDefault ? 1 : 0; - } - - Cursor[] cursors = {extras, ringtoneCursor}; - Cursor cursor = new MergeCursor(cursors); - - while (cursor != null && cursor.moveToNext()) { - results.add(cursor.getString(1)); - } - - return data = results.toArray(new String[0]); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == getRingtonePreference().getCustomRingtoneRequestCode()) { - if (resultCode == RESULT_OK) { - final Uri fileUri = data.getData(); - final Context context = getContext(); - - final RingtonePreference ringtonePreference = getRingtonePreference(); - final int ringtoneType = ringtonePreference.getRingtoneType(); - - // FIXME static field leak - @SuppressLint("StaticFieldLeak") final AsyncTask installTask = new AsyncTask() { - @Override - protected String[] doInBackground(Uri... params) { - try { - Uri newUri = addCustomExternalRingtone(context, params[0], ringtoneType); - - return createRingtoneList(newUri); - } catch (IOException | IllegalArgumentException e) { - Log.e(TAG, "Unable to add new ringtone: ", e); - } - return null; - } - - @Override - protected void onPostExecute(final String[] newData) { - if (newData != null) { - final ListView listView = ((AlertDialog) getDialog()).getListView(); - ArrayAdapter adapter = (ArrayAdapter)((HeaderViewListAdapter)listView.getAdapter()).getWrappedAdapter(); - - adapter.clear(); - adapter.addAll(newData); - - listView.setItemChecked(selectedIndex, true); - listView.setSelection(selectedIndex); - listView.clearFocus(); - } else { - Toast.makeText(context, getString(R.string.RingtonePreference_unable_to_add_ringtone), Toast.LENGTH_SHORT).show(); - } - } - }; - installTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, fileUri); - } else { - ListView listView = ((AlertDialog) getDialog()).getListView(); - listView.setItemChecked(selectedIndex, true); - } - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == getRingtonePreference().getPermissionRequestCode() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - newRingtone(); - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - private void newRingtone() { - boolean hasPerm = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - if (hasPerm) { - final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); - chooseFile.setType("audio/*"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - chooseFile.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"audio/*", "application/ogg"}); - } - startActivityForResult(chooseFile, getRingtonePreference().getCustomRingtoneRequestCode()); - } else { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, getRingtonePreference().getPermissionRequestCode()); - } - } - - @WorkerThread - public static Uri addCustomExternalRingtone(Context context, @NonNull Uri fileUri, final int type) - throws IOException { - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - throw new IOException("External storage is not mounted. Unable to install ringtones."); - } - - if (ContentResolver.SCHEME_FILE.equals(fileUri.getScheme())) { - fileUri = Uri.fromFile(new File(fileUri.getPath())); - } - - String mimeType = context.getContentResolver().getType(fileUri); - - if (mimeType == null) { - String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileUri - .toString()); - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - fileExtension.toLowerCase()); - } - - if (mimeType == null || !(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) { - throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"." - + " Given file has MIME type \"" + mimeType + "\""); - } - - final String subdirectory = getDirForType(type); - - final File outFile = getUniqueExternalFile(context, subdirectory, getFileDisplayNameFromUri(context, fileUri), mimeType); - - if (outFile != null) { - final InputStream input = context.getContentResolver().openInputStream(fileUri); - final OutputStream output = new FileOutputStream(outFile); - - if (input != null) { - byte[] buffer = new byte[10240]; - - for (int len; (len = input.read(buffer)) != -1; ) { - output.write(buffer, 0, len); - } - - input.close(); - } - - output.close(); - - NewRingtoneScanner scanner = null; - try { - scanner = new NewRingtoneScanner(context, outFile); - return scanner.take(); - } catch (InterruptedException e) { - throw new IOException("Audio file failed to scan as a ringtone", e); - } finally { - if (scanner != null) { - scanner.close(); - } - } - } else { - return null; - } - } - - private static String getDirForType(int type) { - switch (type) { - case RingtoneManager.TYPE_ALL: - case RingtoneManager.TYPE_RINGTONE: - return Environment.DIRECTORY_RINGTONES; - case RingtoneManager.TYPE_NOTIFICATION: - return Environment.DIRECTORY_NOTIFICATIONS; - case RingtoneManager.TYPE_ALARM: - return Environment.DIRECTORY_ALARMS; - default: - throw new IllegalArgumentException("Unsupported ringtone type: " + type); - } - } - - private static String getFileDisplayNameFromUri(Context context, Uri uri) { - String scheme = uri.getScheme(); - - if (ContentResolver.SCHEME_FILE.equals(scheme)) { - return uri.getLastPathSegment(); - } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { - - String[] projection = {OpenableColumns.DISPLAY_NAME}; - - Cursor cursor = null; - try { - cursor = context.getContentResolver().query(uri, projection, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - // This will only happen if the Uri isn't either SCHEME_CONTENT or SCHEME_FILE, so we assume - // it already represents the file's name. - return uri.toString(); - } - - /** - * Creates a unique file in the specified external storage with the desired name. If the name is - * taken, the new file's name will have '(%d)' to avoid overwriting files. - * - * @param context {@link Context} to query the file name from. - * @param subdirectory One of the directories specified in {@link android.os.Environment} - * @param fileName desired name for the file. - * @param mimeType MIME type of the file to create. - * @return the File object in the storage, or null if an error occurs. - */ - @Nullable - private static File getUniqueExternalFile(Context context, String subdirectory, String fileName, - String mimeType) { - File externalStorage = Environment.getExternalStoragePublicDirectory(subdirectory); - // Make sure the storage subdirectory exists - //noinspection ResultOfMethodCallIgnored - externalStorage.mkdirs(); - - File outFile; - try { - // Ensure the file has a unique name, as to not override any existing file - outFile = buildUniqueFile(externalStorage, mimeType, fileName); - } catch (FileNotFoundException e) { - // This might also be reached if the number of repeated files gets too high - Log.e(TAG, "Unable to get a unique file name: " + e); - return null; - } - return outFile; - } - - @NonNull - private static File buildUniqueFile(File externalStorage, String mimeType, String fileName) throws FileNotFoundException { - final String[] parts = splitFileName(mimeType, fileName); - - String name = parts[0]; - String ext = (parts[1] != null) ? "." + parts[1] : ""; - - File file = new File(externalStorage, name + ext); - SecureRandom random = new SecureRandom(); - - int n = 0; - while (file.exists()) { - if (n++ >= 32) { - n = random.nextInt(); - } - file = new File(externalStorage, name + " (" + n + ")" + ext); - } - - return file; - } - - @NonNull - public static String[] splitFileName(String mimeType, String displayName) { - String name; - String ext; - - String mimeTypeFromExt; - - // Extract requested extension from display name - final int lastDot = displayName.lastIndexOf('.'); - if (lastDot >= 0) { - name = displayName.substring(0, lastDot); - ext = displayName.substring(lastDot + 1); - mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - ext.toLowerCase()); - } else { - name = displayName; - ext = null; - mimeTypeFromExt = null; - } - - if (mimeTypeFromExt == null) { - mimeTypeFromExt = "application/octet-stream"; - } - - final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( - mimeType); - //noinspection StatementWithEmptyBody - if (TextUtils.equals(mimeType, mimeTypeFromExt) || TextUtils.equals(ext, extFromMimeType)) { - // Extension maps back to requested MIME type; allow it - } else { - // No match; insist that create file matches requested MIME - name = displayName; - ext = extFromMimeType; - } - - - if (ext == null) { - ext = ""; - } - - return new String[]{name, ext}; - } - - /** - * Creates a {@link android.media.MediaScannerConnection} to scan a ringtone file and add its - * information to the internal database. - *

- * It uses a {@link java.util.concurrent.LinkedBlockingQueue} so that the caller can block until - * the scan is completed. - */ - private static class NewRingtoneScanner implements Closeable, MediaScannerConnection.MediaScannerConnectionClient { - private MediaScannerConnection mMediaScannerConnection; - private File mFile; - private LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(1); - - private NewRingtoneScanner(Context context, File file) { - mFile = file; - mMediaScannerConnection = new MediaScannerConnection(context, this); - mMediaScannerConnection.connect(); - } - - @Override - public void close() { - mMediaScannerConnection.disconnect(); - } - - @Override - public void onMediaScannerConnected() { - mMediaScannerConnection.scanFile(mFile.getAbsolutePath(), null); - } - - @Override - public void onScanCompleted(String path, Uri uri) { - if (uri == null) { - // There was some issue with scanning. Delete the copied file so it is not oprhaned. - //noinspection ResultOfMethodCallIgnored - mFile.delete(); - return; - } - try { - mQueue.put(uri); - } catch (InterruptedException e) { - Log.e(TAG, "Unable to put new ringtone Uri in queue", e); - } - } - - private Uri take() throws InterruptedException { - return mQueue.take(); - } - } -} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/SignalRingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/widgets/SignalPreference.java similarity index 61% rename from src/org/thoughtcrime/securesms/preferences/widgets/SignalRingtonePreference.java rename to src/org/thoughtcrime/securesms/preferences/widgets/SignalPreference.java index a1096dc23d..d94ab8ac2e 100644 --- a/src/org/thoughtcrime/securesms/preferences/widgets/SignalRingtonePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/widgets/SignalPreference.java @@ -2,36 +2,34 @@ package org.thoughtcrime.securesms.preferences.widgets; import android.content.Context; -import android.os.Build; -import android.support.annotation.RequiresApi; +import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.widget.TextView; import org.thoughtcrime.securesms.R; -public class SignalRingtonePreference extends AdvancedRingtonePreference { +public class SignalPreference extends Preference { private TextView rightSummary; private CharSequence summary; - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public SignalRingtonePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public SignalPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initialize(); } - public SignalRingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { + public SignalPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(); } - public SignalRingtonePreference(Context context, AttributeSet attrs) { + public SignalPreference(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } - public SignalRingtonePreference(Context context) { + public SignalPreference(Context context) { super(context); initialize(); } @@ -45,17 +43,17 @@ public class SignalRingtonePreference extends AdvancedRingtonePreference { super.onBindViewHolder(view); this.rightSummary = (TextView)view.findViewById(R.id.right_summary); - setSummary(summary); + setSummary(this.summary); } @Override public void setSummary(CharSequence summary) { - this.summary = summary; - super.setSummary(null); - if (rightSummary != null) { - rightSummary.setText(summary); + this.summary = summary; + + if (this.rightSummary != null) { + this.rightSummary.setText(summary); } } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 389e540d06..831d0db040 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -635,7 +635,17 @@ public class TextSecurePreferences { } public static String getNotificationRingtone(Context context) { - return getStringPreference(context, RINGTONE_PREF, Settings.System.DEFAULT_NOTIFICATION_URI.toString()); + String result = getStringPreference(context, RINGTONE_PREF, Settings.System.DEFAULT_NOTIFICATION_URI.toString()); + + if (result != null && result.startsWith("file:")) { + result = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); + } + + return result; + } + + public static void setNotificationRingtone(Context context, String ringtone) { + setStringPreference(context, RINGTONE_PREF, ringtone); } public static boolean isNotificationVibrateEnabled(Context context) {