diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 86db83f29..5e04f88eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -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); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index b8db5726f..437d25388 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -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() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java index 5e99eba79..015a96d98 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java @@ -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) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/account/LockManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/account/LockManagerImpl.java index fb978bd6a..e2e396958 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/account/LockManagerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/account/LockManagerImpl.java @@ -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; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordActivity.java index 7aadbef8b..88fe2dbac 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordActivity.java @@ -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() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordController.java b/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordController.java deleted file mode 100644 index 54e8ab55d..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/login/ChangePasswordController.java +++ /dev/null @@ -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 resultHandler); - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/SignInReminderReceiver.java b/briar-android/src/main/java/org/briarproject/briar/android/login/SignInReminderReceiver.java index 994825bdc..a1237f616 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/login/SignInReminderReceiver.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/login/SignInReminderReceiver.java @@ -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(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/panic/PanicPreferencesFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/panic/PanicPreferencesFragment.java index e37fbaa5b..ff13f6dbf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/panic/PanicPreferencesFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/panic/PanicPreferencesFragment.java @@ -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())) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/AvatarPreference.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/AvatarPreference.java new file mode 100644 index 000000000..77812b14f --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/AvatarPreference.java @@ -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(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java new file mode 100644 index 000000000..f669636f6 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java @@ -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); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsManager.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsManager.java new file mode 100644 index 000000000..61fbfd6e4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsManager.java @@ -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 btEnabled = new MutableLiveData<>(); + private final MutableLiveData wifiEnabled = + new MutableLiveData<>(); + private final MutableLiveData torEnabled = new MutableLiveData<>(); + private final MutableLiveData torNetwork = new MutableLiveData<>(); + private final MutableLiveData torMobile = new MutableLiveData<>(); + private final MutableLiveData 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 btEnabled() { + return btEnabled; + } + + LiveData wifiEnabled() { + return wifiEnabled; + } + + LiveData torEnabled() { + return torEnabled; + } + + LiveData torNetwork() { + return torNetwork; + } + + LiveData torMobile() { + return torMobile; + } + + LiveData torCharging() { + return torCharging; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsStore.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsStore.java new file mode 100644 index 000000000..94a12d53b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsStore.java @@ -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); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/DisplayFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/DisplayFragment.java new file mode 100644 index 000000000..3103a6804 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/DisplayFragment.java @@ -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 entries = new ArrayList<>(tags.length); + List 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 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 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; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsFragment.java new file mode 100644 index 000000000..b39192743 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsFragment.java @@ -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; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsManager.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsManager.java new file mode 100644 index 000000000..83a18c0ce --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/NotificationsManager.java @@ -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 notifyPrivateMessages = + new MutableLiveData<>(); + private final MutableLiveData notifyGroupMessages = + new MutableLiveData<>(); + private final MutableLiveData notifyForumPosts = + new MutableLiveData<>(); + private final MutableLiveData notifyBlogPosts = + new MutableLiveData<>(); + private final MutableLiveData notifyVibration = + new MutableLiveData<>(); + private final MutableLiveData 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 getNotifyPrivateMessages() { + return notifyPrivateMessages; + } + + LiveData getNotifyGroupMessages() { + return notifyGroupMessages; + } + + LiveData getNotifyForumPosts() { + return notifyForumPosts; + } + + LiveData getNotifyBlogPosts() { + return notifyBlogPosts; + } + + LiveData getNotifyVibration() { + return notifyVibration; + } + + @NonNull + LiveData getNotifySound() { + return notifySound; + } + + String getRingtoneName() { + return ringtoneName; + } + + String getRingtoneUri() { + return ringtoneUri; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SecurityFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SecurityFragment.java new file mode 100644 index 000000000..36a10f228 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SecurityFragment.java @@ -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); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java index 747bc9895..18b3c0f6c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java @@ -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); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index 7412b213f..e3769e9e2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -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 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 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 entries = new ArrayList<>(tags.length); - List 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); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsStore.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsStore.java new file mode 100644 index 000000000..b5752de51 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsStore.java @@ -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); + } + }); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java index 803e2bbf2..9d08c2905 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java @@ -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 = new MutableLiveData<>(); - - private final MutableLiveEvent setAvatarFailed = - new MutableLiveEvent<>(); + private final MutableLiveData screenLockEnabled = + new MutableLiveData<>(); + private final MutableLiveData 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 getOwnIdentityInfo() { - return ownIdentityInfo; + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); } - LiveEvent 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 getOwnIdentityInfo() { + return ownIdentityInfo; + } + + LiveData getScreenLockEnabled() { + return screenLockEnabled; + } + + LiveData getScreenLockTimeout() { + return screenLockTimeout; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/TorSummaryProvider.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/TorSummaryProvider.java new file mode 100644 index 000000000..5e0ba6e8b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/TorSummaryProvider.java @@ -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 { + + 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); + } + +} diff --git a/briar-android/src/main/res/drawable/ic_connect_without_contact.xml b/briar-android/src/main/res/drawable/ic_connect_without_contact.xml new file mode 100644 index 000000000..38c9f9901 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_connect_without_contact.xml @@ -0,0 +1,12 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_feedback.xml b/briar-android/src/main/res/drawable/ic_feedback.xml new file mode 100644 index 000000000..7c69a8240 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_feedback.xml @@ -0,0 +1,12 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_notifications.xml b/briar-android/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 000000000..2bc13c6a0 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,12 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_settings_brightness.xml b/briar-android/src/main/res/drawable/ic_settings_brightness.xml new file mode 100644 index 000000000..7fb6eb7ed --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_settings_brightness.xml @@ -0,0 +1,12 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_settings_security.xml b/briar-android/src/main/res/drawable/ic_settings_security.xml new file mode 100644 index 000000000..0746b87ff --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_settings_security.xml @@ -0,0 +1,12 @@ + + + diff --git a/briar-android/src/main/res/layout/activity_settings.xml b/briar-android/src/main/res/layout/activity_settings.xml index 88bf95130..90826e8ba 100644 --- a/briar-android/src/main/res/layout/activity_settings.xml +++ b/briar-android/src/main/res/layout/activity_settings.xml @@ -1,78 +1,8 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> diff --git a/briar-android/src/main/res/layout/list_item_transport_card.xml b/briar-android/src/main/res/layout/list_item_transport_card.xml index 638ef70a8..2ad07a951 100644 --- a/briar-android/src/main/res/layout/list_item_transport_card.xml +++ b/briar-android/src/main/res/layout/list_item_transport_card.xml @@ -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" /> diff --git a/briar-android/src/main/res/layout/preference_avatar.xml b/briar-android/src/main/res/layout/preference_avatar.xml new file mode 100644 index 000000000..61173ea4a --- /dev/null +++ b/briar-android/src/main/res/layout/preference_avatar.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/layout/preference_switch_compat.xml b/briar-android/src/main/res/layout/preference_switch_compat.xml deleted file mode 100644 index 7ebf4bbbe..000000000 --- a/briar-android/src/main/res/layout/preference_switch_compat.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index e92c9350e..a5887a24a 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -468,7 +468,7 @@ Light Dark Automatic (Daytime) - System Default + System default Connections @@ -552,7 +552,6 @@ Cannot load ringtone - Feedback Send feedback diff --git a/briar-android/src/main/res/xml/panic_preferences.xml b/briar-android/src/main/res/xml/panic_preferences.xml index 67887eb26..75c6e195b 100644 --- a/briar-android/src/main/res/xml/panic_preferences.xml +++ b/briar-android/src/main/res/xml/panic_preferences.xml @@ -1,14 +1,14 @@ - - + app:iconSpaceReserved="false" + app:singleLineTitle="false" /> + android:title="@string/panic_app_setting_title" /> - + app:iconSpaceReserved="false" + app:singleLineTitle="false" /> diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml index 21acdd98f..b3cbe0e53 100644 --- a/briar-android/src/main/res/xml/settings.xml +++ b/briar-android/src/main/res/xml/settings.xml @@ -1,236 +1,52 @@ - - - + - + - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:title="Developer Options" + app:allowDividerAbove="true"> + android:title="Create test data"> + android:targetPackage="@string/app_package" /> + android:title="Crash" /> diff --git a/briar-android/src/main/res/xml/settings_connections.xml b/briar-android/src/main/res/xml/settings_connections.xml new file mode 100644 index 000000000..2a25f7e40 --- /dev/null +++ b/briar-android/src/main/res/xml/settings_connections.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/xml/settings_display.xml b/briar-android/src/main/res/xml/settings_display.xml new file mode 100644 index 000000000..4a7d146a9 --- /dev/null +++ b/briar-android/src/main/res/xml/settings_display.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/briar-android/src/main/res/xml/settings_notifications.xml b/briar-android/src/main/res/xml/settings_notifications.xml new file mode 100644 index 000000000..fede5dd56 --- /dev/null +++ b/briar-android/src/main/res/xml/settings_notifications.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/xml/settings_security.xml b/briar-android/src/main/res/xml/settings_security.xml new file mode 100644 index 000000000..8c0c3fa0c --- /dev/null +++ b/briar-android/src/main/res/xml/settings_security.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + +