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

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message"/>
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference"/>
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"

View File

@ -34,6 +34,9 @@ repositories {
maven {
url "https://raw.github.com/whispersystems/maven/master/shortcutbadger/releases/"
}
maven {
url "https://maven.google.com"
}
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
}
@ -42,18 +45,15 @@ repositories {
}
dependencies {
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:recyclerview-v7:25.1.0'
compile 'com.android.support:design:25.1.0'
compile 'com.android.support:support-v13:25.1.0'
compile 'com.android.support:cardview-v7:25.1.0'
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
exclude module: 'support-v4'
}
compile ('com.android.support:gridlayout-v7:25.1.0') {
exclude module: 'support-v4'
}
compile 'com.android.support:multidex:1.0.1'
compile 'com.android.support:appcompat-v7:25.4.0'
compile 'com.android.support:recyclerview-v7:25.4.0'
compile 'com.android.support:design:25.4.0'
compile 'com.android.support:support-v13:25.4.0'
compile 'com.android.support:cardview-v7:25.4.0'
compile 'com.android.support:preference-v7:25.4.0'
compile 'com.android.support:preference-v14:25.4.0'
compile 'com.android.support:gridlayout-v7:25.4.0'
compile 'com.android.support:multidex:1.0.2'
compile 'com.google.android.gms:play-services-gcm:9.6.1'
compile 'com.google.android.gms:play-services-maps:9.6.1'
@ -101,6 +101,10 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
compile 'com.annimon:stream:1.1.8'
compile ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
testCompile 'junit:junit:4.12'
@ -111,6 +115,8 @@ dependencies {
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.1'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.1'
androidTestCompile 'com.android.support:multidex:1.0.2'
androidTestCompile 'com.android.support:multidex-instrumentation:1.0.2'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile ('org.assertj:assertj-core:1.7.1') {
@ -124,14 +130,15 @@ dependencies {
dependencyVerification {
verify = [
'com.android.support:appcompat-v7:b48bfd5efc14da938ba0647f9894aa7d3d90f0b618167652a318f6f336ef303e',
'com.android.support:recyclerview-v7:45beed1778f785c75540b68aa7735b8973a518ac21e8d763188dbbdae6c5b65d',
'com.android.support:design:92466557dc6a222bbff361801b26979573cb7086119331e78c74a2df34d5e11e',
'com.android.support:support-v13:deeb43c2878025f2a0485791f66b5f59a1c6a4c6671c6ad7bb20abfeffaa313f',
'com.android.support:cardview-v7:cd6f472f130a75f029cd1b7c56f72174023d56a2eee2b97577837fe39169d5df',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.android.support:gridlayout-v7:c24f2aa68089fd31b39811bc502e24637a6a8cc29bbf709cda4f4605047c9e11',
'com.android.support:multidex:60df8e7153fabdcf3a6a48a2ce5980b599cf25cbeb3b689b7b615975ca731840',
'com.android.support:appcompat-v7:70551e62660db15b790c5275f56b9de4dd9407d1494d07c8f3dd5698f3638677',
'com.android.support:recyclerview-v7:a2fe121f9d01ed8980e97095b4a3fe9700a0aa0a7d4b0f8c594f765ad8455a0d',
'com.android.support:design:3f409bf2019967ffc344cfaf11e52131fac982468a1707aaeb25bf3c52838966',
'com.android.support:support-v13:f2dcf3eb3fe0271038dc78f6cff9cc3256a591f5c5198277b3887480e5b95e79',
'com.android.support:cardview-v7:f3fbbe1fcfdbec7333c6a2c516c5fd511a909d1975271818e268d6fe297d8c70',
'com.android.support:preference-v7:69bfa8e5527585dc51c02393b882c6b7e1e682995d5fbe45f57b7cb4e6970a7b',
'com.android.support:preference-v14:aac6e6cb89b70e27859d88cf75410a5feab1c7a9364d420de42ddb7c154a866b',
'com.android.support:gridlayout-v7:4c805b95e5b0a39c7244a0d1a14449cc54a9ab242b806f7379b38846f0539ce9',
'com.android.support:multidex:7cd48755c7cfdb6dd2d21cbb02236ec390f6ac91cde87eb62f475b259ab5301d',
'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae',
'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e',
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
@ -163,12 +170,14 @@ dependencyVerification {
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
'com.klinkerapps:android-smsmms:e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1',
'com.annimon:stream:5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9',
'com.android.support:support-v4:ed4cda7c752f51d33f9bbdfff3422b425b323d356cd1bdc9786aa413c912e594',
'com.android.support:support-vector-drawable:2697503d3e8e709023ae176ba5db7f98ca0aa0b4e6290aedcb3c371904806bf7',
'com.android.support:animated-vector-drawable:6d05cb63d1f68900220f85c56dfe1066a9bb19cb0ec1247cc68fc2ba32f6b4a7',
'com.android.support:support-compat:e880fb1209c33fcb43e2b25716808e1a6e0b4d3170d5a8dc7704e15084428f88',
'com.android.support:support-core-ui:0149b54fd3bc9f4b3b2d321ff53c11821b31a2eca1e664d0cee224e8f53073d6',
'com.android.support:transition:cf53f778352fe0b74ff14d838bef9fe79264f3fd43eac499b6e0d1664dbd8997',
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
'com.android.support:support-annotations:a774272036941b4e912eb426d70c848bde7f06a3bf5fb491f75a427dc6595270',
'com.android.support:support-v4:ee44c481a1f4d6978568e223e8125379b52b2ececdd53450e09ebae144bd377d',
'com.android.support:support-vector-drawable:077009d13882ee96f061e4bc2dbe7cce7ae1762d8297592a787ff741afbfb1f2',
'com.android.support:animated-vector-drawable:628ab1d56a6ee4cbedf32617af8b2a1fe02964ed0628e8f898cc09ddba6e1835',
'com.android.support:support-compat:54019c63614ce08b02d7b9605490cd2b29ba5b2505f394a9517450b5f72b30ca',
'com.android.support:support-core-ui:e72ae29b823889686cff6fcb948d6745c2baf6d4c2af4fdffa1ec1e42e3833a3',
'com.android.support:transition:848270144fb180efd2bf928a00ed176dbbc5290badfd638390ffba90088df8b3',
'com.google.android.gms:play-services-base:0ca636a8fc9a5af45e607cdcd61783bf5d561cbbb0f862021ce69606eee5ad49',
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
@ -177,6 +186,7 @@ dependencyVerification {
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541',
'com.google.android:flexbox:a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935',
'com.google.android.gms:play-services-tasks:69ec265168e601d0203d04cd42e34bb019b2f029aa1e16fabd38a5153eea2086',
'org.whispersystems:curve25519-android:82595394422b957d4a5b5f1b27b75ba25cf6dc4db4d312418ca38cd6fff279ca',
'org.whispersystems:signal-protocol-java:5152c2b01a25147967d6bf82e540f947901bdfa79260be3eb3e96b03f787d6b5',
@ -190,16 +200,15 @@ dependencyVerification {
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'com.android.support:support-annotations:47a2a30eab487a490a8a8f16678007c3d2b6dcae1e09b0485a12bbf921200ec3',
'com.android.support:support-media-compat:8d6a1a5ba3d9eb1a25cb8f21bb312ac6280202e3d2900cb0b447d065d0d8a125',
'com.android.support:support-core-utils:a7649e18c04143dde40c218c5ce9a030e7ae674089cd7b18c6cf8ed2a22cf01a',
'com.android.support:support-fragment:1294500b357f52cf3779e2521c79f54ae7844f3b9a5f6727495dbbda7f231377',
'com.android.support:support-media-compat:566a161d9cb0083ef62a53e46b71ce5b3d455b8635b1a0a4ae28d96d4b583de8',
'com.android.support:support-core-utils:34b8437dfa95ff28d29cf57ffa3b1354a9fa9bfe4059f0fd5ce2f5e4326a1748',
'com.android.support:support-fragment:316d35d4d2d2902057efad104a73e4bdb50bee260a7075678185b8cd71170945',
]
}
android {
compileSdkVersion 25
buildToolsVersion '25.0.0'
buildToolsVersion '25.0.2'
useLibrary 'org.apache.http.legacy'
dexOptions {

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" />

10
res/drawable/ic_add.xml Normal file
View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorAccent"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_add" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/add_ringtone_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:maxLines="3"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding"
android:text="@string/RingtonePreference_add_ringtone_text"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?colorAccent" />
</LinearLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/color_picker_widget"
android:layout_width="24dp"
android:layout_height="24dp"
android:clickable="false"
android:focusable="false" />

View File

@ -133,14 +133,12 @@
<item>@string/preferences__fast</item>
<item>@string/preferences__normal</item>
<item>@string/preferences__slow</item>
<item>@string/preferences__custom</item>
</string-array>
<string-array name="pref_led_blink_pattern_values" translatable="false">
<item>300,300</item>
<item>500,2000</item>
<item>3000,3000</item>
<item>custom</item>
</string-array>
<string-array name="pref_repeat_alerts_entries">

View File

@ -199,4 +199,26 @@
<attr name="scaleEmojis" format="boolean" />
</declare-styleable>
<declare-styleable name="RingtonePreference">
<attr name="showAdd" format="boolean" />
<attr name="summaryHasRingtone" format="string|reference" />
</declare-styleable>
<declare-styleable name="ColorPickerPreference">
<attr name="currentColor" format="reference" />
<attr name="colors" format="reference" />
<attr name="sortColors" format="boolean|reference" />
<attr name="colorDescriptions" format="reference" />
<attr name="columns" format="integer|reference" />
<attr name="colorSize" format="enum|reference">
<enum name="large" value="1" />
<enum name="small" value="2" />
</attr>
</declare-styleable>
</resources>

View File

@ -1460,6 +1460,14 @@
<string name="preferences__read_receipts">Read receipts</string>
<string name="preferences__if_read_receipts_are_disabled_you_wont_be_able_to_see_read_receipts">If you read receipts are disabled, you won\'t be able to see read receipts from others.</string>
<string name="RingtonePreference_ringtone_default">Default ringtone</string>
<string name="RingtonePreference_ringtone_silent">None</string>
<string name="RingtonePreference_ringtone_picker_title">Ringtones</string>
<string name="RingtonePreference_notification_sound_default">Default notification sound</string>
<string name="RingtonePreference_alarm_sound_default">Default alarm sound</string>
<string name="RingtonePreference_add_ringtone_text">Add ringtone</string>
<string name="RingtonePreference_unable_to_add_ringtone">Unable to add custom ringtone</string>
<!-- EOF -->

View File

@ -256,4 +256,7 @@
<item name="android:focusable">false</item>
</style>
<style name="PreferenceThemeOverlay.Fix" parent="PreferenceThemeOverlay.v14.Material">
</style>
</resources>

View File

@ -18,6 +18,8 @@
<item name="contact_selection_lay_user">#a0000000</item>
<item name="contact_selection_header_text">@color/textsecure_primary_dark</item>
<item name="dialog_background_color">@color/background_material_light</item>
<item name="pref_divider">@drawable/preference_divider_light</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
</style>
<style name="TextSecure.DarkNoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
@ -34,6 +36,8 @@
<item name="contact_selection_lay_user">#afeeeeee</item>
<item name="contact_selection_header_text">#66eeeeee</item>
<item name="dialog_background_color">@color/background_material_dark</item>
<item name="pref_divider">@drawable/preference_divider_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
</style>
<style name="TextSecure.HighlightTheme" parent="@style/TextSecure.LightTheme">
@ -212,6 +216,7 @@
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment.Light</item>
<item name="group_members_dialog_icon">@drawable/ic_group_grey600_24dp</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
</style>
<style name="TextSecure.DarkTheme" parent="@style/Theme.AppCompat">
@ -335,5 +340,6 @@
<item name="app_protect_timeout_picker_color">@style/BetterPickersDialogFragment</item>
<item name="group_members_dialog_icon">@drawable/ic_group_white_24dp</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
</style>
</resources>

View File

@ -30,7 +30,7 @@
android:entries="@array/pref_led_color_entries"
android:entryValues="@array/pref_led_color_values" />
<org.thoughtcrime.securesms.preferences.LedBlinkPatternListPreference
<org.thoughtcrime.securesms.preferences.SignalListPreference
android:key="pref_led_blink"
android:defaultValue="500,2000"
android:title="@string/preferences__pref_led_blink_title"

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