Merge branch '1826-settings-view-model' into 'master'

Finish migrating SettingsFragment to ViewModel

Closes #1942 and #1826

See merge request briar/briar!1350
This commit is contained in:
akwizgran
2021-04-01 13:20:12 +00:00
37 changed files with 1837 additions and 1135 deletions

View File

@@ -36,6 +36,10 @@ import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.settings.ConnectionsFragment;
import org.briarproject.briar.android.settings.NotificationsFragment;
import org.briarproject.briar.android.settings.SecurityFragment;
import org.briarproject.briar.android.settings.SettingsFragment;
import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
@@ -193,4 +197,12 @@ public interface AndroidComponent
void inject(EmojiTextInputView textInputView);
void inject(BriarModelLoader briarModelLoader);
void inject(SettingsFragment settingsFragment);
void inject(ConnectionsFragment connectionsFragment);
void inject(SecurityFragment securityFragment);
void inject(NotificationsFragment notificationsFragment);
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.StrictMode;
@@ -115,6 +116,11 @@ public class AppModule {
this.application = application;
}
public static AndroidComponent getAndroidComponent(Context ctx) {
BriarApplication app = (BriarApplication) ctx.getApplicationContext();
return app.getApplicationComponent();
}
@Provides
@Singleton
Application providesApplication() {

View File

@@ -12,7 +12,7 @@ import java.util.Locale;
import javax.annotation.Nullable;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
import static org.briarproject.briar.android.settings.DisplayFragment.PREF_LANGUAGE;
@NotNullByDefault
public class Localizer {
@@ -25,7 +25,7 @@ public class Localizer {
private Localizer(SharedPreferences sharedPreferences) {
this(Locale.getDefault(), getLocaleFromTag(
sharedPreferences.getString(LANGUAGE, "default")));
sharedPreferences.getString(PREF_LANGUAGE, "default")));
}
private Localizer(Locale systemLocale, @Nullable Locale userLocale) {

View File

@@ -40,8 +40,8 @@ import static android.os.SystemClock.elapsedRealtime;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -24,7 +25,6 @@ import javax.inject.Inject;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@@ -56,14 +56,18 @@ public class ChangePasswordActivity extends BriarActivity
@VisibleForTesting
ChangePasswordViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_change_password);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
currentPasswordEntryWrapper =
findViewById(R.id.current_password_entry_wrapper);
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
@@ -77,7 +81,6 @@ public class ChangePasswordActivity extends BriarActivity
progress = findViewById(R.id.progress_wheel);
TextWatcher tw = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
@@ -102,8 +105,12 @@ public class ChangePasswordActivity extends BriarActivity
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void enableOrDisableContinueButton() {

View File

@@ -1,14 +0,0 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultHandler;
@NotNullByDefault
public interface ChangePasswordController {
float estimatePasswordStrength(String password);
void changePassword(String oldPassword, String newPassword,
ResultHandler<Boolean> resultHandler);
}

View File

@@ -14,7 +14,7 @@ import javax.inject.Inject;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.content.Intent.ACTION_MY_PACKAGE_REPLACED;
import static org.briarproject.briar.android.settings.SettingsFragment.NOTIFY_SIGN_IN;
import static org.briarproject.briar.android.settings.NotificationsFragment.PREF_NOTIFY_SIGN_IN;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_DISMISS_REMINDER;
public class SignInReminderReceiver extends BroadcastReceiver {
@@ -37,7 +37,7 @@ public class SignInReminderReceiver extends BroadcastReceiver {
if (accountManager.accountExists() &&
!accountManager.hasDatabaseKey()) {
SharedPreferences prefs = app.getDefaultSharedPreferences();
if (prefs.getBoolean(NOTIFY_SIGN_IN, true)) {
if (prefs.getBoolean(PREF_NOTIFY_SIGN_IN, true)) {
notificationManager.showSignInNotification();
}
}

View File

@@ -20,7 +20,7 @@ import javax.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
import androidx.preference.SwitchPreferenceCompat;
import info.guardianproject.panic.PanicResponder;
import static android.app.Activity.RESULT_CANCELED;
@@ -40,7 +40,7 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
Logger.getLogger(PanicPreferencesFragment.class.getName());
private PackageManager pm;
private SwitchPreference lockPref, purgePref;
private SwitchPreferenceCompat lockPref, purgePref;
private ListPreference panicAppPref;
@Override
@@ -51,9 +51,9 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
private void updatePreferences() {
pm = getActivity().getPackageManager();
lockPref = (SwitchPreference) findPreference(KEY_LOCK);
lockPref = (SwitchPreferenceCompat) findPreference(KEY_LOCK);
panicAppPref = (ListPreference) findPreference(KEY_PANIC_APP);
purgePref = (SwitchPreference) findPreference(KEY_PURGE);
purgePref = (SwitchPreferenceCompat) findPreference(KEY_PURGE);
// check for connect/disconnect intents from panic trigger apps
if (PanicResponder.checkForDisconnectIntent(getActivity())) {

View File

@@ -0,0 +1,47 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import de.hdodenhof.circleimageview.CircleImageView;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
@NotNullByDefault
public class AvatarPreference extends Preference {
@Nullable
private OwnIdentityInfo info;
public AvatarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_avatar);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
View v = holder.itemView;
if (info != null) {
TextView textViewUserName = v.findViewById(R.id.username);
CircleImageView imageViewAvatar = v.findViewById(R.id.avatarImage);
textViewUserName.setText(info.getLocalAuthor().getName());
setAvatar(imageViewAvatar, info.getLocalAuthor().getId(),
info.getAuthorInfo());
}
}
void setOwnIdentityInfo(OwnIdentityInfo info) {
this.info = info;
notifyChanged();
}
}

View File

@@ -0,0 +1,118 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConnectionsFragment extends PreferenceFragmentCompat {
static final String PREF_KEY_BLUETOOTH = "pref_key_bluetooth";
static final String PREF_KEY_WIFI = "pref_key_wifi";
static final String PREF_KEY_TOR_ENABLE = "pref_key_tor_enable";
static final String PREF_KEY_TOR_NETWORK = "pref_key_tor_network";
static final String PREF_KEY_TOR_MOBILE_DATA =
"pref_key_tor_mobile_data";
static final String PREF_KEY_TOR_ONLY_WHEN_CHARGING =
"pref_key_tor_only_when_charging";
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private ConnectionsManager connectionsManager;
private SwitchPreferenceCompat enableBluetooth;
private SwitchPreferenceCompat enableWifi;
private SwitchPreferenceCompat enableTor;
private ListPreference torNetwork;
private SwitchPreferenceCompat torMobile;
private SwitchPreferenceCompat torOnlyWhenCharging;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
connectionsManager = viewModel.connectionsManager;
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_connections);
enableBluetooth = findPreference(PREF_KEY_BLUETOOTH);
enableWifi = findPreference(PREF_KEY_WIFI);
enableTor = findPreference(PREF_KEY_TOR_ENABLE);
torNetwork = findPreference(PREF_KEY_TOR_NETWORK);
torMobile = findPreference(PREF_KEY_TOR_MOBILE_DATA);
torOnlyWhenCharging = findPreference(PREF_KEY_TOR_ONLY_WHEN_CHARGING);
torNetwork.setSummaryProvider(viewModel.torSummaryProvider);
enableBluetooth.setPreferenceDataStore(connectionsManager.btStore);
enableWifi.setPreferenceDataStore(connectionsManager.wifiStore);
enableTor.setPreferenceDataStore(connectionsManager.torStore);
torNetwork.setPreferenceDataStore(connectionsManager.torStore);
torMobile.setPreferenceDataStore(connectionsManager.torStore);
torOnlyWhenCharging.setPreferenceDataStore(connectionsManager.torStore);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// persist changes after setting initial value and enabling
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
connectionsManager.btEnabled().observe(lifecycleOwner, enabled -> {
enableBluetooth.setChecked(enabled);
enableAndPersist(enableBluetooth);
});
connectionsManager.wifiEnabled().observe(lifecycleOwner, enabled -> {
enableWifi.setChecked(enabled);
enableAndPersist(enableWifi);
});
connectionsManager.torEnabled().observe(lifecycleOwner, enabled -> {
enableTor.setChecked(enabled);
enableAndPersist(enableTor);
});
connectionsManager.torNetwork().observe(lifecycleOwner, value -> {
torNetwork.setValue(value);
enableAndPersist(torNetwork);
});
connectionsManager.torMobile().observe(lifecycleOwner, enabled -> {
torMobile.setChecked(enabled);
enableAndPersist(torMobile);
});
connectionsManager.torCharging().observe(lifecycleOwner, enabled -> {
torOnlyWhenCharging.setChecked(enabled);
enableAndPersist(torOnlyWhenCharging);
});
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.network_settings_title);
}
}

View File

@@ -0,0 +1,116 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.SettingsViewModel.BT_NAMESPACE;
import static org.briarproject.briar.android.settings.SettingsViewModel.TOR_NAMESPACE;
import static org.briarproject.briar.android.settings.SettingsViewModel.WIFI_NAMESPACE;
@NotNullByDefault
class ConnectionsManager {
final ConnectionsStore btStore;
final ConnectionsStore wifiStore;
final ConnectionsStore torStore;
private final MutableLiveData<Boolean> btEnabled = new MutableLiveData<>();
private final MutableLiveData<Boolean> wifiEnabled =
new MutableLiveData<>();
private final MutableLiveData<Boolean> torEnabled = new MutableLiveData<>();
private final MutableLiveData<String> torNetwork = new MutableLiveData<>();
private final MutableLiveData<Boolean> torMobile = new MutableLiveData<>();
private final MutableLiveData<Boolean> torCharging =
new MutableLiveData<>();
ConnectionsManager(SettingsManager settingsManager,
Executor dbExecutor) {
btStore =
new ConnectionsStore(settingsManager, dbExecutor, BT_NAMESPACE);
wifiStore = new ConnectionsStore(settingsManager, dbExecutor,
WIFI_NAMESPACE);
torStore = new ConnectionsStore(settingsManager, dbExecutor,
TOR_NAMESPACE);
}
void updateBtSetting(Settings btSettings) {
btEnabled.postValue(btSettings.getBoolean(PREF_PLUGIN_ENABLE,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE));
}
void updateWifiSettings(Settings wifiSettings) {
wifiEnabled.postValue(wifiSettings.getBoolean(PREF_PLUGIN_ENABLE,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE));
}
void updateTorSettings(Settings settings) {
Settings torSettings = migrateTorSettings(settings);
torEnabled.postValue(torSettings.getBoolean(PREF_PLUGIN_ENABLE,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE));
int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
torNetwork.postValue(Integer.toString(torNetworkSetting));
torMobile.postValue(torSettings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE));
torCharging
.postValue(torSettings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING));
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateTorSettings(Settings s) {
int network = s.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
s.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
s.putBoolean(PREF_PLUGIN_ENABLE, false);
// We don't need to save the migrated settings - the Tor plugin is
// responsible for that. This code just handles the case where the
// settings are loaded before the plugin migrates them.
}
return s;
}
LiveData<Boolean> btEnabled() {
return btEnabled;
}
LiveData<Boolean> wifiEnabled() {
return wifiEnabled;
}
LiveData<Boolean> torEnabled() {
return torEnabled;
}
LiveData<String> torNetwork() {
return torNetwork;
}
LiveData<Boolean> torMobile() {
return torMobile;
}
LiveData<Boolean> torCharging() {
return torCharging;
}
}

View File

@@ -0,0 +1,63 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import androidx.annotation.Nullable;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_BLUETOOTH;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_ENABLE;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_MOBILE_DATA;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_NETWORK;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_WIFI;
@NotNullByDefault
class ConnectionsStore extends SettingsStore {
ConnectionsStore(
SettingsManager settingsManager,
Executor dbExecutor,
String namespace) {
super(settingsManager, dbExecutor, namespace);
}
@Override
public void putBoolean(String key, boolean value) {
String newKey;
// translate between Android UI pref keys and bramble keys
switch (key) {
case PREF_KEY_BLUETOOTH:
case PREF_KEY_WIFI:
case PREF_KEY_TOR_ENABLE:
newKey = PREF_PLUGIN_ENABLE;
break;
case PREF_KEY_TOR_MOBILE_DATA:
newKey = PREF_TOR_MOBILE;
break;
case PREF_KEY_TOR_ONLY_WHEN_CHARGING:
newKey = PREF_TOR_ONLY_WHEN_CHARGING;
break;
default:
throw new AssertionError();
}
super.putBoolean(newKey, value);
}
@Override
public void putString(String key, @Nullable String value) {
// translate between Android UI pref keys and bramble keys
if (key.equals(PREF_KEY_TOR_NETWORK)) {
super.putString(PREF_TOR_NETWORK, value);
} else {
throw new AssertionError(key);
}
}
}

View File

@@ -0,0 +1,164 @@
package org.briarproject.briar.android.settings;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.util.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import androidx.core.text.TextUtilsCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.SIGN_OUT_URI;
import static org.briarproject.briar.android.settings.SettingsActivity.EXTRA_THEME_CHANGE;
@NotNullByDefault
public class DisplayFragment extends PreferenceFragmentCompat {
public static final String PREF_LANGUAGE = "pref_key_language";
private static final String PREF_THEME = "pref_key_theme";
private static final Logger LOG =
getLogger(DisplayFragment.class.getName());
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_display);
ListPreference language = requireNonNull(findPreference(PREF_LANGUAGE));
setLanguageEntries(language);
language.setOnPreferenceChangeListener(this::onLanguageChanged);
ListPreference theme = requireNonNull(findPreference(PREF_THEME));
setThemeEntries(theme);
theme.setOnPreferenceChangeListener(this::onThemeChanged);
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.display_settings_title);
}
private void setLanguageEntries(ListPreference language) {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void setThemeEntries(ListPreference theme) {
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0]));
// also remove corresponding value
List<CharSequence> values =
new ArrayList<>(Arrays.asList(theme.getEntryValues()));
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
}
private boolean onThemeChanged(Preference preference, Object newValue) {
// activate new theme
FragmentActivity activity = requireActivity();
UiUtils.setTheme(activity, (String) newValue);
// bring up parent activity, so it can change its theme as well
// upstream bug: https://issuetracker.google.com/issues/38352704
Intent intent = new Intent(getActivity(), ENTRY_ACTIVITY);
intent.setFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// bring this activity back to the foreground
intent = new Intent(getActivity(), activity.getClass());
intent.putExtra(EXTRA_THEME_CHANGE, true);
startActivity(intent);
activity.finish();
return true;
}
private boolean onLanguageChanged(Preference preference, Object newValue) {
ListPreference language = (ListPreference) preference;
if (!language.getValue().equals(newValue)) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.pref_language_title);
builder.setMessage(R.string.pref_language_changed);
builder.setPositiveButton(R.string.sign_out_button, (d, i) -> {
language.setValue((String) newValue);
Intent intent = new Intent(getContext(), ENTRY_ACTIVITY);
intent.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
intent.setData(SIGN_OUT_URI);
requireActivity().startActivity(intent);
requireActivity().finish();
});
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
builder.show();
}
return false;
}
}

View File

@@ -0,0 +1,235 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static android.app.Activity.RESULT_OK;
import static android.media.RingtoneManager.ACTION_RINGTONE_PICKER;
import static android.media.RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class NotificationsFragment extends PreferenceFragmentCompat {
public static final String PREF_NOTIFY_SIGN_IN = "pref_key_notify_sign_in";
private static final int NOTIFICATION_CHANNEL_API = 26;
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private NotificationsManager nm;
private SwitchPreferenceCompat notifyPrivateMessages;
private SwitchPreferenceCompat notifyGroupMessages;
private SwitchPreferenceCompat notifyForumPosts;
private SwitchPreferenceCompat notifyBlogPosts;
private SwitchPreferenceCompat notifyVibration;
private Preference notifySound;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
nm = viewModel.notificationsManager;
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_notifications);
notifyPrivateMessages = findPreference(PREF_NOTIFY_PRIVATE);
notifyGroupMessages = findPreference(PREF_NOTIFY_GROUP);
notifyForumPosts = findPreference(PREF_NOTIFY_FORUM);
notifyBlogPosts = findPreference(PREF_NOTIFY_BLOG);
notifyVibration = findPreference(PREF_NOTIFY_VIBRATION);
notifySound = findPreference(PREF_NOTIFY_SOUND);
if (SDK_INT < NOTIFICATION_CHANNEL_API) {
// NOTIFY_SIGN_IN gets stored in Android's SharedPreferences
notifyPrivateMessages
.setPreferenceDataStore(viewModel.settingsStore);
notifyGroupMessages.setPreferenceDataStore(viewModel.settingsStore);
notifyForumPosts.setPreferenceDataStore(viewModel.settingsStore);
notifyBlogPosts.setPreferenceDataStore(viewModel.settingsStore);
notifyVibration.setPreferenceDataStore(viewModel.settingsStore);
notifySound.setOnPreferenceClickListener(pref ->
onNotificationSoundClicked()
);
} else {
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifySound.setVisible(false);
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (SDK_INT < NOTIFICATION_CHANNEL_API) {
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
nm.getNotifyPrivateMessages().observe(lifecycleOwner, enabled -> {
notifyPrivateMessages.setChecked(enabled);
enableAndPersist(notifyPrivateMessages);
});
nm.getNotifyGroupMessages().observe(lifecycleOwner, enabled -> {
notifyGroupMessages.setChecked(enabled);
enableAndPersist(notifyGroupMessages);
});
nm.getNotifyForumPosts().observe(lifecycleOwner, enabled -> {
notifyForumPosts.setChecked(enabled);
enableAndPersist(notifyForumPosts);
});
nm.getNotifyBlogPosts().observe(lifecycleOwner, enabled -> {
notifyBlogPosts.setChecked(enabled);
enableAndPersist(notifyBlogPosts);
});
nm.getNotifyVibration().observe(lifecycleOwner, enabled -> {
notifyVibration.setChecked(enabled);
enableAndPersist(notifyVibration);
});
nm.getNotifySound().observe(lifecycleOwner, enabled -> {
String text;
if (enabled) {
String ringtoneName = nm.getRingtoneName();
if (isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
notifySound.setEnabled(true);
});
}
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.notification_settings_title);
}
@Override
public void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_RINGTONE && result == RESULT_OK &&
data != null) {
Uri uri = data.getParcelableExtra(EXTRA_RINGTONE_PICKED_URI);
nm.onRingtoneSet(uri);
}
}
@TargetApi(NOTIFICATION_CHANNEL_API)
private void setupNotificationPreference(SwitchPreferenceCompat pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setEnabled(true);
pref.setOnPreferenceClickListener(clickedPref -> {
String packageName = requireContext().getPackageName();
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId);
Context ctx = requireContext();
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (requireNonNull(nm.getNotifySound().getValue())) {
Uri uri;
String ringtoneUri = nm.getRingtoneUri();
if (isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
if (i.resolveActivity(requireActivity().getPackageManager()) != null) {
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true;
}
}

View File

@@ -0,0 +1,158 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.R;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_NAME;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_URI;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class NotificationsManager {
private final static Logger LOG =
getLogger(NotificationsManager.class.getName());
private final Context ctx;
private final SettingsManager settingsManager;
private final Executor dbExecutor;
private final MutableLiveData<Boolean> notifyPrivateMessages =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyGroupMessages =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyForumPosts =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyBlogPosts =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyVibration =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifySound =
new MutableLiveData<>();
private volatile String ringtoneName, ringtoneUri;
NotificationsManager(Context ctx,
SettingsManager settingsManager,
Executor dbExecutor) {
this.ctx = ctx;
this.settingsManager = settingsManager;
this.dbExecutor = dbExecutor;
}
void updateSettings(Settings settings) {
notifyPrivateMessages.postValue(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.postValue(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.postValue(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.postValue(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.postValue(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME);
ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
notifySound.postValue(settings.getBoolean(PREF_NOTIFY_SOUND, true));
}
void onRingtoneSet(@Nullable Uri uri) {
Settings s = new Settings();
if (uri == null) {
// The user chose silence
s.putBoolean(PREF_NOTIFY_SOUND, false);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else if (RingtoneManager.isDefault(uri)) {
// The user chose the default
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(ctx, uri);
if (r == null || "file".equals(uri.getScheme())) {
Toast.makeText(ctx, R.string.cannot_load_ringtone, LENGTH_SHORT)
.show();
} else {
String name = r.getTitle(ctx);
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
}
}
dbExecutor.execute(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, SETTINGS_NAMESPACE);
logDuration(LOG, "Merging notification settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<Boolean> getNotifyPrivateMessages() {
return notifyPrivateMessages;
}
LiveData<Boolean> getNotifyGroupMessages() {
return notifyGroupMessages;
}
LiveData<Boolean> getNotifyForumPosts() {
return notifyForumPosts;
}
LiveData<Boolean> getNotifyBlogPosts() {
return notifyBlogPosts;
}
LiveData<Boolean> getNotifyVibration() {
return notifyVibration;
}
@NonNull
LiveData<Boolean> getNotifySound() {
return notifySound;
}
String getRingtoneName() {
return ringtoneName;
}
String getRingtoneUri() {
return ringtoneUri;
}
}

View File

@@ -0,0 +1,112 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SecurityFragment extends PreferenceFragmentCompat {
public static final String PREF_SCREEN_LOCK = "pref_key_lock";
public static final String PREF_SCREEN_LOCK_TIMEOUT =
"pref_key_lock_timeout";
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private SwitchPreferenceCompat screenLock;
private ListPreference screenLockTimeout;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_security);
getPreferenceManager().setPreferenceDataStore(viewModel.settingsStore);
screenLock = findPreference(PREF_SCREEN_LOCK);
screenLockTimeout =
requireNonNull(findPreference(PREF_SCREEN_LOCK_TIMEOUT));
screenLockTimeout.setSummaryProvider(preference -> {
CharSequence timeout = screenLockTimeout.getValue();
String never = getString(R.string.pref_lock_timeout_value_never);
if (timeout.equals(never)) {
return getString(R.string.pref_lock_timeout_never_summary);
} else {
return getString(R.string.pref_lock_timeout_summary,
screenLockTimeout.getEntry());
}
});
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (SDK_INT < 21) {
screenLock.setVisible(false);
screenLockTimeout.setVisible(false);
} else {
// timeout depends on screenLock and gets disabled automatically
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel.getScreenLockTimeout().observe(lifecycleOwner, value -> {
screenLockTimeout.setValue(value);
enableAndPersist(screenLockTimeout);
});
}
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.security_settings_title);
checkScreenLock();
}
private void checkScreenLock() {
if (SDK_INT < 21) return;
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel.getScreenLockEnabled().removeObservers(lifecycleOwner);
if (hasScreenLock(requireActivity())) {
viewModel.getScreenLockEnabled().observe(lifecycleOwner, on -> {
screenLock.setChecked(on);
enableAndPersist(screenLock);
});
screenLock.setSummary(R.string.pref_lock_summary);
} else {
screenLock.setEnabled(false);
screenLock.setPersistent(false);
screenLock.setChecked(false);
screenLock.setSummary(R.string.pref_lock_disabled_summary);
}
}
}

View File

@@ -1,41 +1,37 @@
package org.briarproject.briar.android.settings;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.view.AuthorView;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProvider;
import de.hdodenhof.circleimageview.CircleImageView;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_AVATAR_IMAGE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsActivity extends BriarActivity
implements OnPreferenceStartFragmentCallback {
public class SettingsActivity extends BriarActivity {
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel settingsViewModel;
@Inject
FeatureFlags featureFlags;
static final String EXTRA_THEME_CHANGE = "themeChange";
@Override
public void onCreate(Bundle bundle) {
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
ActionBar actionBar = getSupportActionBar();
@@ -44,43 +40,15 @@ public class SettingsActivity extends BriarActivity {
actionBar.setDisplayHomeAsUpEnabled(true);
}
setContentView(R.layout.activity_settings);
if (featureFlags.shouldEnableProfilePictures()) {
ViewModelProvider provider =
new ViewModelProvider(this, viewModelFactory);
settingsViewModel = provider.get(SettingsViewModel.class);
TextView textViewUserName = findViewById(R.id.username);
CircleImageView imageViewAvatar =
findViewById(R.id.avatarImage);
settingsViewModel.getOwnIdentityInfo().observe(this, us -> {
textViewUserName.setText(us.getLocalAuthor().getName());
AuthorView.setAvatar(imageViewAvatar,
us.getLocalAuthor().getId(), us.getAuthorInfo());
});
settingsViewModel.getSetAvatarFailed()
.observeEvent(this, failed -> {
if (failed) {
Toast.makeText(this,
R.string.change_profile_picture_failed_message,
LENGTH_LONG).show();
}
});
View avatarGroup = findViewById(R.id.avatarGroup);
avatarGroup.setOnClickListener(e -> selectAvatarImage());
} else {
View view = findViewById(R.id.avatarGroup);
view.setVisibility(View.GONE);
// show display fragment after theme change
Bundle extras = getIntent().getExtras();
if (bundle == null && extras != null &&
extras.getBoolean(EXTRA_THEME_CHANGE, false)) {
FragmentManager fragmentManager = getSupportFragmentManager();
showNextFragment(fragmentManager, new DisplayFragment());
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
setContentView(R.layout.activity_settings);
}
@Override
@@ -92,30 +60,40 @@ public class SettingsActivity extends BriarActivity {
return false;
}
private void selectAvatarImage() {
Intent intent = UiUtils.createSelectImageIntent(false);
startActivityForResult(intent, REQUEST_AVATAR_IMAGE);
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller,
Preference pref) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentFactory fragmentFactory = fragmentManager.getFragmentFactory();
Fragment fragment = fragmentFactory
.instantiate(getClassLoader(), pref.getFragment());
fragment.setTargetFragment(caller, 0);
// Replace the existing Fragment with the new Fragment
showNextFragment(fragmentManager, fragment);
return true;
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
private void showNextFragment(FragmentManager fragmentManager, Fragment f) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in,
R.anim.step_next_out)
.replace(R.id.fragmentContainer, f)
.addToBackStack(null)
.commit();
}
if (request == REQUEST_AVATAR_IMAGE && result == RESULT_OK) {
onAvatarImageReceived(data);
/**
* If the preference is not yet enabled, this enables the preference
* and makes it persist changed values.
* Call this after setting the initial value
* to prevent this change from getting persisted in the DB unnecessarily.
*/
static void enableAndPersist(Preference pref) {
if (!pref.isEnabled()) {
pref.setEnabled(true);
pref.setPersistent(true);
}
}
private void onAvatarImageReceived(@Nullable Intent resultData) {
if (resultData == null) return;
Uri uri = resultData.getData();
if (uri == null) return;
ConfirmAvatarDialogFragment dialog =
ConfirmAvatarDialogFragment.newInstance(uri);
dialog.show(getSupportFragmentManager(),
ConfirmAvatarDialogFragment.TAG);
}
}

View File

@@ -1,761 +1,120 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.util.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.core.text.TextUtilsCompat;
import androidx.preference.ListPreference;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
import androidx.preference.SwitchPreference;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.media.RingtoneManager.ACTION_RINGTONE_PICKER;
import static android.media.RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.SIGN_OUT_URI;
import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_AVATAR_IMAGE;
import static org.briarproject.briar.android.util.UiUtils.createSelectImageIntent;
import static org.briarproject.briar.android.util.UiUtils.triggerFeedback;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_NAME;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_URI;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsFragment extends PreferenceFragmentCompat
implements EventListener, OnPreferenceChangeListener {
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String SETTINGS_NAMESPACE = "android-ui";
public static final String LANGUAGE = "pref_key_language";
public static final String PREF_SCREEN_LOCK = "pref_key_lock";
public static final String PREF_SCREEN_LOCK_TIMEOUT =
"pref_key_lock_timeout";
public static final String NOTIFY_SIGN_IN = "pref_key_notify_sign_in";
private static final String BT_NAMESPACE =
BluetoothConstants.ID.getString();
private static final String BT_ENABLE = "pref_key_bluetooth";
private static final String WIFI_NAMESPACE = LanTcpConstants.ID.getString();
private static final String WIFI_ENABLE = "pref_key_wifi";
private static final String TOR_NAMESPACE = TorConstants.ID.getString();
private static final String TOR_ENABLE = "pref_key_tor_enable";
private static final String TOR_NETWORK = "pref_key_tor_network";
private static final String TOR_MOBILE = "pref_key_tor_mobile_data";
private static final String TOR_ONLY_WHEN_CHARGING =
"pref_key_tor_only_when_charging";
private static final Logger LOG =
Logger.getLogger(SettingsFragment.class.getName());
private SettingsActivity listener;
private ListPreference language;
private SwitchPreference enableBluetooth;
private SwitchPreference enableWifi;
private SwitchPreference enableTor;
private ListPreference torNetwork;
private SwitchPreference torMobile;
private SwitchPreference torOnlyWhenCharging;
private SwitchPreference screenLock;
private ListPreference screenLockTimeout;
private SwitchPreference notifyPrivateMessages;
private SwitchPreference notifyGroupMessages;
private SwitchPreference notifyForumPosts;
private SwitchPreference notifyBlogPosts;
private SwitchPreference notifyVibration;
private Preference notifySound;
// Fields that are accessed from background threads must be volatile
private volatile Settings settings, btSettings, wifiSettings, torSettings;
private volatile boolean settingsLoaded = false;
private static final String PREF_KEY_AVATAR = "pref_key_avatar";
private static final String PREF_KEY_FEEDBACK = "pref_key_send_feedback";
private static final String PREF_KEY_DEV = "pref_key_dev";
private static final String PREF_KEY_EXPLODE = "pref_key_explode";
@Inject
volatile SettingsManager settingsManager;
@Inject
volatile EventBus eventBus;
@Inject
LocationUtils locationUtils;
@Inject
CircumventionProvider circumventionProvider;
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private AvatarPreference prefAvatar;
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
listener = (SettingsActivity) context;
listener.getActivityComponent().inject(this);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings);
language = findPreference(LANGUAGE);
setLanguageEntries();
ListPreference theme = findPreference("pref_key_theme");
enableBluetooth = findPreference(BT_ENABLE);
enableWifi = findPreference(WIFI_ENABLE);
enableTor = findPreference(TOR_ENABLE);
torNetwork = findPreference(TOR_NETWORK);
torMobile = findPreference(TOR_MOBILE);
torOnlyWhenCharging = findPreference(TOR_ONLY_WHEN_CHARGING);
screenLock = findPreference(PREF_SCREEN_LOCK);
screenLockTimeout = findPreference(PREF_SCREEN_LOCK_TIMEOUT);
notifyPrivateMessages =
findPreference("pref_key_notify_private_messages");
notifyGroupMessages = findPreference("pref_key_notify_group_messages");
notifyForumPosts = findPreference("pref_key_notify_forum_posts");
notifyBlogPosts = findPreference("pref_key_notify_blog_posts");
notifyVibration = findPreference("pref_key_notify_vibration");
notifySound = findPreference("pref_key_notify_sound");
language.setOnPreferenceChangeListener(this);
theme.setOnPreferenceChangeListener((preference, newValue) -> {
if (getActivity() != null) {
// activate new theme
UiUtils.setTheme(getActivity(), (String) newValue);
// bring up parent activity, so it can change its theme as well
// upstream bug: https://issuetracker.google.com/issues/38352704
Intent intent = new Intent(getActivity(), ENTRY_ACTIVITY);
intent.setFlags(
FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// bring this activity back to the foreground
intent = new Intent(getActivity(), getActivity().getClass());
startActivity(intent);
getActivity().finish();
}
return true;
});
enableBluetooth.setOnPreferenceChangeListener(this);
enableWifi.setOnPreferenceChangeListener(this);
enableTor.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
torMobile.setOnPreferenceChangeListener(this);
torOnlyWhenCharging.setOnPreferenceChangeListener(this);
screenLock.setOnPreferenceChangeListener(this);
screenLockTimeout.setOnPreferenceChangeListener(this);
prefAvatar = requireNonNull(findPreference(PREF_KEY_AVATAR));
if (viewModel.shouldEnableProfilePictures()) {
prefAvatar.setOnPreferenceClickListener(preference -> {
Intent intent = createSelectImageIntent(false);
startActivityForResult(intent, REQUEST_AVATAR_IMAGE);
return true;
});
} else {
prefAvatar.setVisible(false);
}
Preference prefFeedback =
requireNonNull(findPreference("pref_key_send_feedback"));
requireNonNull(findPreference(PREF_KEY_FEEDBACK));
prefFeedback.setOnPreferenceClickListener(preference -> {
triggerFeedback(requireContext());
return true;
});
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0]));
// also remove corresponding value
List<CharSequence> values =
new ArrayList<>(Arrays.asList(theme.getEntryValues()));
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
Preference explode = requireNonNull(findPreference("pref_key_explode"));
Preference explode = requireNonNull(findPreference(PREF_KEY_EXPLODE));
if (IS_DEBUG_BUILD) {
explode.setOnPreferenceClickListener(preference -> {
throw new RuntimeException("Boom!");
});
} else {
explode.setVisible(false);
findPreference("pref_key_test_data").setVisible(false);
PreferenceGroup testing = explode.getParent();
if (testing == null) throw new AssertionError();
testing.setVisible(false);
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
dev.setVisible(false);
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
ColorDrawable divider = new ColorDrawable(
ContextCompat.getColor(requireContext(), R.color.divider));
setDivider(divider);
return view;
super.onViewCreated(view, savedInstanceState);
viewModel.getOwnIdentityInfo().observe(getViewLifecycleOwner(), us ->
prefAvatar.setOwnIdentityInfo(us)
);
}
@Override
public void onStart() {
super.onStart();
eventBus.addListener(this);
setSettingsEnabled(false);
loadSettings();
requireActivity().setTitle(R.string.settings_button);
}
@Override
public void onStop() {
super.onStop();
eventBus.removeListener(this);
}
private void setLanguageEntries() {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void setTorNetworkSummary(int torNetworkSetting) {
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {
torNetwork.setSummary("%s"); // use setting value
return;
}
// Look up country name in the user's chosen language if available
String country = locationUtils.getCurrentCountry();
String countryName = getCountryDisplayName(country);
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
boolean useBridges = circumventionProvider.doBridgesWork(country);
String setting =
getString(R.string.tor_network_setting_without_bridges);
if (blocked && useBridges) {
setting = getString(R.string.tor_network_setting_with_bridges);
} else if (blocked) {
setting = getString(R.string.tor_network_setting_never);
}
torNetwork.setSummary(
getString(R.string.tor_network_setting_summary, setting,
countryName));
}
private void loadSettings() {
listener.runOnDbThread(() -> {
try {
long start = now();
settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
btSettings = settingsManager.getSettings(BT_NAMESPACE);
wifiSettings = settingsManager.getSettings(WIFI_NAMESPACE);
torSettings = settingsManager.getSettings(TOR_NAMESPACE);
settingsLoaded = true;
logDuration(LOG, "Loading settings", start);
displaySettings();
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateTorSettings(Settings s) {
int network = s.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
s.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
s.putBoolean(PREF_PLUGIN_ENABLE, false);
// We don't need to save the migrated settings - the Tor plugin is
// responsible for that. This code just handles the case where the
// settings are loaded before the plugin migrates them.
}
return s;
}
private void displaySettings() {
listener.runOnUiThreadUnlessDestroyed(() -> {
// due to events, we might try to display before a load completed
if (!settingsLoaded) return;
boolean btEnabledSetting = btSettings.getBoolean(PREF_PLUGIN_ENABLE,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableBluetooth.setChecked(btEnabledSetting);
boolean wifiEnabledSetting =
wifiSettings.getBoolean(PREF_PLUGIN_ENABLE,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableWifi.setChecked(wifiEnabledSetting);
boolean torEnabledSetting =
torSettings.getBoolean(PREF_PLUGIN_ENABLE,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableTor.setChecked(torEnabledSetting);
int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
torNetwork.setValue(Integer.toString(torNetworkSetting));
setTorNetworkSummary(torNetworkSetting);
boolean torMobileSetting = torSettings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE);
torMobile.setChecked(torMobileSetting);
boolean torChargingSetting =
torSettings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING);
torOnlyWhenCharging.setChecked(torChargingSetting);
displayScreenLockSetting();
if (SDK_INT < 26) {
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
notifySound.setOnPreferenceClickListener(
pref -> onNotificationSoundClicked());
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName =
settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
} else {
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifySound.setVisible(false);
}
setSettingsEnabled(true);
});
}
private void setSettingsEnabled(boolean enabled) {
// preferences not needed here, because handled by SharedPreferences:
// - pref_key_theme
// - pref_key_notify_sign_in
// preferences partly needed here, because they have their own logic
// - pref_key_lock (screenLock -> displayScreenLockSetting())
// - pref_key_lock_timeout (screenLockTimeout)
enableBluetooth.setEnabled(enabled);
enableWifi.setEnabled(enabled);
enableTor.setEnabled(enabled);
torNetwork.setEnabled(enabled);
torMobile.setEnabled(enabled);
torOnlyWhenCharging.setEnabled(enabled);
if (!enabled) screenLock.setEnabled(false);
notifyPrivateMessages.setEnabled(enabled);
notifyGroupMessages.setEnabled(enabled);
notifyForumPosts.setEnabled(enabled);
notifyBlogPosts.setEnabled(enabled);
notifyVibration.setEnabled(enabled);
notifySound.setEnabled(enabled);
}
private void displayScreenLockSetting() {
if (SDK_INT < 21) {
screenLock.setVisible(false);
screenLockTimeout.setVisible(false);
} else {
if (getActivity() != null && hasScreenLock(getActivity())) {
screenLock.setEnabled(true);
screenLock.setChecked(
settings.getBoolean(PREF_SCREEN_LOCK, false));
screenLock.setSummary(R.string.pref_lock_summary);
} else {
screenLock.setEnabled(false);
screenLock.setChecked(false);
screenLock.setSummary(R.string.pref_lock_disabled_summary);
}
// timeout depends on screenLock and gets disabled automatically
int timeout = settings.getInt(PREF_SCREEN_LOCK_TIMEOUT,
Integer.valueOf(getString(
R.string.pref_lock_timeout_value_default)));
String newValue = String.valueOf(timeout);
screenLockTimeout.setValue(newValue);
setScreenLockTimeoutSummary(newValue);
}
}
private void setScreenLockTimeoutSummary(String timeout) {
String never = getString(R.string.pref_lock_timeout_value_never);
if (timeout.equals(never)) {
screenLockTimeout
.setSummary(R.string.pref_lock_timeout_never_summary);
} else {
screenLockTimeout
.setSummary(R.string.pref_lock_timeout_summary);
}
}
@TargetApi(26)
private void setupNotificationPreference(SwitchPreference pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setOnPreferenceClickListener(clickedPref -> {
String packageName = requireContext().getPackageName();
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId);
Context ctx = requireContext();
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri =
settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
if (i.resolveActivity(requireActivity().getPackageManager()) != null) {
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference == language) {
if (!language.getValue().equals(newValue))
languageChanged((String) newValue);
return false;
} else if (preference == enableBluetooth) {
boolean btSetting = (Boolean) newValue;
storeBluetoothSetting(btSetting);
} else if (preference == enableWifi) {
boolean wifiSetting = (Boolean) newValue;
storeWifiSetting(wifiSetting);
} else if (preference == enableTor) {
boolean torEnabledSetting = (Boolean) newValue;
storeTorEnabledSetting(torEnabledSetting);
} else if (preference == torNetwork) {
int torNetworkSetting = Integer.valueOf((String) newValue);
storeTorNetworkSetting(torNetworkSetting);
setTorNetworkSummary(torNetworkSetting);
} else if (preference == torMobile) {
boolean torMobileSetting = (Boolean) newValue;
storeTorMobileSetting(torMobileSetting);
} else if (preference == torOnlyWhenCharging) {
boolean torChargingSetting = (Boolean) newValue;
storeTorChargingSetting(torChargingSetting);
} else if (preference == screenLock) {
Settings s = new Settings();
s.putBoolean(PREF_SCREEN_LOCK, (Boolean) newValue);
storeSettings(s);
} else if (preference == screenLockTimeout) {
Settings s = new Settings();
String value = (String) newValue;
s.putInt(PREF_SCREEN_LOCK_TIMEOUT, Integer.valueOf(value));
storeSettings(s);
setScreenLockTimeoutSummary(value);
} else if (preference == notifyPrivateMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyGroupMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyForumPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyBlogPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyVibration) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) newValue);
storeSettings(s);
}
return true;
}
private void languageChanged(String newValue) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.pref_language_title);
builder.setMessage(R.string.pref_language_changed);
builder.setPositiveButton(R.string.sign_out_button,
(dialogInterface, i) -> {
language.setValue(newValue);
Intent intent = new Intent(getContext(), ENTRY_ACTIVITY);
intent.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
intent.setData(SIGN_OUT_URI);
requireActivity().startActivity(intent);
requireActivity().finish();
});
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
builder.show();
}
private void storeTorEnabledSetting(boolean torEnabledSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, torEnabledSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorNetworkSetting(int torNetworkSetting) {
Settings s = new Settings();
s.putInt(PREF_TOR_NETWORK, torNetworkSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorMobileSetting(boolean torMobileSetting) {
Settings s = new Settings();
s.putBoolean(PREF_TOR_MOBILE, torMobileSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorChargingSetting(boolean torChargingSetting) {
Settings s = new Settings();
s.putBoolean(PREF_TOR_ONLY_WHEN_CHARGING, torChargingSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeBluetoothSetting(boolean btSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, btSetting);
mergeSettings(s, BT_NAMESPACE);
}
private void storeWifiSetting(boolean wifiSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, wifiSetting);
mergeSettings(s, WIFI_NAMESPACE);
}
private void storeSettings(Settings s) {
mergeSettings(s, SETTINGS_NAMESPACE);
}
private void mergeSettings(Settings s, String namespace) {
listener.runOnDbThread(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@Override
public void onActivityResult(int request, int result, Intent data) {
public void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_RINGTONE && result == RESULT_OK) {
Settings s = new Settings();
Uri uri = data.getParcelableExtra(EXTRA_RINGTONE_PICKED_URI);
if (uri == null) {
// The user chose silence
s.putBoolean(PREF_NOTIFY_SOUND, false);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else if (RingtoneManager.isDefault(uri)) {
// The user chose the default
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
if (r == null || "file".equals(uri.getScheme())) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
} else {
String name = r.getTitle(getContext());
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
}
}
storeSettings(s);
}
}
if (request == REQUEST_AVATAR_IMAGE && result == RESULT_OK) {
if (data == null) return;
Uri uri = data.getData();
if (uri == null) return;
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
String namespace = s.getNamespace();
if (namespace.equals(SETTINGS_NAMESPACE)) {
LOG.info("Settings updated");
settings = s.getSettings();
displaySettings();
} else if (namespace.equals(BT_NAMESPACE)) {
LOG.info("Bluetooth settings updated");
btSettings = s.getSettings();
displaySettings();
} else if (namespace.equals(WIFI_NAMESPACE)) {
LOG.info("Wifi settings updated");
wifiSettings = s.getSettings();
displaySettings();
} else if (namespace.equals(TOR_NAMESPACE)) {
LOG.info("Tor settings updated");
torSettings = migrateTorSettings(s.getSettings());
displaySettings();
}
DialogFragment dialog =
ConfirmAvatarDialogFragment.newInstance(uri);
dialog.show(getParentFragmentManager(),
ConfirmAvatarDialogFragment.TAG);
}
}

View File

@@ -0,0 +1,80 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceDataStore;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
/**
* A custom PreferenceDataStore that stores settings in Briar's encrypted DB.
*/
@NotNullByDefault
class SettingsStore extends PreferenceDataStore {
private final static Logger LOG = getLogger(SettingsStore.class.getName());
private final SettingsManager settingsManager;
private final Executor dbExecutor;
private final String namespace;
SettingsStore(SettingsManager settingsManager,
Executor dbExecutor,
String namespace) {
this.settingsManager = settingsManager;
this.dbExecutor = dbExecutor;
this.namespace = namespace;
}
@Override
public void putBoolean(String key, boolean value) {
if (LOG.isLoggable(INFO))
LOG.info("Store bool setting: " + key + "=" + value);
Settings s = new Settings();
s.putBoolean(key, value);
storeSettings(s);
}
@Override
public void putInt(String key, int value) {
if (LOG.isLoggable(INFO))
LOG.info("Store int setting: " + key + "=" + value);
Settings s = new Settings();
s.putInt(key, value);
storeSettings(s);
}
@Override
public void putString(String key, @Nullable String value) {
if (LOG.isLoggable(INFO))
LOG.info("Store string setting: " + key + "=" + value);
Settings s = new Settings();
s.put(key, value);
storeSettings(s);
}
private void storeSettings(Settings s) {
dbExecutor.execute(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging " + namespace + " settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
}

View File

@@ -3,17 +3,34 @@ package org.briarproject.briar.android.settings;
import android.app.Application;
import android.content.ContentResolver;
import android.net.Uri;
import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.UnsupportedMimeTypeException;
import org.briarproject.briar.android.attachment.media.ImageCompressor;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.api.avatar.AvatarManager;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
@@ -25,66 +42,127 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.lifecycle.AndroidViewModel;
import androidx.annotation.AnyThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.Arrays.asList;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@NotNullByDefault
class SettingsViewModel extends AndroidViewModel {
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class SettingsViewModel extends DbViewModel implements EventListener {
private final static Logger LOG =
getLogger(SettingsViewModel.class.getName());
static final String BT_NAMESPACE =
BluetoothConstants.ID.getString();
static final String WIFI_NAMESPACE = LanTcpConstants.ID.getString();
static final String TOR_NAMESPACE = TorConstants.ID.getString();
private final SettingsManager settingsManager;
private final IdentityManager identityManager;
private final EventBus eventBus;
private final AvatarManager avatarManager;
private final AuthorManager authorManager;
private final ImageCompressor imageCompressor;
@IoExecutor
private final Executor ioExecutor;
@DatabaseExecutor
private final Executor dbExecutor;
private final FeatureFlags featureFlags;
final SettingsStore settingsStore;
final TorSummaryProvider torSummaryProvider;
final ConnectionsManager connectionsManager;
final NotificationsManager notificationsManager;
private volatile Settings settings;
private final MutableLiveData<OwnIdentityInfo> ownIdentityInfo =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> setAvatarFailed =
new MutableLiveEvent<>();
private final MutableLiveData<Boolean> screenLockEnabled =
new MutableLiveData<>();
private final MutableLiveData<String> screenLockTimeout =
new MutableLiveData<>();
@Inject
SettingsViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
SettingsManager settingsManager,
IdentityManager identityManager,
EventBus eventBus,
AvatarManager avatarManager,
AuthorManager authorManager,
ImageCompressor imageCompressor,
LocationUtils locationUtils,
CircumventionProvider circumventionProvider,
@IoExecutor Executor ioExecutor,
@DatabaseExecutor Executor dbExecutor) {
super(application);
FeatureFlags featureFlags) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.settingsManager = settingsManager;
this.identityManager = identityManager;
this.eventBus = eventBus;
this.imageCompressor = imageCompressor;
this.avatarManager = avatarManager;
this.authorManager = authorManager;
this.ioExecutor = ioExecutor;
this.dbExecutor = dbExecutor;
this.featureFlags = featureFlags;
settingsStore = new SettingsStore(settingsManager, dbExecutor,
SETTINGS_NAMESPACE);
torSummaryProvider = new TorSummaryProvider(getApplication(),
locationUtils, circumventionProvider);
connectionsManager =
new ConnectionsManager(settingsManager, dbExecutor);
notificationsManager = new NotificationsManager(getApplication(),
settingsManager, dbExecutor);
loadOwnIdentityInfo();
eventBus.addListener(this);
loadSettings();
if (shouldEnableProfilePictures()) loadOwnIdentityInfo();
}
LiveData<OwnIdentityInfo> getOwnIdentityInfo() {
return ownIdentityInfo;
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
LiveEvent<Boolean> getSetAvatarFailed() {
return setAvatarFailed;
private void loadSettings() {
runOnDbThread(() -> {
try {
long start = now();
settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
updateSettings(settings);
connectionsManager.updateBtSetting(
settingsManager.getSettings(BT_NAMESPACE));
connectionsManager.updateWifiSettings(
settingsManager.getSettings(WIFI_NAMESPACE));
connectionsManager.updateTorSettings(
settingsManager.getSettings(TOR_NAMESPACE));
logDuration(LOG, "Loading settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
boolean shouldEnableProfilePictures() {
return featureFlags.shouldEnableProfilePictures();
}
private void loadOwnIdentityInfo() {
dbExecutor.execute(() -> {
runOnDbThread(() -> {
try {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
AuthorInfo authorInfo = authorManager.getMyAuthorInfo();
@@ -96,13 +174,47 @@ class SettingsViewModel extends AndroidViewModel {
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
String namespace = s.getNamespace();
if (namespace.equals(SETTINGS_NAMESPACE)) {
LOG.info("Settings updated");
settings = s.getSettings();
updateSettings(settings);
} else if (namespace.equals(BT_NAMESPACE)) {
LOG.info("Bluetooth settings updated");
connectionsManager.updateBtSetting(s.getSettings());
} else if (namespace.equals(WIFI_NAMESPACE)) {
LOG.info("Wifi settings updated");
connectionsManager.updateWifiSettings(s.getSettings());
} else if (namespace.equals(TOR_NAMESPACE)) {
LOG.info("Tor settings updated");
connectionsManager.updateTorSettings(s.getSettings());
}
}
}
@AnyThread
private void updateSettings(Settings settings) {
screenLockEnabled.postValue(settings.getBoolean(PREF_SCREEN_LOCK,
false));
int defaultTimeout = Integer.parseInt(getApplication()
.getString(R.string.pref_lock_timeout_value_default));
screenLockTimeout.postValue(String.valueOf(
settings.getInt(PREF_SCREEN_LOCK_TIMEOUT, defaultTimeout)
));
notificationsManager.updateSettings(settings);
}
void setAvatar(Uri uri) {
ioExecutor.execute(() -> {
try {
trySetAvatar(uri);
} catch (IOException e) {
logException(LOG, WARNING, e);
setAvatarFailed.postEvent(true);
onSetAvatarFailed();
}
});
}
@@ -120,15 +232,34 @@ class SettingsViewModel extends AndroidViewModel {
"ContentResolver returned null when opening InputStream");
InputStream compressed = imageCompressor.compressImage(is, contentType);
dbExecutor.execute(() -> {
runOnDbThread(() -> {
try {
avatarManager.addAvatar(ImageCompressor.MIME_TYPE, compressed);
loadOwnIdentityInfo();
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
setAvatarFailed.postEvent(true);
onSetAvatarFailed();
}
});
}
@AnyThread
private void onSetAvatarFailed() {
androidExecutor.runOnUiThread(() -> Toast.makeText(getApplication(),
R.string.change_profile_picture_failed_message, LENGTH_LONG)
.show());
}
LiveData<OwnIdentityInfo> getOwnIdentityInfo() {
return ownIdentityInfo;
}
LiveData<Boolean> getScreenLockEnabled() {
return screenLockEnabled;
}
LiveData<String> getScreenLockTimeout() {
return screenLockTimeout;
}
}

View File

@@ -0,0 +1,57 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.R;
import androidx.preference.ListPreference;
import androidx.preference.Preference.SummaryProvider;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName;
@NotNullByDefault
class TorSummaryProvider implements SummaryProvider<ListPreference> {
private final Context ctx;
private final LocationUtils locationUtils;
private final CircumventionProvider circumventionProvider;
TorSummaryProvider(Context ctx,
LocationUtils locationUtils,
CircumventionProvider circumventionProvider) {
this.ctx = ctx;
this.locationUtils = locationUtils;
this.circumventionProvider = circumventionProvider;
}
@Override
public CharSequence provideSummary(ListPreference preference) {
int torNetworkSetting = Integer.parseInt(preference.getValue());
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {
return preference.getEntry(); // use setting value
}
// Look up country name in the user's chosen language if available
String country = locationUtils.getCurrentCountry();
String countryName = getCountryDisplayName(country);
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
boolean useBridges = circumventionProvider.doBridgesWork(country);
String setting =
ctx.getString(R.string.tor_network_setting_without_bridges);
if (blocked && useBridges) {
setting = ctx.getString(R.string.tor_network_setting_with_bridges);
} else if (blocked) {
setting = ctx.getString(R.string.tor_network_setting_never);
}
return ctx.getString(R.string.tor_network_setting_summary, setting,
countryName);
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M11,14H9c0,-4.97 4.03,-9 9,-9v2C14.13,7 11,10.13 11,14zM18,11V9c-2.76,0 -5,2.24 -5,5h2C15,12.34 16.34,11 18,11zM7,4c0,-1.11 -0.89,-2 -2,-2S3,2.89 3,4s0.89,2 2,2S7,5.11 7,4zM11.45,4.5h-2C9.21,5.92 7.99,7 6.5,7h-3C2.67,7 2,7.67 2,8.5V11h6V8.74C9.86,8.15 11.25,6.51 11.45,4.5zM19,17c1.11,0 2,-0.89 2,-2s-0.89,-2 -2,-2s-2,0.89 -2,2S17.89,17 19,17zM20.5,18h-3c-1.49,0 -2.71,-1.08 -2.95,-2.5h-2c0.2,2.01 1.59,3.65 3.45,4.24V22h6v-2.5C22,18.67 21.33,18 20.5,18z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM13,14h-2v-2h2v2zM13,10h-2L11,6h2v4z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02zM8,16h2.5l1.5,1.5 1.5,-1.5L16,16v-2.5l1.5,-1.5 -1.5,-1.5L16,8h-2.5L12,6.5 10.5,8L8,8v2.5L6.5,12 8,13.5L8,16zM12,9c1.66,0 3,1.34 3,3s-1.34,3 -3,3L12,9z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
tools:ignore="NewApi">
<path
android:fillColor="#FF000000"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View File

@@ -1,78 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragmentContainer"
android:name="org.briarproject.briar.android.settings.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatarGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarImage"
style="@style/BriarAvatar"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/avatarExplanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="username" />
<TextView
android:id="@+id/avatarExplanation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:text="@string/change_profile_picture"
android:textColor="@color/briar_text_secondary_inverse"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toBottomOf="@+id/username" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/fragment"
android:name="org.briarproject.briar.android.settings.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

View File

@@ -44,7 +44,6 @@
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"
android:widgetLayout="@layout/preference_switch_compat"
tools:checked="true"
tools:text="@string/tor_enable_title" />

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_width="800dp"
tools:layout_height="75dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatarGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarImage"
style="@style/BriarAvatar"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/avatarExplanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="username" />
<TextView
android:id="@+id/avatarExplanation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:text="@string/change_profile_picture"
android:textColor="@color/briar_text_secondary_inverse"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toBottomOf="@+id/username" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Needed for SwitchPreference on Android 4 (API < 21)-->
<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
tools:targetApi="n" />

View File

@@ -468,7 +468,7 @@
<string name="pref_theme_light">Light</string>
<string name="pref_theme_dark">Dark</string>
<string name="pref_theme_auto">Automatic (Daytime)</string>
<string name="pref_theme_system">System Default</string>
<string name="pref_theme_system">System default</string>
<!-- Settings Connections -->
<string name="network_settings_title">Connections</string>
@@ -552,7 +552,6 @@
<string name="cannot_load_ringtone">Cannot load ringtone</string>
<!-- Settings Feedback -->
<string name="feedback_settings_title">Feedback</string>
<string name="send_feedback">Send feedback</string>
<!-- Link Warning -->

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_key_lock"
android:summary="@string/panic_setting_signout_summary"
android:title="@string/panic_setting_signout_title"
app:iconSpaceReserved="false"/>
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<PreferenceCategory
android:layout="@layout/preferences_category"
@@ -18,15 +18,16 @@
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:key="pref_key_panic_app"
android:summary="@string/panic_app_setting_summary"
android:title="@string/panic_app_setting_title"/>
android:title="@string/panic_app_setting_title" />
<SwitchPreference
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="false"
android:key="pref_key_purge"
android:summary="@string/purge_setting_summary"
android:title="@string/purge_setting_title"
app:iconSpaceReserved="false"/>
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
</PreferenceCategory>

View File

@@ -1,236 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/display_settings_title">
<ListPreference
android:defaultValue="default"
android:entryValues="@array/pref_language_values"
android:key="pref_key_language"
android:summary="%s"
android:title="@string/pref_language_title"
app:iconSpaceReserved="false"/>
<org.briarproject.briar.android.settings.AvatarPreference android:key="pref_key_avatar" />
<ListPreference
android:defaultValue="@string/pref_theme_light_value"
android:entries="@array/pref_theme_entries"
android:entryValues="@array/pref_theme_values"
android:key="pref_key_theme"
android:summary="%s"
android:title="@string/pref_theme_title"
app:iconSpaceReserved="false"/>
<Preference
android:title="@string/display_settings_title"
app:fragment="org.briarproject.briar.android.settings.DisplayFragment"
app:icon="@drawable/ic_settings_brightness" />
</PreferenceCategory>
<Preference
android:title="@string/network_settings_title"
app:fragment="org.briarproject.briar.android.settings.ConnectionsFragment"
app:icon="@drawable/ic_connect_without_contact" />
<Preference
android:title="@string/security_settings_title"
app:fragment="org.briarproject.briar.android.settings.SecurityFragment"
app:icon="@drawable/ic_settings_security" />
<Preference
android:title="@string/notification_settings_title"
app:fragment="org.briarproject.briar.android.settings.NotificationsFragment"
app:icon="@drawable/ic_notifications" />
<Preference
android:key="pref_key_send_feedback"
android:title="@string/send_feedback"
app:icon="@drawable/ic_feedback" />
<PreferenceCategory
android:key="pref_key_dev"
android:layout="@layout/preferences_category"
android:title="@string/network_settings_title">
<SwitchPreference
android:defaultValue="false"
android:key="pref_key_bluetooth"
android:persistent="false"
android:title="@string/bluetooth_setting"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="false"
android:key="pref_key_wifi"
android:persistent="false"
android:title="@string/wifi_setting"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_tor_enable"
android:persistent="false"
android:title="@string/tor_enable_title"
android:summary="@string/tor_enable_summary"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<ListPreference
android:defaultValue="0"
android:dependency="pref_key_tor_enable"
android:entries="@array/tor_network_setting_names"
android:entryValues="@array/tor_network_setting_values"
android:key="pref_key_tor_network"
android:persistent="false"
android:summary="%s"
android:title="@string/tor_network_setting"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:dependency="pref_key_tor_enable"
android:key="pref_key_tor_mobile_data"
android:persistent="false"
android:title="@string/tor_mobile_data_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="false"
android:dependency="pref_key_tor_enable"
android:key="pref_key_tor_only_when_charging"
android:persistent="false"
android:title="@string/tor_only_when_charging_title"
android:summary="@string/tor_only_when_charging_summary"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/security_settings_title">
<SwitchPreference
android:enabled="false"
android:key="pref_key_lock"
android:persistent="false"
android:summary="@string/pref_lock_summary"
android:title="@string/pref_lock_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<ListPreference
android:defaultValue="@string/pref_lock_timeout_value_default"
android:dependency="pref_key_lock"
android:entries="@array/pref_key_lock_timeout_entries"
android:entryValues="@array/pref_key_lock_timeout_values"
android:key="pref_key_lock_timeout"
android:persistent="false"
android:summary="@string/pref_lock_timeout_summary"
android:title="@string/pref_lock_timeout_title"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_key_change_password"
android:title="@string/change_password"
app:iconSpaceReserved="false">
<intent
android:targetClass="org.briarproject.briar.android.login.ChangePasswordActivity"
android:targetPackage="@string/app_package"/>
</Preference>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/panic_setting_title">
<Preference
android:summary="@string/panic_setting_hint"
android:title="@string/panic_setting"
app:iconSpaceReserved="false">
<intent
android:targetClass="org.briarproject.briar.android.panic.PanicPreferencesActivity"
android:targetPackage="@string/app_package"/>
</Preference>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/notification_settings_title">
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_sign_in"
android:summary="@string/notify_sign_in_summary"
android:title="@string/notify_sign_in_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_private_messages"
android:persistent="false"
android:summary="@string/notify_private_messages_setting_summary"
android:title="@string/notify_private_messages_setting_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_group_messages"
android:persistent="false"
android:summary="@string/notify_group_messages_setting_summary"
android:title="@string/notify_group_messages_setting_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_forum_posts"
android:persistent="false"
android:summary="@string/notify_forum_posts_setting_summary"
android:title="@string/notify_forum_posts_setting_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_blog_posts"
android:persistent="false"
android:summary="@string/notify_blog_posts_setting_summary"
android:title="@string/notify_blog_posts_setting_title"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="true"
android:key="pref_key_notify_vibration"
android:persistent="false"
android:title="@string/notify_vibration_setting"
android:widgetLayout="@layout/preference_switch_compat"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_key_notify_sound"
android:title="@string/notify_sound_setting"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/feedback_settings_title">
<Preference
android:key="pref_key_send_feedback"
android:title="@string/send_feedback"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="Testing">
android:title="Developer Options"
app:allowDividerAbove="true">
<Preference
android:key="pref_key_test_data"
android:title="Create Test Data"
app:iconSpaceReserved="false">
android:title="Create test data">
<intent
android:targetClass="org.briarproject.briar.android.test.TestDataActivity"
android:targetPackage="@string/app_package"/>
android:targetPackage="@string/app_package" />
</Preference>
<Preference
android:key="pref_key_explode"
android:title="Crash"
app:iconSpaceReserved="false"/>
android:title="Crash" />
</PreferenceCategory>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="false"
android:key="pref_key_bluetooth"
android:persistent="false"
android:title="@string/bluetooth_setting"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="false"
android:key="pref_key_wifi"
android:persistent="false"
android:title="@string/wifi_setting"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="pref_key_tor_enable"
android:persistent="false"
android:summary="@string/tor_enable_summary"
android:title="@string/tor_enable_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<ListPreference
android:defaultValue="0"
android:dependency="pref_key_tor_enable"
android:enabled="false"
android:entries="@array/tor_network_setting_names"
android:entryValues="@array/tor_network_setting_values"
android:key="pref_key_tor_network"
android:persistent="false"
android:summary="%s"
android:title="@string/tor_network_setting"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:dependency="pref_key_tor_enable"
android:enabled="false"
android:key="pref_key_tor_mobile_data"
android:persistent="false"
android:title="@string/tor_mobile_data_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:dependency="pref_key_tor_enable"
android:enabled="false"
android:key="pref_key_tor_only_when_charging"
android:persistent="false"
android:summary="@string/tor_only_when_charging_summary"
android:title="@string/tor_only_when_charging_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
</PreferenceScreen>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<ListPreference
android:defaultValue="default"
android:entryValues="@array/pref_language_values"
android:key="pref_key_language"
android:summary="%s"
android:title="@string/pref_language_title"
app:iconSpaceReserved="false"
tools:summary="System default" />
<ListPreference
android:defaultValue="@string/pref_theme_light_value"
android:entries="@array/pref_theme_entries"
android:entryValues="@array/pref_theme_values"
android:key="pref_key_theme"
android:summary="%s"
android:title="@string/pref_theme_title"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_key_notify_sign_in"
android:summary="@string/notify_sign_in_summary"
android:title="@string/notify_sign_in_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="notifyPrivateMessages"
android:persistent="false"
android:summary="@string/notify_private_messages_setting_summary"
android:title="@string/notify_private_messages_setting_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="notifyGroupMessages"
android:persistent="false"
android:summary="@string/notify_group_messages_setting_summary"
android:title="@string/notify_group_messages_setting_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="notifyForumPosts"
android:persistent="false"
android:summary="@string/notify_forum_posts_setting_summary"
android:title="@string/notify_forum_posts_setting_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="notifyBlogPosts"
android:persistent="false"
android:summary="@string/notify_blog_posts_setting_summary"
android:title="@string/notify_blog_posts_setting_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="false"
android:key="notifyVibration"
android:persistent="false"
android:title="@string/notify_vibration_setting"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<Preference
android:key="notifySound"
android:title="@string/notify_sound_setting"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<SwitchPreferenceCompat
android:enabled="false"
android:key="pref_key_lock"
android:persistent="false"
android:summary="@string/pref_lock_summary"
android:title="@string/pref_lock_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<ListPreference
android:defaultValue="@string/pref_lock_timeout_value_default"
android:dependency="pref_key_lock"
android:enabled="false"
android:entries="@array/pref_key_lock_timeout_entries"
android:entryValues="@array/pref_key_lock_timeout_values"
android:key="pref_key_lock_timeout"
android:persistent="false"
android:title="@string/pref_lock_timeout_title"
app:iconSpaceReserved="false"
tools:summary="@string/pref_lock_timeout_summary" />
<Preference
android:key="pref_key_change_password"
android:title="@string/change_password"
app:iconSpaceReserved="false">
<intent
android:targetClass="org.briarproject.briar.android.login.ChangePasswordActivity"
android:targetPackage="@string/app_package" />
</Preference>
<PreferenceCategory
android:layout="@layout/preferences_category"
android:title="@string/panic_setting_title">
<Preference
android:summary="@string/panic_setting_hint"
android:title="@string/panic_setting"
app:iconSpaceReserved="false">
<intent
android:targetClass="org.briarproject.briar.android.panic.PanicPreferencesActivity"
android:targetPackage="@string/app_package" />
</Preference>
</PreferenceCategory>
</PreferenceScreen>