diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 796b4ef67d..0d746d0fcf 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="org.thoughtcrime.securesms"> - + + + \ No newline at end of file diff --git a/res/drawable/ic_add.xml b/res/drawable/ic_add.xml new file mode 100644 index 0000000000..40d57023eb --- /dev/null +++ b/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/res/layout/add_ringtone_item.xml b/res/layout/add_ringtone_item.xml new file mode 100644 index 0000000000..836123eebd --- /dev/null +++ b/res/layout/add_ringtone_item.xml @@ -0,0 +1,45 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/preference_widget_color_swatch.xml b/res/layout/preference_widget_color_swatch.xml new file mode 100644 index 0000000000..0ac82c7fe2 --- /dev/null +++ b/res/layout/preference_widget_color_swatch.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index a10f39b352..4f1695359a 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -133,14 +133,12 @@ @string/preferences__fast @string/preferences__normal @string/preferences__slow - @string/preferences__custom 300,300 500,2000 3000,3000 - custom diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 619dc7a1b9..1374635526 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -199,4 +199,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 85d85cad5a..f87b6caef3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1460,6 +1460,14 @@ Read receipts If you read receipts are disabled, you won\'t be able to see read receipts from others. + Default ringtone + None + Ringtones + Default notification sound + Default alarm sound + Add ringtone + Unable to add custom ringtone + diff --git a/res/values/styles.xml b/res/values/styles.xml index e533566c55..7f0f82c060 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -256,4 +256,7 @@ false + + diff --git a/res/values/themes.xml b/res/values/themes.xml index a3888ca472..c90d95b9c9 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -18,6 +18,8 @@ #a0000000 @color/textsecure_primary_dark @color/background_material_light + @drawable/preference_divider_light + @style/PreferenceThemeOverlay.Fix diff --git a/res/xml/preferences_notifications.xml b/res/xml/preferences_notifications.xml index 62a9562518..273070373b 100644 --- a/res/xml/preferences_notifications.xml +++ b/res/xml/preferences_notifications.xml @@ -30,7 +30,7 @@ android:entries="@array/pref_led_color_entries" android:entryValues="@array/pref_led_color_values" /> - parent, View view, int position, long id) { - defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE); - customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE); - positiveButton.setEnabled(position == 0 || validator.isValid(customText.getText().toString())); + public static class CustomDefaultPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat { + + private static final String INPUT_TYPE = "input_type"; + + private Spinner spinner; + private EditText customText; + private TextView defaultLabel; + + public static CustomDefaultPreferenceDialogFragmentCompat newInstance(String key) { + CustomDefaultPreferenceDialogFragmentCompat fragment = new CustomDefaultPreferenceDialogFragmentCompat(); + Bundle b = new Bundle(1); + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key); + fragment.setArguments(b); + return fragment; } @Override - public void onNothingSelected(AdapterView parent) { - defaultLabel.setVisibility(View.VISIBLE); - customText.setVisibility(View.GONE); + protected void onBindDialogView(@NonNull View view) { + Log.w(TAG, "onBindDialogView"); + super.onBindDialogView(view); + + CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); + + this.spinner = (Spinner) view.findViewById(R.id.default_or_custom); + this.defaultLabel = (TextView) view.findViewById(R.id.default_label); + this.customText = (EditText) view.findViewById(R.id.custom_edit); + + this.customText.setInputType(preference.inputType); + this.customText.addTextChangedListener(new TextValidator()); + this.customText.setText(preference.getCustomValue()); + this.spinner.setOnItemSelectedListener(new SelectionLister()); + this.defaultLabel.setText(preference.getPrettyPrintValue(preference.defaultValue)); } - } - private class TextValidator implements TextWatcher { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + public Dialog onCreateDialog(Bundle instanceState) { + Dialog dialog = super.onCreateDialog(instanceState); + + CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); + + if (preference.isCustom()) spinner.setSelection(1, true); + else spinner.setSelection(0, true); + + return dialog; + } @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} + public void onDialogClosed(boolean positiveResult) { + CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); - @Override - public void afterTextChanged(Editable s) { - if (spinner.getSelectedItemPosition() == 1) { - positiveButton.setEnabled(validator.isValid(s.toString())); + if (positiveResult) { + if (spinner != null) preference.setCustom(spinner.getSelectedItemPosition() == 1); + if (customText != null) preference.setCustomValue(customText.getText().toString()); + + preference.setSummary(preference.getSummary()); } } - } - protected interface CustomPreferenceValidator { - public boolean isValid(String value); - } - - private static class NullValidator implements CustomPreferenceValidator { - @Override - public boolean isValid(String value) { - return true; + interface CustomPreferenceValidator { + public boolean isValid(String value); } - } - public static class UriValidator implements CustomPreferenceValidator { - @Override - public boolean isValid(String value) { - if (TextUtils.isEmpty(value)) return true; - - try { - new URI(value); + private static class NullValidator implements CustomPreferenceValidator { + @Override + public boolean isValid(String value) { return true; - } catch (URISyntaxException mue) { - return false; } } - } - public static class HostnameValidator implements CustomPreferenceValidator { - @Override - public boolean isValid(String value) { - if (TextUtils.isEmpty(value)) return true; + private class TextValidator implements TextWatcher { - try { - URI uri = new URI(null, value, null, null); - return true; - } catch (URISyntaxException mue) { - return false; + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); + + if (spinner.getSelectedItemPosition() == 1) { + Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE); + positiveButton.setEnabled(preference.validator.isValid(s.toString())); + } } } - } - public static class PortValidator implements CustomPreferenceValidator { - @Override - public boolean isValid(String value) { - try { - Integer.parseInt(value); - return true; - } catch (NumberFormatException e) { - return false; + public static class UriValidator implements CustomPreferenceValidator { + @Override + public boolean isValid(String value) { + if (TextUtils.isEmpty(value)) return true; + + try { + new URI(value); + return true; + } catch (URISyntaxException mue) { + return false; + } } } + + public static class HostnameValidator implements CustomPreferenceValidator { + @Override + public boolean isValid(String value) { + if (TextUtils.isEmpty(value)) return true; + + try { + URI uri = new URI(null, value, null, null); + return true; + } catch (URISyntaxException mue) { + return false; + } + } + } + + public static class PortValidator implements CustomPreferenceValidator { + @Override + public boolean isValid(String value) { + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } + } + + private class SelectionLister implements AdapterView.OnItemSelectedListener { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + CustomDefaultPreference preference = (CustomDefaultPreference)getPreference(); + Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE); + + defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE); + customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE); + positiveButton.setEnabled(position == 0 || preference.validator.isValid(customText.getText().toString())); + } + + @Override + public void onNothingSelected(AdapterView parent) { + defaultLabel.setVisibility(View.VISIBLE); + customText.setVisibility(View.GONE); + } + } + } diff --git a/src/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java b/src/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java index 26da2811b7..2e7c4539d7 100644 --- a/src/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java +++ b/src/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; -import android.preference.CheckBoxPreference; +import android.support.v7.preference.CheckBoxPreference; import android.util.AttributeSet; import org.thoughtcrime.securesms.R; diff --git a/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java index 60c26900f2..ee6ae9a7a0 100644 --- a/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java @@ -7,13 +7,12 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.preference.PreferenceFragment; import android.support.v7.app.AlertDialog; +import android.support.v7.preference.CheckBoxPreference; +import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; @@ -35,7 +34,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE import java.io.IOException; -public class AdvancedPreferenceFragment extends PreferenceFragment { +public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment { private static final String TAG = AdvancedPreferenceFragment.class.getSimpleName(); private static final String PUSH_MESSAGING_PREF = "pref_toggle_push_messaging"; @@ -49,7 +48,6 @@ public class AdvancedPreferenceFragment extends PreferenceFragment { public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); masterSecret = getArguments().getParcelable("master_secret"); - addPreferencesFromResource(R.xml.preferences_advanced); initializeIdentitySelection(); @@ -58,6 +56,11 @@ public class AdvancedPreferenceFragment extends PreferenceFragment { submitDebugLog.setSummary(getVersion(getActivity())); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_advanced); + } + @Override public void onResume() { super.onResume(); diff --git a/src/org/thoughtcrime/securesms/preferences/AdvancedRingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/AdvancedRingtonePreference.java index 56072f751e..f83b5cabc4 100644 --- a/src/org/thoughtcrime/securesms/preferences/AdvancedRingtonePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/AdvancedRingtonePreference.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.net.Uri; import android.os.Build; -import android.preference.RingtonePreference; import android.support.annotation.RequiresApi; import android.util.AttributeSet; @@ -36,4 +35,6 @@ public class AdvancedRingtonePreference extends RingtonePreference { public void setCurrentRingtone(Uri uri) { currentRingtone = uri; } + + } diff --git a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 4df78f1405..b9a694f7b3 100644 --- a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -6,11 +6,11 @@ import android.content.Intent; import android.content.res.TypedArray; import android.os.Build; import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.PreferenceScreen; -import android.support.v4.preference.PreferenceFragment; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; +import android.support.v7.preference.CheckBoxPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import android.widget.Toast; import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder; @@ -37,7 +37,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_app_protection); masterSecret = getArguments().getParcelable("master_secret"); disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary"); @@ -52,6 +51,11 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment .setOnPreferenceChangeListener(new DisablePassphraseClickListener()); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_app_protection); + } + @Override public void onResume() { super.onResume(); @@ -65,7 +69,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private void initializePlatformSpecificOptions() { PreferenceScreen preferenceScreen = getPreferenceScreen(); - Preference screenSecurityPreference = findPreference(TextSecurePreferences.SCREEN_SECURITY_PREF); + Preference screenSecurityPreference = findPreference(TextSecurePreferences.SCREEN_SECURITY_PREF); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH && screenSecurityPreference != null) { diff --git a/src/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java index df77d18d02..4da08ec32d 100644 --- a/src/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java @@ -2,7 +2,8 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.os.Bundle; -import android.preference.ListPreference; +import android.support.annotation.Nullable; +import android.support.v7.preference.ListPreference; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; @@ -15,7 +16,6 @@ public class AppearancePreferenceFragment extends ListSummaryPreferenceFragment @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_appearance); this.findPreference(TextSecurePreferences.THEME_PREF).setOnPreferenceChangeListener(new ListSummaryListener()); this.findPreference(TextSecurePreferences.LANGUAGE_PREF).setOnPreferenceChangeListener(new ListSummaryListener()); @@ -23,6 +23,11 @@ public class AppearancePreferenceFragment extends ListSummaryPreferenceFragment initializeListSummary((ListPreference)findPreference(TextSecurePreferences.LANGUAGE_PREF)); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_appearance); + } + @Override public void onStart() { super.onStart(); diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index a8bf74b14d..3e890af872 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -3,15 +3,13 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.support.v4.preference.PreferenceFragment; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; +import android.support.v7.preference.EditTextPreference; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; import android.text.TextUtils; import android.util.Log; -import android.view.View; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; @@ -28,7 +26,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_chats); findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_MOBILE_PREF) .setOnPreferenceChangeListener(new MediaDownloadChangeListener()); @@ -47,6 +44,11 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF)); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_chats); + } + @Override public void onResume() { super.onResume(); @@ -99,7 +101,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { } } - private class MediaDownloadChangeListener implements OnPreferenceChangeListener { + private class MediaDownloadChangeListener implements Preference.OnPreferenceChangeListener { @SuppressWarnings("unchecked") @Override public boolean onPreferenceChange(Preference preference, Object newValue) { Log.w(TAG, "onPreferenceChange"); diff --git a/src/org/thoughtcrime/securesms/preferences/ColorPickerPreference.java b/src/org/thoughtcrime/securesms/preferences/ColorPickerPreference.java new file mode 100644 index 0000000000..23f101fd65 --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/ColorPickerPreference.java @@ -0,0 +1,252 @@ +package org.thoughtcrime.securesms.preferences; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.res.TypedArrayUtils; +import android.support.v7.preference.DialogPreference; +import android.support.v7.preference.PreferenceViewHolder; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; + +import com.takisoft.colorpicker.ColorPickerDialog; +import com.takisoft.colorpicker.ColorPickerDialog.Size; +import com.takisoft.colorpicker.ColorStateDrawable; + +import org.thoughtcrime.securesms.R; + +public class ColorPickerPreference extends DialogPreference { + + private static final String TAG = ColorPickerPreference.class.getSimpleName(); + + private int[] colors; + private CharSequence[] colorDescriptions; + private int color; + private int columns; + private int size; + private boolean sortColors; + + private ImageView colorWidget; + private OnPreferenceChangeListener listener; + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference, defStyleAttr, 0); + + int colorsId = a.getResourceId(R.styleable.ColorPickerPreference_colors, R.array.color_picker_default_colors); + + if (colorsId != 0) { + colors = context.getResources().getIntArray(colorsId); + } + + colorDescriptions = a.getTextArray(R.styleable.ColorPickerPreference_colorDescriptions); + color = a.getColor(R.styleable.ColorPickerPreference_currentColor, 0); + columns = a.getInt(R.styleable.ColorPickerPreference_columns, 4); + size = a.getInt(R.styleable.ColorPickerPreference_colorSize, 2); + sortColors = a.getBoolean(R.styleable.ColorPickerPreference_sortColors, false); + + a.recycle(); + + setWidgetLayoutResource(R.layout.preference_widget_color_swatch); + } + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressLint("RestrictedApi") + public ColorPickerPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle, + android.R.attr.dialogPreferenceStyle)); + } + + public ColorPickerPreference(Context context) { + this(context, null); + } + + @Override + public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) { + super.setOnPreferenceChangeListener(listener); + this.listener = listener; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + colorWidget = (ImageView) holder.findViewById(R.id.color_picker_widget); + setColorOnWidget(color); + } + + private void setColorOnWidget(int color) { + if (colorWidget == null) { + return; + } + + Drawable[] colorDrawable = new Drawable[] + {ContextCompat.getDrawable(getContext(), R.drawable.colorpickerpreference_pref_swatch)}; + colorWidget.setImageDrawable(new ColorStateDrawable(colorDrawable, color)); + } + + /** + * Returns the current color. + * + * @return The current color. + */ + public int getColor() { + return color; + } + + /** + * Sets the current color. + * + * @param color The current color. + */ + public void setColor(int color) { + setInternalColor(color, false); + } + + /** + * Returns all of the available colors. + * + * @return The available colors. + */ + public int[] getColors() { + return colors; + } + + /** + * Sets the available colors. + * + * @param colors The available colors. + */ + public void setColors(int[] colors) { + this.colors = colors; + } + + /** + * Returns whether the available colors should be sorted automatically based on their HSV + * values. + * + * @return Whether the available colors should be sorted automatically based on their HSV + * values. + */ + public boolean isSortColors() { + return sortColors; + } + + /** + * Sets whether the available colors should be sorted automatically based on their HSV + * values. The sorting does not modify the order of the original colors supplied via + * {@link #setColors(int[])} or the XML attribute {@code app:colors}. + * + * @param sortColors Whether the available colors should be sorted automatically based on their + * HSV values. + */ + public void setSortColors(boolean sortColors) { + this.sortColors = sortColors; + } + + /** + * Returns the available colors' descriptions that can be used by accessibility services. + * + * @return The available colors' descriptions. + */ + public CharSequence[] getColorDescriptions() { + return colorDescriptions; + } + + /** + * Sets the available colors' descriptions that can be used by accessibility services. + * + * @param colorDescriptions The available colors' descriptions. + */ + public void setColorDescriptions(CharSequence[] colorDescriptions) { + this.colorDescriptions = colorDescriptions; + } + + /** + * Returns the number of columns to be used in the picker dialog for displaying the available + * colors. If the value is less than or equals to 0, the number of columns will be determined + * automatically by the system using FlexboxLayoutManager. + * + * @return The number of columns to be used in the picker dialog. + * @see com.google.android.flexbox.FlexboxLayoutManager + */ + public int getColumns() { + return columns; + } + + /** + * Sets the number of columns to be used in the picker dialog for displaying the available + * colors. If the value is less than or equals to 0, the number of columns will be determined + * automatically by the system using FlexboxLayoutManager. + * + * @param columns The number of columns to be used in the picker dialog. Use 0 to set it to + * 'auto' mode. + * @see com.google.android.flexbox.FlexboxLayoutManager + */ + public void setColumns(int columns) { + this.columns = columns; + } + + /** + * Returns the size of the color swatches in the dialog. It can be either + * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. + * + * @return The size of the color swatches in the dialog. + * @see ColorPickerDialog#SIZE_SMALL + * @see ColorPickerDialog#SIZE_LARGE + */ + @Size + public int getSize() { + return size; + } + + /** + * Sets the size of the color swatches in the dialog. It can be either + * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. + * + * @param size The size of the color swatches in the dialog. It can be either + * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}. + * @see ColorPickerDialog#SIZE_SMALL + * @see ColorPickerDialog#SIZE_LARGE + */ + public void setSize(@Size int size) { + this.size = size; + } + + private void setInternalColor(int color, boolean force) { + int oldColor = getPersistedInt(0); + + boolean changed = oldColor != color; + + if (changed || force) { + this.color = color; + + persistInt(color); + + setColorOnWidget(color); + + if (listener != null) listener.onPreferenceChange(this, color); + notifyChanged(); + } + } + + @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; + setInternalColor(restoreValue ? getPersistedInt(0) : (!TextUtils.isEmpty(defaultValue) ? Color.parseColor(defaultValue) : 0), true); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/preferences/ColorPickerPreferenceDialogFragmentCompat.java b/src/org/thoughtcrime/securesms/preferences/ColorPickerPreferenceDialogFragmentCompat.java new file mode 100644 index 0000000000..1ccb9fbc38 --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/ColorPickerPreferenceDialogFragmentCompat.java @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.preferences; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.preference.PreferenceDialogFragmentCompat; + +import com.takisoft.colorpicker.ColorPickerDialog; +import com.takisoft.colorpicker.OnColorSelectedListener; + +public class ColorPickerPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements OnColorSelectedListener { + + private int pickedColor; + + public static ColorPickerPreferenceDialogFragmentCompat newInstance(String key) { + ColorPickerPreferenceDialogFragmentCompat fragment = new ColorPickerPreferenceDialogFragmentCompat(); + Bundle b = new Bundle(1); + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + ColorPickerPreference pref = getColorPickerPreference(); + + ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(getContext()) + .setSelectedColor(pref.getColor()) + .setColors(pref.getColors()) + .setColorContentDescriptions(pref.getColorDescriptions()) + .setSize(pref.getSize()) + .setSortColors(pref.isSortColors()) + .setColumns(pref.getColumns()) + .build(); + + ColorPickerDialog dialog = new ColorPickerDialog(getActivity(), this, params); + dialog.setTitle(pref.getDialogTitle()); + + return dialog; + } + + @Override + public void onDialogClosed(boolean positiveResult) { + ColorPickerPreference preference = getColorPickerPreference(); + + if (positiveResult) { + preference.setColor(pickedColor); + } + } + + @Override + public void onColorSelected(int color) { + this.pickedColor = color; + + super.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); + } + + ColorPickerPreference getColorPickerPreference() { + return (ColorPickerPreference) getPreference(); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/preferences/ColorPreference.java b/src/org/thoughtcrime/securesms/preferences/ColorPreference.java deleted file mode 100644 index 219908125f..0000000000 --- a/src/org/thoughtcrime/securesms/preferences/ColorPreference.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.thoughtcrime.securesms.preferences; - -/* - * Copyright 2013 Google Inc. - * - * 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. - */ - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Bundle; -import android.preference.Preference; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.GridLayout; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import org.thoughtcrime.securesms.R; - -/** - * A preference that allows the user to choose an application or shortcut. - */ -public class ColorPreference extends Preference { - private int[] mColorChoices = {}; - private int mValue = 0; - private int mItemLayoutId = R.layout.color_preference_item; - private int mNumColumns = 5; - private View mPreviewView; - - public ColorPreference(Context context) { - super(context); - initAttrs(null, 0); - } - - public ColorPreference(Context context, AttributeSet attrs) { - super(context, attrs); - initAttrs(attrs, 0); - } - - public ColorPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initAttrs(attrs, defStyle); - } - - private void initAttrs(AttributeSet attrs, int defStyle) { - TypedArray a = getContext().getTheme().obtainStyledAttributes( - attrs, R.styleable.ColorPreference, defStyle, defStyle); - - try { - mItemLayoutId = a.getResourceId(R.styleable.ColorPreference_itemLayout, mItemLayoutId); - mNumColumns = a.getInteger(R.styleable.ColorPreference_numColumns, mNumColumns); -// int choicesResId = a.getResourceId(R.styleable.ColorPreference_choices, -// R.array.default_color_choice_values); -// if (choicesResId > 0) { -// String[] choices = a.getResources().getStringArray(choicesResId); -// mColorChoices = new int[choices.length]; -// for (int i = 0; i < choices.length; i++) { -// mColorChoices[i] = Color.parseColor(choices[i]); -// } -// } - - } finally { - a.recycle(); - } - - setWidgetLayoutResource(mItemLayoutId); - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - mPreviewView = view.findViewById(R.id.color_view); - setColorViewValue(mPreviewView, mValue, false); - } - - public void setValue(int value) { - if (callChangeListener(value)) { - mValue = value; - persistInt(value); - notifyChanged(); - } - } - - public void setChoices(int[] values) { - mColorChoices = values; - } - - @Override - protected void onClick() { - super.onClick(); - - ColorDialogFragment fragment = ColorDialogFragment.newInstance(); - fragment.setPreference(this); - - ((AppCompatActivity) getContext()).getSupportFragmentManager().beginTransaction() - .add(fragment, getFragmentTag()) - .commit(); - } - - @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); - - AppCompatActivity activity = (AppCompatActivity) getContext(); - ColorDialogFragment fragment = (ColorDialogFragment) activity - .getSupportFragmentManager().findFragmentByTag(getFragmentTag()); - if (fragment != null) { - // re-bind preference to fragment - fragment.setPreference(this); - } - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getInt(index, 0); - } - - @Override - protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { - setValue(restoreValue ? getPersistedInt(0) : (Integer) defaultValue); - } - - public String getFragmentTag() { - return "color_" + getKey(); - } - - public int getValue() { - return mValue; - } - - public static class ColorDialogFragment extends android.support.v4.app.DialogFragment { - private ColorPreference mPreference; - private GridLayout mColorGrid; - - public ColorDialogFragment() { - } - - public static ColorDialogFragment newInstance() { - return new ColorDialogFragment(); - } - - public void setPreference(ColorPreference preference) { - mPreference = preference; - repopulateItems(); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - repopulateItems(); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); - View rootView = layoutInflater.inflate(R.layout.color_preference_items, null); - - mColorGrid = (GridLayout) rootView.findViewById(R.id.color_grid); - mColorGrid.setColumnCount(mPreference.mNumColumns); - repopulateItems(); - - return new AlertDialog.Builder(getActivity()) - .setView(rootView) - .create(); - } - - private void repopulateItems() { - if (mPreference == null || mColorGrid == null) { - return; - } - - Context context = mColorGrid.getContext(); - mColorGrid.removeAllViews(); - for (final int color : mPreference.mColorChoices) { - View itemView = LayoutInflater.from(context) - .inflate(R.layout.color_preference_item, mColorGrid, false); - - setColorViewValue(itemView.findViewById(R.id.color_view), color, - color == mPreference.getValue()); - itemView.setClickable(true); - itemView.setFocusable(true); - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mPreference.setValue(color); - dismiss(); - } - }); - - mColorGrid.addView(itemView); - } - - sizeDialog(); - } - - @Override - public void onStart() { - super.onStart(); - sizeDialog(); - } - - private void sizeDialog() { -// if (mPreference == null || mColorGrid == null) { -// return; -// } -// -// Dialog dialog = getDialog(); -// if (dialog == null) { -// return; -// } -// -// final Resources res = mColorGrid.getContext().getResources(); -// DisplayMetrics dm = res.getDisplayMetrics(); -// -// // Can't use Integer.MAX_VALUE here (weird issue observed otherwise on 4.2) -// mColorGrid.measure( -// View.MeasureSpec.makeMeasureSpec(dm.widthPixels, View.MeasureSpec.AT_MOST), -// View.MeasureSpec.makeMeasureSpec(dm.heightPixels, View.MeasureSpec.AT_MOST)); -// int width = mColorGrid.getMeasuredWidth(); -// int height = mColorGrid.getMeasuredHeight(); -// -// int extraPadding = res.getDimensionPixelSize(R.dimen.color_grid_extra_padding); -// -// width += extraPadding; -// height += extraPadding; -// -// dialog.getWindow().setLayout(width, height); - } - } - - private static void setColorViewValue(View view, int color, boolean selected) { - if (view instanceof ImageView) { - ImageView imageView = (ImageView) view; - Resources res = imageView.getContext().getResources(); - - Drawable currentDrawable = imageView.getDrawable(); - GradientDrawable colorChoiceDrawable; - if (currentDrawable instanceof GradientDrawable) { - // Reuse drawable - colorChoiceDrawable = (GradientDrawable) currentDrawable; - } else { - colorChoiceDrawable = new GradientDrawable(); - colorChoiceDrawable.setShape(GradientDrawable.OVAL); - } - - // Set stroke to dark version of color -// int darkenedColor = Color.rgb( -// Color.red(color) * 192 / 256, -// Color.green(color) * 192 / 256, -// Color.blue(color) * 192 / 256); - - colorChoiceDrawable.setColor(color); -// colorChoiceDrawable.setStroke((int) TypedValue.applyDimension( -// TypedValue.COMPLEX_UNIT_DIP, 2, res.getDisplayMetrics()), darkenedColor); - - Drawable drawable = colorChoiceDrawable; - if (selected) { - BitmapDrawable checkmark = (BitmapDrawable) res.getDrawable(R.drawable.check); - checkmark.setGravity(Gravity.CENTER); - drawable = new LayerDrawable(new Drawable[]{ - colorChoiceDrawable, - checkmark}); - } - - imageView.setImageDrawable(drawable); - - } else if (view instanceof TextView) { - ((TextView) view).setTextColor(color); - } - } -} diff --git a/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index 220aabb757..e25ffa60ab 100644 --- a/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -2,10 +2,19 @@ package org.thoughtcrime.securesms.preferences; import android.os.Bundle; -import android.support.v4.preference.PreferenceFragment; +import android.support.v4.app.DialogFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; import android.view.View; -public class CorrectedPreferenceFragment extends PreferenceFragment { +import org.thoughtcrime.securesms.components.CustomDefaultPreference; + +public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat { + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + } @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -15,4 +24,25 @@ public class CorrectedPreferenceFragment extends PreferenceFragment { if (lv != null) lv.setPadding(0, 0, 0, 0); } + @Override + public void onDisplayPreferenceDialog(Preference preference) { + DialogFragment dialogFragment = null; + + if (preference instanceof RingtonePreference) { + dialogFragment = RingtonePreferenceDialogFragmentCompat.newInstance(preference.getKey()); + } else if (preference instanceof ColorPickerPreference) { + dialogFragment = ColorPickerPreferenceDialogFragmentCompat.newInstance(preference.getKey()); + } else if (preference instanceof CustomDefaultPreference) { + dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey()); + } + + if (dialogFragment != null) { + dialogFragment.setTargetFragment(this, 0); + dialogFragment.show(getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG"); + } else { + super.onDisplayPreferenceDialog(preference); + } + } + + } diff --git a/src/org/thoughtcrime/securesms/preferences/LEDColorListPreference.java b/src/org/thoughtcrime/securesms/preferences/LEDColorListPreference.java index 774e8bdb8d..107875ada5 100644 --- a/src/org/thoughtcrime/securesms/preferences/LEDColorListPreference.java +++ b/src/org/thoughtcrime/securesms/preferences/LEDColorListPreference.java @@ -18,11 +18,10 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.graphics.drawable.GradientDrawable; -import android.preference.ListPreference; import android.support.annotation.NonNull; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; -import android.util.Log; -import android.view.View; import android.widget.ImageView; import org.thoughtcrime.securesms.R; @@ -69,8 +68,8 @@ public class LEDColorListPreference extends ListPreference { } @Override - protected void onBindView(View view) { - super.onBindView(view); + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); this.colorImageView = (ImageView)view.findViewById(R.id.color_view); setPreviewColor(getValue()); } diff --git a/src/org/thoughtcrime/securesms/preferences/LedBlinkPatternListPreference.java b/src/org/thoughtcrime/securesms/preferences/LedBlinkPatternListPreference.java deleted file mode 100644 index 529274aabd..0000000000 --- a/src/org/thoughtcrime/securesms/preferences/LedBlinkPatternListPreference.java +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.preferences; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Parcelable; -import android.preference.ListPreference; -import android.support.v7.app.AlertDialog; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; -import android.widget.TextView; -import android.widget.Toast; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.TextSecurePreferences; - -/** - * List preference for LED blink pattern notification. - * - * @author Moxie Marlinspike - */ - -public class LedBlinkPatternListPreference extends SignalListPreference implements OnSeekBarChangeListener { - - private Context context; - private SeekBar seekBarOn; - private SeekBar seekBarOff; - - private TextView seekBarOnLabel; - private TextView seekBarOffLabel; - - private boolean dialogInProgress; - - public LedBlinkPatternListPreference(Context context) { - super(context); - this.context = context; - } - - public LedBlinkPatternListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - this.context = context; - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (positiveResult) { - String blinkPattern = TextSecurePreferences.getNotificationLedPattern(context); - if (blinkPattern.equals("custom")) showDialog(); - } - } - - private void initializeSeekBarValues() { - String patternString = TextSecurePreferences.getNotificationLedPatternCustom(context); - String[] patternArray = patternString.split(","); - seekBarOn.setProgress(Integer.parseInt(patternArray[0])); - seekBarOff.setProgress(Integer.parseInt(patternArray[1])); - } - - private void initializeDialog(View view) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setIconAttribute(R.attr.dialog_info_icon); - builder.setTitle(R.string.preferences__pref_led_blink_custom_pattern_title); - builder.setView(view); - builder.setOnCancelListener(new CustomDialogCancelListener()); - builder.setNegativeButton(android.R.string.cancel, new CustomDialogCancelListener()); - builder.setPositiveButton(android.R.string.ok, new CustomDialogClickListener()); - builder.show(); - } - - private void showDialog() { - LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.led_pattern_dialog, null); - - this.seekBarOn = (SeekBar)view.findViewById(R.id.SeekBarOn); - this.seekBarOff = (SeekBar)view.findViewById(R.id.SeekBarOff); - this.seekBarOnLabel = (TextView)view.findViewById(R.id.SeekBarOnMsLabel); - this.seekBarOffLabel = (TextView)view.findViewById(R.id.SeekBarOffMsLabel); - - this.seekBarOn.setOnSeekBarChangeListener(this); - this.seekBarOff.setOnSeekBarChangeListener(this); - - initializeSeekBarValues(); - initializeDialog(view); - - dialogInProgress = true; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - if (dialogInProgress) { - showDialog(); - } - } - - @Override - protected View onCreateDialogView() { - dialogInProgress = false; - return super.onCreateDialogView(); - } - - public void onProgressChanged(SeekBar seekbar, int progress, boolean fromTouch) { - if (seekbar.equals(seekBarOn)) { - seekBarOnLabel.setText(Integer.toString(progress)); - } else if (seekbar.equals(seekBarOff)) { - seekBarOffLabel.setText(Integer.toString(progress)); - } - } - - public void onStartTrackingTouch(SeekBar seekBar) { - } - - public void onStopTrackingTouch(SeekBar seekBar) { - } - - private class CustomDialogCancelListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { - public void onClick(DialogInterface dialog, int which) { - dialogInProgress = false; - } - - public void onCancel(DialogInterface dialog) { - dialogInProgress = false; - } - } - - private class CustomDialogClickListener implements DialogInterface.OnClickListener { - - public void onClick(DialogInterface dialog, int which) { - String pattern = seekBarOnLabel.getText() + "," + seekBarOffLabel.getText(); - dialogInProgress = false; - - TextSecurePreferences.setNotificationLedPatternCustom(context, pattern); - Toast.makeText(context, R.string.preferences__pref_led_blink_custom_pattern_set, Toast.LENGTH_LONG).show(); - } - - } - -} diff --git a/src/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java index ebb6375933..274fedeae9 100644 --- a/src/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java @@ -1,8 +1,8 @@ package org.thoughtcrime.securesms.preferences; -import android.preference.ListPreference; -import android.preference.Preference; -import android.support.v4.preference.PreferenceFragment; + +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; import org.thoughtcrime.securesms.R; diff --git a/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java b/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java index e04547a570..a7f86016c6 100644 --- a/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java @@ -19,7 +19,7 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import android.support.v4.preference.PreferenceFragment; +import android.support.annotation.Nullable; import android.util.Log; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; @@ -33,19 +33,23 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.io.IOException; -public class MmsPreferencesFragment extends PreferenceFragment { +public class MmsPreferencesFragment extends CorrectedPreferenceFragment { private static final String TAG = MmsPreferencesFragment.class.getSimpleName(); @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_manual_mms); ((PassphraseRequiredActionBarActivity) getActivity()).getSupportActionBar() .setTitle(R.string.preferences__advanced_mms_access_point_names); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_manual_mms); + } + @Override public void onResume() { super.onResume(); @@ -74,15 +78,15 @@ public class MmsPreferencesFragment extends PreferenceFragment { @Override protected void onPostExecute(LegacyMmsConnection.Apn apnDefaults) { ((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF)) - .setValidator(new CustomDefaultPreference.UriValidator()) + .setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.UriValidator()) .setDefaultValue(apnDefaults.getMmsc()); ((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF)) - .setValidator(new CustomDefaultPreference.HostnameValidator()) + .setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.HostnameValidator()) .setDefaultValue(apnDefaults.getProxy()); ((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF)) - .setValidator(new CustomDefaultPreference.PortValidator()) + .setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.PortValidator()) .setDefaultValue(apnDefaults.getPort()); ((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_USERNAME_PREF)) diff --git a/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 45067ee796..65812f820c 100644 --- a/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -7,11 +7,12 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceManager; -import android.preference.RingtonePreference; +import android.support.annotation.Nullable; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceManager; import android.text.TextUtils; +import android.util.Log; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; @@ -21,13 +22,14 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment { + private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); + private MasterSecret masterSecret; @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); masterSecret = getArguments().getParcelable("master_secret"); - addPreferencesFromResource(R.xml.preferences_notifications); this.findPreference(TextSecurePreferences.LED_COLOR_PREF) .setOnPreferenceChangeListener(new ListSummaryListener()); @@ -47,7 +49,12 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme initializeListSummary((ListPreference) findPreference(TextSecurePreferences.REPEAT_ALERTS_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)); - initializeRingtoneSummary((RingtonePreference) findPreference(TextSecurePreferences.RINGTONE_PREF)); + initializeRingtoneSummary((AdvancedRingtonePreference) findPreference(TextSecurePreferences.RINGTONE_PREF)); + } + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_notifications); } @Override @@ -59,12 +66,12 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - String value = (String) newValue; + Uri value = (Uri) newValue; - if (TextUtils.isEmpty(value)) { + if (value == null) { preference.setSummary(R.string.preferences__silent); } else { - Ringtone tone = RingtoneManager.getRingtone(getActivity(), Uri.parse(value)); + Ringtone tone = RingtoneManager.getRingtone(getActivity(), value); if (tone != null) { preference.setSummary(tone.getTitle(getActivity())); } @@ -74,12 +81,13 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme } } - private void initializeRingtoneSummary(RingtonePreference pref) { - RingtoneSummaryListener listener = - (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + private void initializeRingtoneSummary(AdvancedRingtonePreference pref) { + RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String encodedUri = sharedPreferences.getString(pref.getKey(), null); + Uri uri = !TextUtils.isEmpty(encodedUri) ? Uri.parse(encodedUri) : null; - listener.onPreferenceChange(pref, sharedPreferences.getString(pref.getKey(), "")); + listener.onPreferenceChange(pref, uri); } public static CharSequence getSummary(Context context) { diff --git a/src/org/thoughtcrime/securesms/preferences/ProfilePreference.java b/src/org/thoughtcrime/securesms/preferences/ProfilePreference.java index 11060d6131..b0ade21558 100644 --- a/src/org/thoughtcrime/securesms/preferences/ProfilePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/ProfilePreference.java @@ -5,10 +5,11 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; -import android.preference.Preference; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceViewHolder; +import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -16,7 +17,6 @@ import android.widget.ImageView; import android.widget.TextView; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.profiles.AvatarHelper; @@ -55,12 +55,11 @@ public class ProfilePreference extends Preference { } @Override - protected void onBindView(View view) { - super.onBindView(view); - - avatarView = ViewUtil.findById(view, R.id.avatar); - profileNameView = ViewUtil.findById(view, R.id.profile_name); - profileNumberView = ViewUtil.findById(view, R.id.number); + public void onBindViewHolder(PreferenceViewHolder viewHolder) { + super.onBindViewHolder(viewHolder); + avatarView = (ImageView)viewHolder.findViewById(R.id.avatar); + profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name); + profileNumberView = (TextView)viewHolder.findViewById(R.id.number); refresh(); } diff --git a/src/org/thoughtcrime/securesms/preferences/RingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/RingtonePreference.java new file mode 100644 index 0000000000..a955196e09 --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/RingtonePreference.java @@ -0,0 +1,464 @@ +package org.thoughtcrime.securesms.preferences; + +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.annotation.Nullable; +import android.support.annotation.StringRes; +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/RingtonePreferenceDialogFragmentCompat.java b/src/org/thoughtcrime/securesms/preferences/RingtonePreferenceDialogFragmentCompat.java new file mode 100644 index 0000000000..57f703c075 --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/RingtonePreferenceDialogFragmentCompat.java @@ -0,0 +1,583 @@ +package org.thoughtcrime.securesms.preferences; + + +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.CursorAdapter; +import android.widget.HeaderViewListAdapter; +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.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 Cursor cursor; + + 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(); + + createCursor(ringtonePreference.getRingtone()); + + String colTitle = cursor.getColumnName(RingtoneManager.TITLE_COLUMN_INDEX); + + 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(cursor, selectedIndex, colTitle, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (i < cursor.getCount()) { + 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 Cursor createCursor(Uri ringtoneUri) { + RingtonePreference ringtonePreference = getRingtonePreference(); + 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}; + return this.cursor = new MergeCursor(cursors); + } + + @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 Cursor doInBackground(Uri... params) { + try { + Uri newUri = addCustomExternalRingtone(context, params[0], ringtoneType); + + return createCursor(newUri); + } catch (IOException | IllegalArgumentException e) { + Log.e(TAG, "Unable to add new ringtone: ", e); + } + return null; + } + + @Override + protected void onPostExecute(final Cursor newCursor) { + if (newCursor != null) { + final ListView listView = ((AlertDialog) getDialog()).getListView(); + final CursorAdapter adapter = ((CursorAdapter) ((HeaderViewListAdapter) listView.getAdapter()).getWrappedAdapter()); + adapter.changeCursor(newCursor); + + 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.execute(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/SignalListPreference.java b/src/org/thoughtcrime/securesms/preferences/SignalListPreference.java index 179589ff8c..5b5f07414e 100644 --- a/src/org/thoughtcrime/securesms/preferences/SignalListPreference.java +++ b/src/org/thoughtcrime/securesms/preferences/SignalListPreference.java @@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.preferences; import android.content.Context; import android.os.Build; -import android.preference.ListPreference; import android.support.annotation.RequiresApi; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -45,8 +46,8 @@ public class SignalListPreference extends ListPreference { } @Override - protected void onBindView(View view) { - super.onBindView(view); + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); this.rightSummary = (TextView)view.findViewById(R.id.right_summary); setSummary(this.summary); diff --git a/src/org/thoughtcrime/securesms/preferences/SignalRingtonePreference.java b/src/org/thoughtcrime/securesms/preferences/SignalRingtonePreference.java index 51aa0a4943..33257ee02c 100644 --- a/src/org/thoughtcrime/securesms/preferences/SignalRingtonePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/SignalRingtonePreference.java @@ -5,6 +5,7 @@ import android.content.Context; import android.os.Build; import android.preference.RingtonePreference; import android.support.annotation.RequiresApi; +import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; @@ -42,8 +43,8 @@ public class SignalRingtonePreference extends AdvancedRingtonePreference { } @Override - protected void onBindView(View view) { - super.onBindView(view); + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); this.rightSummary = (TextView)view.findViewById(R.id.right_summary); setSummary(summary); diff --git a/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java index 5d1e317633..a3a4b2bd33 100644 --- a/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java @@ -6,14 +6,14 @@ import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceScreen; import android.provider.Settings; import android.provider.Telephony; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; -import android.support.v4.preference.PreferenceFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; @@ -27,7 +27,7 @@ public class SmsMmsPreferenceFragment extends CorrectedPreferenceFragment { @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_sms_mms); + this.findPreference(MMS_PREF) .setOnPreferenceClickListener(new ApnPreferencesClickListener()); @@ -35,6 +35,11 @@ public class SmsMmsPreferenceFragment extends CorrectedPreferenceFragment { initializePlatformSpecificOptions(); } + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_sms_mms); + } + @Override public void onResume() { super.onResume();