mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
Support granular "custom" MMS preferences.
1) Make each MMS preference an individual choice between custom and default. 2) Display default values. Closes #2487 // FREEBIE
This commit is contained in:
parent
e31ddf0599
commit
534df06794
16 changed files with 543 additions and 221 deletions
|
@ -101,7 +101,7 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".MmsPreferencesActivity"
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ShareActivity"
|
||||
|
|
33
res/layout/custom_default_preference_dialog.xml
Normal file
33
res/layout/custom_default_preference_dialog.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Spinner android:id="@+id/default_or_custom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:entries="@array/default_or_custom_entries"
|
||||
android:entryValues="@array/default_or_custom_values"
|
||||
android:layout_marginStart="16dip"
|
||||
android:layout_marginLeft="16dip"
|
||||
android:spinnerMode="dropdown"/>
|
||||
|
||||
<TextView android:id="@+id/default_label"
|
||||
android:layout_marginLeft="18dip"
|
||||
android:layout_marginRight="16dip"
|
||||
android:layout_marginTop="16dip"
|
||||
android:layout_marginBottom="16dip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<EditText android:id="@+id/custom_edit"
|
||||
android:layout_marginLeft="16dip"
|
||||
android:layout_marginRight="16dip"
|
||||
android:layout_marginTop="16dip"
|
||||
android:layout_marginBottom="16dip"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -202,4 +202,15 @@
|
|||
<item>5</item>
|
||||
<item>10</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="default_or_custom_values" translatable="false">
|
||||
<item>default</item>
|
||||
<item>custom</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="default_or_custom_entries">
|
||||
<item>@string/arrays__use_default</item>
|
||||
<item>@string/arrays__use_custom</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -101,4 +101,9 @@
|
|||
<attr name="pref_ic_advanced" format="reference" />
|
||||
|
||||
<attr name="app_protect_timeout_picker_color" format="reference"/>
|
||||
|
||||
<declare-styleable name="CustomDefaultPreference">
|
||||
<attr name="custom_pref_toggle" format="string"/>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -153,6 +153,11 @@
|
|||
<!-- ConversationListItem -->
|
||||
<string name="ConversationListItem_key_exchange_message">Key exchange message...</string>
|
||||
|
||||
<!-- CustomDefaultPreference -->
|
||||
<string name="CustomDefaultPreference_using_custom">Using custom: %s</string>
|
||||
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
|
||||
<string name="CustomDefaultPreference_none">None</string>
|
||||
|
||||
<!-- DateUtils -->
|
||||
<string name="DateUtils_now">Now</string>
|
||||
|
||||
|
@ -691,6 +696,8 @@
|
|||
<!-- arrays.xml -->
|
||||
<string name="arrays__import_export">Import / export</string>
|
||||
<string name="arrays__my_identity_key">My identity key</string>
|
||||
<string name="arrays__use_default">Use default</string>
|
||||
<string name="arrays__use_custom">Use custom</string>
|
||||
|
||||
<!-- plurals.xml -->
|
||||
<plurals name="minutes_ago">
|
||||
|
@ -775,11 +782,11 @@
|
|||
<string name="preferences__advanced_mms_access_point_names">Manual MMS settings</string>
|
||||
<string name="preferences__enable_manual_mms">Use manual MMS settings</string>
|
||||
<string name="preferences__override_system_mms_settings">Override system MMS settings with the information below.</string>
|
||||
<string name="preferences__mmsc_url_required">MMSC URL (Required)</string>
|
||||
<string name="preferences__mms_proxy_host_optional">MMS Proxy Host (Optional)</string>
|
||||
<string name="preferences__mms_proxy_port_optional">MMS Proxy Port (Optional)</string>
|
||||
<string name="preferences__mmsc_username_optional">MMSC Username (Optional)</string>
|
||||
<string name="preferences__mmsc_password_optional">MMSC Password (Optional)</string>
|
||||
<string name="preferences__mmsc_url">MMSC URL</string>
|
||||
<string name="preferences__mms_proxy_host">MMS Proxy Host</string>
|
||||
<string name="preferences__mms_proxy_port">MMS Proxy Port</string>
|
||||
<string name="preferences__mmsc_username">MMSC Username</string>
|
||||
<string name="preferences__mmsc_password">MMSC Password</string>
|
||||
<string name="preferences__sms_delivery_reports">SMS delivery reports</string>
|
||||
<string name="preferences__request_a_delivery_report_for_each_sms_message_you_send">Request a delivery report for each SMS message you send</string>
|
||||
<string name="preferences__automatically_delete_older_messages_once_a_conversation_thread_exceeds_a_specified_length">Automatically delete older messages once a conversation thread exceeds a specified length</string>
|
||||
|
|
|
@ -1,33 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<CheckBoxPreference android:key="pref_enable_manual_mms"
|
||||
android:defaultValue="false"
|
||||
android:title="@string/preferences__enable_manual_mms"
|
||||
android:summary="@string/preferences__override_system_mms_settings"/>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<EditTextPreference android:key="pref_apn_mmsc_host"
|
||||
android:title="@string/preferences__mmsc_url_required"
|
||||
android:dependency="pref_enable_manual_mms"
|
||||
android:inputType="textUri" />
|
||||
<org.thoughtcrime.securesms.components.CustomDefaultPreference
|
||||
app:custom_pref_toggle="pref_apn_mmsc_custom_host"
|
||||
android:key="pref_apn_mmsc_host"
|
||||
android:title="@string/preferences__mmsc_url"
|
||||
android:inputType="textUri" />
|
||||
|
||||
<EditTextPreference android:key="pref_apn_mms_proxy"
|
||||
android:title="@string/preferences__mms_proxy_host_optional"
|
||||
android:dependency="pref_enable_manual_mms"
|
||||
android:inputType="textUri" />
|
||||
<org.thoughtcrime.securesms.components.CustomDefaultPreference
|
||||
app:custom_pref_toggle="pref_apn_mms_custom_proxy"
|
||||
android:key="pref_apn_mms_proxy"
|
||||
android:title="@string/preferences__mms_proxy_host"
|
||||
android:inputType="textUri" />
|
||||
|
||||
<EditTextPreference android:key="pref_apn_mms_proxy_port"
|
||||
android:title="@string/preferences__mms_proxy_port_optional"
|
||||
android:dependency="pref_enable_manual_mms"
|
||||
android:inputType="number" />
|
||||
<org.thoughtcrime.securesms.components.CustomDefaultPreference
|
||||
app:custom_pref_toggle="pref_apn_mms_custom_proxy_port"
|
||||
android:key="pref_apn_mms_proxy_port"
|
||||
android:title="@string/preferences__mms_proxy_port"
|
||||
android:inputType="number"/>
|
||||
|
||||
<EditTextPreference android:key="pref_apn_mmsc_username"
|
||||
android:title="@string/preferences__mmsc_username_optional"
|
||||
android:dependency="pref_enable_manual_mms"
|
||||
android:inputType="textNoSuggestions" />
|
||||
<org.thoughtcrime.securesms.components.CustomDefaultPreference
|
||||
app:custom_pref_toggle="pref_apn_mmsc_custom_username"
|
||||
android:key="pref_apn_mmsc_username"
|
||||
android:title="@string/preferences__mmsc_username"
|
||||
android:inputType="textNoSuggestions"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.CustomDefaultPreference
|
||||
app:custom_pref_toggle="pref_apn_mmsc_custom_password"
|
||||
android:key="pref_apn_mmsc_password"
|
||||
android:title="@string/preferences__mmsc_password"
|
||||
android:inputType="textVisiblePassword"/>
|
||||
|
||||
<EditTextPreference android:key="pref_apn_mmsc_password"
|
||||
android:title="@string/preferences__mmsc_password_optional"
|
||||
android:dependency="pref_enable_manual_mms"
|
||||
android:inputType="textVisiblePassword" />
|
||||
</PreferenceScreen>
|
|
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
|||
import org.thoughtcrime.securesms.jobs.SmsDecryptJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2014 Open 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceChangeListener;
|
||||
import android.support.v4.preference.PreferenceFragment;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
|
||||
public class MmsPreferencesFragment extends PreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
initializePreferences();
|
||||
initializeEditTextSummaries();
|
||||
((PassphraseRequiredActionBarActivity) getActivity()).getSupportActionBar()
|
||||
.setTitle(R.string.preferences__advanced_mms_access_point_names);
|
||||
}
|
||||
|
||||
private void initializePreferences() {
|
||||
if (!OutgoingMmsConnection.isConnectionPossible(getActivity())) {
|
||||
TextSecurePreferences.setUseLocalApnsEnabled(getActivity(), true);
|
||||
addPreferencesFromResource(R.xml.preferences_manual_mms);
|
||||
this.findPreference(TextSecurePreferences.ENABLE_MANUAL_MMS_PREF)
|
||||
.setOnPreferenceChangeListener(new OverrideMmsChangeListener());
|
||||
} else {
|
||||
addPreferencesFromResource(R.xml.preferences_manual_mms);
|
||||
}
|
||||
this.findPreference(TextSecurePreferences.MMSC_HOST_PREF).setOnPreferenceChangeListener(new ValidUriVerificationListener());
|
||||
this.findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF).setOnPreferenceChangeListener(new ValidHostnameVerificationListener());
|
||||
this.findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF).setOnPreferenceChangeListener(new EditTextVerificationListener());
|
||||
this.findPreference(TextSecurePreferences.MMSC_USERNAME_PREF).setOnPreferenceChangeListener(new EditTextVerificationListener());
|
||||
this.findPreference(TextSecurePreferences.MMSC_PASSWORD_PREF).setOnPreferenceChangeListener(new EditTextVerificationListener());
|
||||
}
|
||||
|
||||
private void initializeEditTextSummary(final EditTextPreference preference) {
|
||||
preference.setSummary(TextUtils.isEmpty(preference.getText()) ? getString(R.string.MmsPreferencesFragment__not_set) : preference.getText());
|
||||
}
|
||||
|
||||
private void initializeEditTextSummaries() {
|
||||
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_HOST_PREF));
|
||||
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF));
|
||||
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF));
|
||||
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_USERNAME_PREF));
|
||||
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_PASSWORD_PREF));
|
||||
}
|
||||
|
||||
private class OverrideMmsChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object o) {
|
||||
TextSecurePreferences.setUseLocalApnsEnabled(getActivity(), true);
|
||||
Toast.makeText(getActivity(), R.string.MmsPreferencesFragment__manual_mms_settings_are_required,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int enabledResId = R.string.MmsPreferencesFragment__enabled;
|
||||
final int disabledResId = R.string.MmsPreferencesFragment__disabled;
|
||||
|
||||
return context.getString(TextSecurePreferences.isUseLocalApnsEnabled(context) ? enabledResId : disabledResId);
|
||||
}
|
||||
|
||||
private class EditTextVerificationListener implements OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
String newString = (String)newValue;
|
||||
if (isValid(newString)) {
|
||||
preference.setSummary(TextUtils.isEmpty(newString) ? getString(R.string.MmsPreferencesFragment__not_set) : newString);
|
||||
return true;
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getErrorMessage(), Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isValid(String newString) { return true; }
|
||||
protected int getErrorMessage() { return 0; }
|
||||
}
|
||||
|
||||
private class ValidUriVerificationListener extends EditTextVerificationListener {
|
||||
@Override
|
||||
protected boolean isValid(String newString) {
|
||||
if (TextUtils.isEmpty(newString)) return true;
|
||||
try {
|
||||
new URI(newString);
|
||||
return true;
|
||||
} catch (URISyntaxException mue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getErrorMessage() {
|
||||
return R.string.MmsPreferencesFragment__invalid_uri;
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidHostnameVerificationListener extends EditTextVerificationListener {
|
||||
@Override
|
||||
protected boolean isValid(String newString) {
|
||||
if (TextUtils.isEmpty(newString)) return true;
|
||||
try {
|
||||
URI uri = new URI(null, newString, null, null);
|
||||
return true;
|
||||
} catch (URISyntaxException mue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getErrorMessage() {
|
||||
return R.string.MmsPreferencesFragment__invalid_host;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import android.os.Bundle;
|
|||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
|
||||
|
||||
public class PromptMmsActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private Button okButton;
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.preference.DialogPreference;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
|
||||
public class CustomDefaultPreference extends DialogPreference {
|
||||
|
||||
private static final String TAG = CustomDefaultPreference.class.getSimpleName();
|
||||
|
||||
private final int inputType;
|
||||
private final String customPreference;
|
||||
private final String customToggle;
|
||||
|
||||
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);
|
||||
|
||||
int[] attributeNames = new int[]{android.R.attr.inputType, R.attr.custom_pref_toggle};
|
||||
TypedArray attributes = context.obtainStyledAttributes(attrs, attributeNames);
|
||||
|
||||
this.inputType = attributes.getInt(0, 0);
|
||||
this.customPreference = getKey();
|
||||
this.customToggle = attributes.getString(1);
|
||||
this.validator = new NullValidator();
|
||||
|
||||
attributes.recycle();
|
||||
|
||||
setPersistent(false);
|
||||
setDialogLayoutResource(R.layout.custom_default_preference_dialog);
|
||||
}
|
||||
|
||||
public CustomDefaultPreference setValidator(CustomPreferenceValidator validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CustomDefaultPreference setDefaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
this.setSummary(getSummary());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSummary() {
|
||||
if (isCustom()) {
|
||||
return getContext().getString(R.string.CustomDefaultPreference_using_custom,
|
||||
getPrettyPrintValue(getCustomValue()));
|
||||
} else {
|
||||
return getContext().getString(R.string.CustomDefaultPreference_using_default,
|
||||
getPrettyPrintValue(getDefaultValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
private boolean isCustom() {
|
||||
return TextSecurePreferences.getBooleanPreference(getContext(), customToggle, false);
|
||||
}
|
||||
|
||||
private void setCustom(boolean custom) {
|
||||
TextSecurePreferences.setBooleanPreference(getContext(), customToggle, custom);
|
||||
}
|
||||
|
||||
private String getCustomValue() {
|
||||
return TextSecurePreferences.getStringPreference(getContext(), customPreference, "");
|
||||
}
|
||||
|
||||
private void setCustomValue(String value) {
|
||||
TextSecurePreferences.setStringPreference(getContext(), customPreference, value);
|
||||
}
|
||||
|
||||
private String getDefaultValue() {
|
||||
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()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
defaultLabel.setVisibility(View.VISIBLE);
|
||||
customText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private class TextValidator implements TextWatcher {
|
||||
|
||||
@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) {
|
||||
if (spinner.getSelectedItemPosition() == 1) {
|
||||
positiveButton.setEnabled(validator.isValid(s.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected interface CustomPreferenceValidator {
|
||||
public boolean isValid(String value);
|
||||
}
|
||||
|
||||
private static class NullValidator implements CustomPreferenceValidator {
|
||||
@Override
|
||||
public boolean isValid(String value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
|||
import org.thoughtcrime.securesms.mms.MmsConnection.Apn;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -93,13 +94,11 @@ public class ApnDatabase {
|
|||
null,
|
||||
SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
}
|
||||
protected Apn getLocallyConfiguredMmsConnectionParameters() throws ApnUnavailableException {
|
||||
if (TextSecurePreferences.isUseLocalApnsEnabled(context)) {
|
||||
String mmsc = TextSecurePreferences.getMmscUrl(context).trim();
|
||||
if (TextUtils.isEmpty(mmsc))
|
||||
throw new ApnUnavailableException("Malformed locally configured MMSC.");
|
||||
|
||||
if (!mmsc.startsWith("http"))
|
||||
private Apn getCustomApnParameters() {
|
||||
String mmsc = TextSecurePreferences.getMmscUrl(context).trim();
|
||||
|
||||
if (!TextUtils.isEmpty(mmsc) && !mmsc.startsWith("http"))
|
||||
mmsc = "http://" + mmsc;
|
||||
|
||||
String proxy = TextSecurePreferences.getMmscProxy(context);
|
||||
|
@ -108,26 +107,12 @@ public class ApnDatabase {
|
|||
String pass = TextSecurePreferences.getMmscPassword(context);
|
||||
|
||||
return new Apn(mmsc, proxy, port, user, pass);
|
||||
}
|
||||
|
||||
throw new ApnUnavailableException("No locally configured parameters available");
|
||||
|
||||
}
|
||||
|
||||
public Apn getMmsConnectionParameters(final String mccmnc, final String apn) {
|
||||
|
||||
if (TextSecurePreferences.isUseLocalApnsEnabled(context)) {
|
||||
Log.w(TAG, "Choosing locally-overridden MMS settings");
|
||||
try {
|
||||
return getLocallyConfiguredMmsConnectionParameters();
|
||||
} catch (ApnUnavailableException aue) {
|
||||
Log.w(TAG, "preference to use local apn set, but no parameters avaiable. falling back.");
|
||||
}
|
||||
}
|
||||
|
||||
public Apn getDefaultApnParameters(String mccmnc, String apn) {
|
||||
if (mccmnc == null) {
|
||||
Log.w(TAG, "mccmnc was null, returning null");
|
||||
return null;
|
||||
return Apn.EMPTY;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
|
@ -161,9 +146,25 @@ public class ApnDatabase {
|
|||
}
|
||||
|
||||
Log.w(TAG, "No matching APNs found, returning null");
|
||||
return null;
|
||||
|
||||
return Apn.EMPTY;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Optional<Apn> getMmsConnectionParameters(String mccmnc, String apn) {
|
||||
Apn customApn = getCustomApnParameters();
|
||||
Apn defaultApn = getDefaultApnParameters(mccmnc, apn);
|
||||
Apn result = new Apn(customApn, defaultApn,
|
||||
TextSecurePreferences.getUseCustomMmsc(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxy(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxyPort(context),
|
||||
TextSecurePreferences.getUseCustomMmscUsername(context),
|
||||
TextSecurePreferences.getUseCustomMmscPassword(context));
|
||||
|
||||
if (TextUtils.isEmpty(result.getMmsc())) return Optional.absent();
|
||||
else return Optional.of(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.ApnDatabase;
|
|||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -56,27 +57,24 @@ public abstract class MmsConnection {
|
|||
this.apn = apn;
|
||||
}
|
||||
|
||||
protected static Apn getLocalApn(Context context) throws ApnUnavailableException {
|
||||
try {
|
||||
Apn params = ApnDatabase.getInstance(context)
|
||||
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
public static Apn getApn(Context context, String apnName) throws ApnUnavailableException {
|
||||
Log.w(TAG, "Getting MMSC params for apn " + apnName);
|
||||
|
||||
if (params == null) {
|
||||
try {
|
||||
Optional<Apn> params = ApnDatabase.getInstance(context)
|
||||
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
|
||||
if (!params.isPresent()) {
|
||||
throw new ApnUnavailableException("No parameters available from ApnDefaults.");
|
||||
}
|
||||
|
||||
return params;
|
||||
return params.get();
|
||||
} catch (IOException ioe) {
|
||||
throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public static Apn getApn(Context context, String apnName) throws ApnUnavailableException {
|
||||
Log.w(TAG, "Getting MMSC params for apn " + apnName);
|
||||
return getLocalApn(context);
|
||||
}
|
||||
|
||||
protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
|
||||
throws IOException
|
||||
{
|
||||
|
@ -171,6 +169,9 @@ public abstract class MmsConnection {
|
|||
protected abstract HttpUriRequest constructRequest(boolean useProxy) throws IOException;
|
||||
|
||||
public static class Apn {
|
||||
|
||||
public static Apn EMPTY = new Apn("", "", "", "", "");
|
||||
|
||||
private final String mmsc;
|
||||
private final String proxy;
|
||||
private final String port;
|
||||
|
@ -185,6 +186,20 @@ public abstract class MmsConnection {
|
|||
this.password = password;
|
||||
}
|
||||
|
||||
public Apn(Apn customApn, Apn defaultApn,
|
||||
boolean useCustomMmsc,
|
||||
boolean useCustomProxy,
|
||||
boolean useCustomProxyPort,
|
||||
boolean useCustomUsername,
|
||||
boolean useCustomPassword)
|
||||
{
|
||||
this.mmsc = useCustomMmsc ? customApn.mmsc : defaultApn.mmsc;
|
||||
this.proxy = useCustomProxy ? customApn.proxy : defaultApn.proxy;
|
||||
this.port = useCustomProxyPort ? customApn.port : defaultApn.port;
|
||||
this.username = useCustomUsername ? customApn.username : defaultApn.username;
|
||||
this.password = useCustomPassword ? customApn.password : defaultApn.password;
|
||||
}
|
||||
|
||||
public boolean hasProxy() {
|
||||
return !TextUtils.isEmpty(proxy);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* 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;
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
@ -22,6 +22,7 @@ import android.support.v4.app.FragmentManager;
|
|||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Copyright (C) 2014 Open 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.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.preference.PreferenceFragment;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CustomDefaultPreference;
|
||||
import org.thoughtcrime.securesms.database.ApnDatabase;
|
||||
import org.thoughtcrime.securesms.mms.MmsConnection;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class MmsPreferencesFragment extends PreferenceFragment {
|
||||
|
||||
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 onResume() {
|
||||
super.onResume();
|
||||
new LoadApnDefaultsTask().execute();
|
||||
}
|
||||
|
||||
private class LoadApnDefaultsTask extends AsyncTask<Void, Void, MmsConnection.Apn> {
|
||||
|
||||
@Override
|
||||
protected MmsConnection.Apn doInBackground(Void... params) {
|
||||
try {
|
||||
Context context = getActivity();
|
||||
|
||||
if (context != null) {
|
||||
return ApnDatabase.getInstance(context)
|
||||
.getDefaultApnParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MmsConnection.Apn apnDefaults) {
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF))
|
||||
.setValidator(new CustomDefaultPreference.UriValidator())
|
||||
.setDefaultValue(apnDefaults.getMmsc());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF))
|
||||
.setValidator(new CustomDefaultPreference.HostnameValidator())
|
||||
.setDefaultValue(apnDefaults.getProxy());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF))
|
||||
.setValidator(new CustomDefaultPreference.PortValidator())
|
||||
.setDefaultValue(apnDefaults.getPort());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_USERNAME_PREF))
|
||||
.setDefaultValue(apnDefaults.getPort());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PASSWORD_PREF))
|
||||
.setDefaultValue(apnDefaults.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -15,7 +15,6 @@ import android.support.v4.preference.PreferenceFragment;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.MmsPreferencesFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.OutgoingSmsPreference;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -46,8 +45,6 @@ public class SmsMmsPreferenceFragment extends PreferenceFragment {
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__sms_mms);
|
||||
this.findPreference(MMS_PREF)
|
||||
.setSummary(MmsPreferencesFragment.getSummary(getActivity()));
|
||||
|
||||
initializePlatformSpecificOptions();
|
||||
}
|
||||
|
|
|
@ -15,10 +15,15 @@ public class TextSecurePreferences {
|
|||
public static final String DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase";
|
||||
public static final String THEME_PREF = "pref_theme";
|
||||
public static final String LANGUAGE_PREF = "pref_language";
|
||||
private static final String MMSC_CUSTOM_HOST_PREF = "pref_apn_mmsc_custom_host";
|
||||
public static final String MMSC_HOST_PREF = "pref_apn_mmsc_host";
|
||||
private static final String MMSC_CUSTOM_PROXY_PREF = "pref_apn_mms_custom_proxy";
|
||||
public static final String MMSC_PROXY_HOST_PREF = "pref_apn_mms_proxy";
|
||||
private static final String MMSC_CUSTOM_PROXY_PORT_PREF = "pref_apn_mms_custom_proxy_port";
|
||||
public static final String MMSC_PROXY_PORT_PREF = "pref_apn_mms_proxy_port";
|
||||
private static final String MMSC_CUSTOM_USERNAME_PREF = "pref_apn_mmsc_custom_username";
|
||||
public static final String MMSC_USERNAME_PREF = "pref_apn_mmsc_username";
|
||||
private static final String MMSC_CUSTOM_PASSWORD_PREF = "pref_apn_mmsc_custom_password";
|
||||
public static final String MMSC_PASSWORD_PREF = "pref_apn_mmsc_password";
|
||||
public static final String THREAD_TRIM_LENGTH = "pref_trim_length";
|
||||
public static final String THREAD_TRIM_NOW = "pref_trim_now";
|
||||
|
@ -206,26 +211,91 @@ public class TextSecurePreferences {
|
|||
setBooleanPreference(context, DISABLE_PASSPHRASE_PREF, disabled);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomMmsc(Context context) {
|
||||
boolean legacy = TextSecurePreferences.isLegacyUseLocalApnsEnabled(context);
|
||||
return getBooleanPreference(context, MMSC_CUSTOM_HOST_PREF, legacy);
|
||||
}
|
||||
|
||||
public static void setUseCustomMmsc(Context context, boolean value) {
|
||||
setBooleanPreference(context, MMSC_CUSTOM_HOST_PREF, value);
|
||||
}
|
||||
|
||||
public static String getMmscUrl(Context context) {
|
||||
return getStringPreference(context, MMSC_HOST_PREF, "");
|
||||
}
|
||||
|
||||
public static void setMmscUrl(Context context, String mmsc) {
|
||||
setStringPreference(context, MMSC_HOST_PREF, mmsc);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomMmscProxy(Context context) {
|
||||
boolean legacy = TextSecurePreferences.isLegacyUseLocalApnsEnabled(context);
|
||||
return getBooleanPreference(context, MMSC_CUSTOM_PROXY_PREF, legacy);
|
||||
}
|
||||
|
||||
public static void setUseCustomMmscProxy(Context context, boolean value) {
|
||||
setBooleanPreference(context, MMSC_CUSTOM_PROXY_PREF, value);
|
||||
}
|
||||
|
||||
public static String getMmscProxy(Context context) {
|
||||
return getStringPreference(context, MMSC_PROXY_HOST_PREF, "");
|
||||
}
|
||||
|
||||
public static void setMmscProxy(Context context, String value) {
|
||||
setStringPreference(context, MMSC_PROXY_HOST_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomMmscProxyPort(Context context) {
|
||||
boolean legacy = TextSecurePreferences.isLegacyUseLocalApnsEnabled(context);
|
||||
return getBooleanPreference(context, MMSC_CUSTOM_PROXY_PORT_PREF, legacy);
|
||||
}
|
||||
|
||||
public static void setUseCustomMmscProxyPort(Context context, boolean value) {
|
||||
setBooleanPreference(context, MMSC_CUSTOM_PROXY_PORT_PREF, value);
|
||||
}
|
||||
|
||||
public static String getMmscProxyPort(Context context) {
|
||||
return getStringPreference(context, MMSC_PROXY_PORT_PREF, "");
|
||||
}
|
||||
|
||||
public static void setMmscProxyPort(Context context, String value) {
|
||||
setStringPreference(context, MMSC_PROXY_PORT_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomMmscUsername(Context context) {
|
||||
boolean legacy = TextSecurePreferences.isLegacyUseLocalApnsEnabled(context);
|
||||
return getBooleanPreference(context, MMSC_CUSTOM_USERNAME_PREF, legacy);
|
||||
}
|
||||
|
||||
public static void setUseCustomMmscUsername(Context context, boolean value) {
|
||||
setBooleanPreference(context, MMSC_CUSTOM_USERNAME_PREF, value);
|
||||
}
|
||||
|
||||
public static String getMmscUsername(Context context) {
|
||||
return getStringPreference(context, MMSC_USERNAME_PREF, "");
|
||||
}
|
||||
|
||||
public static void setMmscUsername(Context context, String value) {
|
||||
setStringPreference(context, MMSC_USERNAME_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomMmscPassword(Context context) {
|
||||
boolean legacy = TextSecurePreferences.isLegacyUseLocalApnsEnabled(context);
|
||||
return getBooleanPreference(context, MMSC_CUSTOM_PASSWORD_PREF, legacy);
|
||||
}
|
||||
|
||||
public static void setUseCustomMmscPassword(Context context, boolean value) {
|
||||
setBooleanPreference(context, MMSC_CUSTOM_PASSWORD_PREF, value);
|
||||
}
|
||||
|
||||
public static String getMmscPassword(Context context) {
|
||||
return getStringPreference(context, MMSC_PASSWORD_PREF, "");
|
||||
}
|
||||
|
||||
public static void setMmscPassword(Context context, String value) {
|
||||
setStringPreference(context, MMSC_PASSWORD_PREF, value);
|
||||
}
|
||||
|
||||
public static String getIdentityContactUri(Context context) {
|
||||
return getStringPreference(context, IDENTITY_PREF, null);
|
||||
}
|
||||
|
@ -242,14 +312,10 @@ public class TextSecurePreferences {
|
|||
return getBooleanPreference(context, SCREEN_SECURITY_PREF, true);
|
||||
}
|
||||
|
||||
public static boolean isUseLocalApnsEnabled(Context context) {
|
||||
public static boolean isLegacyUseLocalApnsEnabled(Context context) {
|
||||
return getBooleanPreference(context, ENABLE_MANUAL_MMS_PREF, false);
|
||||
}
|
||||
|
||||
public static void setUseLocalApnsEnabled(Context context, boolean useLocal) {
|
||||
setBooleanPreference(context, ENABLE_MANUAL_MMS_PREF, useLocal);
|
||||
}
|
||||
|
||||
public static int getLastVersionCode(Context context) {
|
||||
return getIntegerPreference(context, LAST_VERSION_CODE_PREF, 0);
|
||||
}
|
||||
|
@ -373,11 +439,11 @@ public class TextSecurePreferences {
|
|||
setLongPreference(context, PUSH_REGISTRATION_REMINDER_PREF, time);
|
||||
}
|
||||
|
||||
private static void setBooleanPreference(Context context, String key, boolean value) {
|
||||
public static void setBooleanPreference(Context context, String key, boolean value) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
private static boolean getBooleanPreference(Context context, String key, boolean defaultValue) {
|
||||
public static boolean getBooleanPreference(Context context, String key, boolean defaultValue) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
|
@ -385,7 +451,7 @@ public class TextSecurePreferences {
|
|||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
private static String getStringPreference(Context context, String key, String defaultValue) {
|
||||
public static String getStringPreference(Context context, String key, String defaultValue) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getString(key, defaultValue);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue