Update to V7 PreferencesCompat library

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2017-09-20 18:10:44 -07:00
parent cb9bc9659b
commit a1c276f70b
35 changed files with 1788 additions and 657 deletions

View File

@@ -25,16 +25,17 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.preference.Preference;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
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.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.Preference;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
@@ -138,7 +139,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.preferences);
MasterSecret masterSecret = getArguments().getParcelable("master_secret");
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
@@ -163,6 +163,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
}
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
}
@Override
public void onResume() {
super.onResume();

View File

@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.components;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.preference.DialogPreference;
import android.support.v7.preference.PreferenceDialogFragmentCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -19,6 +21,7 @@ import android.widget.Spinner;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.CustomPreferenceValidator;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.net.URI;
@@ -36,11 +39,6 @@ public class CustomDefaultPreference extends DialogPreference {
private CustomPreferenceValidator validator;
private String defaultValue;
private Spinner spinner;
private EditText customText;
private TextView defaultLabel;
private Button positiveButton;
public CustomDefaultPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -50,7 +48,7 @@ public class CustomDefaultPreference extends DialogPreference {
this.inputType = attributes.getInt(0, 0);
this.customPreference = getKey();
this.customToggle = attributes.getString(1);
this.validator = new NullValidator();
this.validator = new CustomDefaultPreferenceDialogFragmentCompat.NullValidator();
attributes.recycle();
@@ -80,40 +78,6 @@ public class CustomDefaultPreference extends DialogPreference {
}
}
@Override
protected void onBindDialogView(@NonNull View view) {
super.onBindDialogView(view);
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(inputType);
this.customText.addTextChangedListener(new TextValidator());
this.customText.setText(getCustomValue());
this.spinner.setOnItemSelectedListener(new SelectionLister());
this.defaultLabel.setText(getPrettyPrintValue(defaultValue));
}
@Override
protected void showDialog(Bundle instanceState) {
super.showDialog(instanceState);
positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
if (isCustom()) spinner.setSelection(1, true);
else spinner.setSelection(0, true);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
if (spinner != null) setCustom(spinner.getSelectedItemPosition() == 1);
if (customText != null) setCustomValue(customText.getText().toString());
setSummary(getSummary());
}
}
private String getPrettyPrintValue(String value) {
if (TextUtils.isEmpty(value)) return getContext().getString(R.string.CustomDefaultPreference_none);
else return value;
@@ -139,87 +103,155 @@ public class CustomDefaultPreference extends DialogPreference {
return defaultValue;
}
private class SelectionLister implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> 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);
}
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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");

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@@ -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;

View File

@@ -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))

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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.
* <p>
* 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'.
* <p>
* 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'.
* <p>
* 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)}.
* <p>
* 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)}.
* <p>
* 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[])}.
* <p>
* 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[])}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@@ -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<Uri, Void, Cursor> installTask = new AsyncTask<Uri, Void, Cursor>() {
@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.
* <p>
* 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<Uri> 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();
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();