1package eu.siacs.conversations.ui;
2
3import android.app.FragmentManager;
4import android.content.DialogInterface;
5import android.content.Intent;
6import android.content.SharedPreferences;
7import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
8import android.content.pm.PackageManager;
9import android.net.Uri;
10import android.os.Build;
11import android.os.Bundle;
12import android.preference.CheckBoxPreference;
13import android.preference.ListPreference;
14import android.preference.Preference;
15import android.preference.PreferenceCategory;
16import android.preference.PreferenceManager;
17import android.preference.PreferenceScreen;
18import android.provider.MediaStore;
19import android.util.Log;
20import android.widget.Toast;
21
22import androidx.annotation.NonNull;
23import androidx.appcompat.app.AlertDialog;
24import androidx.core.content.ContextCompat;
25import androidx.databinding.DataBindingUtil;
26
27import com.google.android.material.color.DynamicColors;
28import com.google.android.material.dialog.MaterialAlertDialogBuilder;
29import com.google.common.base.Strings;
30import com.google.common.collect.ImmutableList;
31import com.google.common.collect.Lists;
32
33import java.io.File;
34import java.net.URI;
35import java.net.URISyntaxException;
36import java.security.KeyStoreException;
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.Collections;
40import java.util.List;
41
42import eu.siacs.conversations.Config;
43import eu.siacs.conversations.Conversations;
44import eu.siacs.conversations.R;
45import eu.siacs.conversations.crypto.OmemoSetting;
46import eu.siacs.conversations.databinding.ActivitySettingsBinding;
47import eu.siacs.conversations.entities.Account;
48import eu.siacs.conversations.persistance.FileBackend;
49import eu.siacs.conversations.services.ExportBackupService;
50import eu.siacs.conversations.services.MemorizingTrustManager;
51import eu.siacs.conversations.services.QuickConversationsService;
52import eu.siacs.conversations.services.UnifiedPushDistributor;
53import eu.siacs.conversations.ui.util.SettingsUtils;
54import eu.siacs.conversations.utils.GeoHelper;
55import eu.siacs.conversations.utils.TimeFrameUtils;
56import eu.siacs.conversations.xmpp.Jid;
57
58public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
59
60 public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
61 public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
62 public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
63 public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode";
64 public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
65 public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
66 public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
67 public static final String BROADCAST_LAST_ACTIVITY = "last_activity";
68 public static final String THEME = "theme";
69 public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
70 public static final String OMEMO_SETTING = "omemo";
71 public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
72
73 public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
74
75 private SettingsFragment mSettingsFragment;
76
77 @Override
78 protected void onCreate(Bundle savedInstanceState) {
79 super.onCreate(savedInstanceState);
80 final ActivitySettingsBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_settings);
81 FragmentManager fm = getFragmentManager();
82 mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content);
83 if (mSettingsFragment == null
84 || !mSettingsFragment.getClass().equals(SettingsFragment.class)) {
85 mSettingsFragment = new SettingsFragment();
86 fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit();
87 }
88 mSettingsFragment.setActivityIntent(getIntent());
89 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
90 setSupportActionBar(binding.toolbar);
91 configureActionBar(getSupportActionBar());
92 }
93
94 @Override
95 void onBackendConnected() {
96 final Preference accountPreference =
97 mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT);
98 reconfigureUpAccountPreference(accountPreference);
99 }
100
101 private void reconfigureUpAccountPreference(final Preference preference) {
102 final ListPreference listPreference;
103 if (preference instanceof ListPreference) {
104 listPreference = (ListPreference) preference;
105 } else {
106 return;
107 }
108 final List<CharSequence> accounts =
109 ImmutableList.copyOf(
110 Lists.transform(
111 xmppConnectionService.getAccounts(),
112 a -> a.getJid().asBareJid().toEscapedString()));
113 final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
114 final ImmutableList.Builder<CharSequence> entryValues = new ImmutableList.Builder<>();
115 entries.add(getString(R.string.no_account_deactivated));
116 entryValues.add("none");
117 entries.addAll(accounts);
118 entryValues.addAll(accounts);
119 listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
120 listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
121 if (!accounts.contains(listPreference.getValue())) {
122 listPreference.setValue("none");
123 }
124 }
125
126 @Override
127 public void onStart() {
128 super.onStart();
129 PreferenceManager.getDefaultSharedPreferences(this)
130 .registerOnSharedPreferenceChangeListener(this);
131
132 changeOmemoSettingSummary();
133
134 if (QuickConversationsService.isQuicksy()
135 || QuickConversationsService.isPlayStoreFlavor()
136 || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
137 final PreferenceCategory groupChats =
138 (PreferenceCategory) mSettingsFragment.findPreference("group_chats");
139 final Preference channelDiscoveryMethod =
140 mSettingsFragment.findPreference("channel_discovery_method");
141 if (groupChats != null && channelDiscoveryMethod != null) {
142 groupChats.removePreference(channelDiscoveryMethod);
143 }
144 }
145
146 if (QuickConversationsService.isQuicksy()) {
147 final PreferenceCategory connectionOptions =
148 (PreferenceCategory) mSettingsFragment.findPreference("connection_options");
149 PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert");
150 if (connectionOptions != null) {
151 expert.removePreference(connectionOptions);
152 }
153 }
154
155 PreferenceScreen mainPreferenceScreen =
156 (PreferenceScreen) mSettingsFragment.findPreference("main_screen");
157
158 PreferenceCategory attachmentsCategory =
159 (PreferenceCategory) mSettingsFragment.findPreference("attachments");
160 CheckBoxPreference locationPlugin =
161 (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin");
162 if (attachmentsCategory != null && locationPlugin != null) {
163 if (!GeoHelper.isLocationPluginInstalled(this)) {
164 attachmentsCategory.removePreference(locationPlugin);
165 }
166 }
167
168 // this feature is only available on Huawei Android 6.
169 PreferenceScreen huaweiPreferenceScreen =
170 (PreferenceScreen) mSettingsFragment.findPreference("huawei");
171 if (huaweiPreferenceScreen != null) {
172 Intent intent = huaweiPreferenceScreen.getIntent();
173 // remove when Api version is above M (Version 6.0) or if the intent is not callable
174 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) {
175 PreferenceCategory generalCategory =
176 (PreferenceCategory) mSettingsFragment.findPreference("general");
177 generalCategory.removePreference(huaweiPreferenceScreen);
178 if (generalCategory.getPreferenceCount() == 0) {
179 if (mainPreferenceScreen != null) {
180 mainPreferenceScreen.removePreference(generalCategory);
181 }
182 }
183 }
184 }
185
186 final PreferenceCategory uiPreferenceCategory = (PreferenceCategory) mSettingsFragment.findPreference("ui");
187 final Preference dynamicColorsPreference = mSettingsFragment.findPreference("dynamic_colors");
188 if (dynamicColorsPreference != null && !DynamicColors.isDynamicColorAvailable()) {
189 uiPreferenceCategory.removePreference(dynamicColorsPreference);
190 }
191
192 ListPreference automaticMessageDeletionList =
193 (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION);
194 if (automaticMessageDeletionList != null) {
195 final int[] choices =
196 getResources().getIntArray(R.array.automatic_message_deletion_values);
197 CharSequence[] entries = new CharSequence[choices.length];
198 CharSequence[] entryValues = new CharSequence[choices.length];
199 for (int i = 0; i < choices.length; ++i) {
200 entryValues[i] = String.valueOf(choices[i]);
201 if (choices[i] == 0) {
202 entries[i] = getString(R.string.never);
203 } else {
204 entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]);
205 }
206 }
207 automaticMessageDeletionList.setEntries(entries);
208 automaticMessageDeletionList.setEntryValues(entryValues);
209 }
210
211 final Preference removeCertsPreference =
212 mSettingsFragment.findPreference("remove_trusted_certificates");
213 if (removeCertsPreference != null) {
214 removeCertsPreference.setOnPreferenceClickListener(
215 preference -> {
216 final MemorizingTrustManager mtm =
217 xmppConnectionService.getMemorizingTrustManager();
218 final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
219 if (aliases.isEmpty()) {
220 displayToast(getString(R.string.toast_no_trusted_certs));
221 return true;
222 }
223 final ArrayList<Integer> selectedItems = new ArrayList<>();
224 final MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(SettingsActivity.this);
225 dialogBuilder.setTitle(
226 getResources().getString(R.string.dialog_manage_certs_title));
227 dialogBuilder.setMultiChoiceItems(
228 aliases.toArray(new CharSequence[0]),
229 null,
230 (dialog, indexSelected, isChecked) -> {
231 if (isChecked) {
232 selectedItems.add(indexSelected);
233 } else if (selectedItems.contains(indexSelected)) {
234 selectedItems.remove(Integer.valueOf(indexSelected));
235 }
236 ((AlertDialog) dialog)
237 .getButton(DialogInterface.BUTTON_POSITIVE)
238 .setEnabled(!selectedItems.isEmpty());
239 });
240
241 dialogBuilder.setPositiveButton(
242 getResources()
243 .getString(R.string.dialog_manage_certs_positivebutton),
244 (dialog, which) -> {
245 int count = selectedItems.size();
246 if (count > 0) {
247 for (int i = 0; i < count; i++) {
248 try {
249 final int item =
250 Integer.parseInt(
251 selectedItems.get(i).toString());
252 String alias = aliases.get(item);
253 mtm.deleteCertificate(alias);
254 } catch (final KeyStoreException e) {
255 displayToast("Error: " + e.getLocalizedMessage());
256 }
257 }
258 if (xmppConnectionServiceBound) {
259 reconnectAccounts();
260 }
261 displayToast(
262 getResources()
263 .getQuantityString(
264 R.plurals.toast_delete_certificates,
265 count,
266 count));
267 }
268 });
269 dialogBuilder.setNegativeButton(
270 getResources()
271 .getString(R.string.dialog_manage_certs_negativebutton),
272 null);
273 AlertDialog removeCertsDialog = dialogBuilder.create();
274 removeCertsDialog.show();
275 removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
276 return true;
277 });
278 }
279
280 final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup");
281 if (createBackupPreference != null) {
282 createBackupPreference.setSummary(
283 getString(
284 R.string.pref_create_backup_summary,
285 FileBackend.getBackupDirectory(this).getAbsolutePath()));
286 createBackupPreference.setOnPreferenceClickListener(
287 preference -> {
288 if (hasStoragePermission(REQUEST_CREATE_BACKUP)) {
289 createBackup();
290 }
291 return true;
292 });
293 }
294
295 if (Config.ONLY_INTERNAL_STORAGE) {
296 final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache");
297 if (cleanCachePreference != null) {
298 cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache());
299 }
300
301 final Preference cleanPrivateStoragePreference =
302 mSettingsFragment.findPreference("clean_private_storage");
303 if (cleanPrivateStoragePreference != null) {
304 cleanPrivateStoragePreference.setOnPreferenceClickListener(
305 preference -> cleanPrivateStorage());
306 }
307 }
308
309 final Preference deleteOmemoPreference =
310 mSettingsFragment.findPreference("delete_omemo_identities");
311 if (deleteOmemoPreference != null) {
312 deleteOmemoPreference.setOnPreferenceClickListener(
313 preference -> deleteOmemoIdentities());
314 }
315 if (Config.omemoOnly()) {
316 final PreferenceCategory privacyCategory =
317 (PreferenceCategory) mSettingsFragment.findPreference("privacy");
318 final Preference omemoPreference =mSettingsFragment.findPreference(OMEMO_SETTING);
319 if (omemoPreference != null) {
320 privacyCategory.removePreference(omemoPreference);
321 }
322 }
323 }
324
325 private void changeOmemoSettingSummary() {
326 final ListPreference omemoPreference =
327 (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING);
328 if (omemoPreference == null) {
329 return;
330 }
331 final String value = omemoPreference.getValue();
332 switch (value) {
333 case "always":
334 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always);
335 break;
336 case "default_on":
337 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on);
338 break;
339 case "default_off":
340 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off);
341 break;
342 }
343 }
344
345 private boolean isCallable(final Intent i) {
346 return i != null
347 && !getPackageManager()
348 .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
349 }
350
351 private boolean cleanCache() {
352 Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
353 intent.setData(Uri.parse("package:" + getPackageName()));
354 startActivity(intent);
355 return true;
356 }
357
358 private boolean cleanPrivateStorage() {
359 for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) {
360 cleanPrivateFiles(type);
361 }
362 return true;
363 }
364
365 private void cleanPrivateFiles(final String type) {
366 try {
367 File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/");
368 File[] array = dir.listFiles();
369 if (array != null) {
370 for (int b = 0; b < array.length; b++) {
371 String name = array[b].getName().toLowerCase();
372 if (name.equals(".nomedia")) {
373 continue;
374 }
375 if (array[b].isFile()) {
376 array[b].delete();
377 }
378 }
379 }
380 } catch (Throwable e) {
381 Log.e("CleanCache", e.toString());
382 }
383 }
384
385 private boolean deleteOmemoIdentities() {
386 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
387 builder.setTitle(R.string.pref_delete_omemo_identities);
388 final List<CharSequence> accounts = new ArrayList<>();
389 for (Account account : xmppConnectionService.getAccounts()) {
390 if (account.isEnabled()) {
391 accounts.add(account.getJid().asBareJid().toString());
392 }
393 }
394 final boolean[] checkedItems = new boolean[accounts.size()];
395 builder.setMultiChoiceItems(
396 accounts.toArray(new CharSequence[accounts.size()]),
397 checkedItems,
398 (dialog, which, isChecked) -> {
399 checkedItems[which] = isChecked;
400 final AlertDialog alertDialog = (AlertDialog) dialog;
401 for (boolean item : checkedItems) {
402 if (item) {
403 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
404 return;
405 }
406 }
407 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
408 });
409 builder.setNegativeButton(R.string.cancel, null);
410 builder.setPositiveButton(
411 R.string.delete_selected_keys,
412 (dialog, which) -> {
413 for (int i = 0; i < checkedItems.length; ++i) {
414 if (checkedItems[i]) {
415 try {
416 Jid jid = Jid.of(accounts.get(i).toString());
417 Account account = xmppConnectionService.findAccountByJid(jid);
418 if (account != null) {
419 account.getAxolotlService().regenerateKeys(true);
420 }
421 } catch (IllegalArgumentException e) {
422 //
423 }
424 }
425 }
426 });
427 AlertDialog dialog = builder.create();
428 dialog.show();
429 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
430 return true;
431 }
432
433 @Override
434 public void onStop() {
435 super.onStop();
436 PreferenceManager.getDefaultSharedPreferences(this)
437 .unregisterOnSharedPreferenceChangeListener(this);
438 }
439
440 @Override
441 public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
442 final List<String> resendPresence =
443 Arrays.asList(
444 "confirm_messages",
445 DND_ON_SILENT_MODE,
446 AWAY_WHEN_SCREEN_IS_OFF,
447 "allow_message_correction",
448 TREAT_VIBRATE_AS_SILENT,
449 MANUALLY_CHANGE_PRESENCE,
450 BROADCAST_LAST_ACTIVITY);
451 if (name.equals(OMEMO_SETTING)) {
452 OmemoSetting.load(this, preferences);
453 changeOmemoSettingSummary();
454 } else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
455 xmppConnectionService.toggleForegroundService();
456 } else if (resendPresence.contains(name)) {
457 if (xmppConnectionServiceBound) {
458 if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
459 xmppConnectionService.toggleScreenEventReceiver();
460 }
461 xmppConnectionService.refreshAllPresences();
462 }
463 } else if (name.equals("dont_trust_system_cas")) {
464 xmppConnectionService.updateMemorizingTrustmanager();
465 reconnectAccounts();
466 } else if (name.equals("use_tor")) {
467 if (preferences.getBoolean(name, false)) {
468 displayToast(getString(R.string.audio_video_disabled_tor));
469 }
470 reconnectAccounts();
471 xmppConnectionService.reinitializeMuclumbusService();
472 } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
473 xmppConnectionService.expireOldMessages(true);
474 } else if (name.equals(THEME)) {
475 final var value = preferences.getString(THEME,getString(R.string.theme));
476 final int desiredNightMode = Conversations.getDesiredNightMode(value);
477 setDesiredNightMode(desiredNightMode);
478 } else if (name.equals("dynamic_colors")) {
479 final var value = preferences.getBoolean("dynamic_colors",false);
480 setDynamicColors(value);
481 } else if (name.equals(PREVENT_SCREENSHOTS)) {
482 SettingsUtils.applyScreenshotPreventionSetting(this);
483 } else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
484 final String pushServerPreference =
485 Strings.nullToEmpty(preferences.getString(
486 UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
487 getString(R.string.default_push_server))).trim();
488 if (isJidInvalid(pushServerPreference) || isHttpUri(pushServerPreference)) {
489 Toast.makeText(this,R.string.invalid_jid,Toast.LENGTH_LONG).show();
490 }
491 if (xmppConnectionService.reconfigurePushDistributor()) {
492 xmppConnectionService.renewUnifiedPushEndpoints();
493 }
494 }
495 }
496
497 private static boolean isJidInvalid(final String input) {
498 if (Strings.isNullOrEmpty(input)) {
499 return true;
500 }
501 try {
502 Jid.ofEscaped(input);
503 return false;
504 } catch (final IllegalArgumentException e) {
505 return true;
506 }
507 }
508
509 private static boolean isHttpUri(final String input) {
510 final URI uri;
511 try {
512 uri = new URI(input);
513 } catch (final URISyntaxException e) {
514 return false;
515 }
516 return Arrays.asList("http","https").contains(uri.getScheme());
517 }
518
519 @Override
520 public void onResume() {
521 super.onResume();
522 SettingsUtils.applyScreenshotPreventionSetting(this);
523 }
524
525 @Override
526 public void onRequestPermissionsResult(
527 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
528 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
529 if (grantResults.length > 0)
530 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
531 if (requestCode == REQUEST_CREATE_BACKUP) {
532 createBackup();
533 }
534 } else {
535 Toast.makeText(
536 this,
537 getString(
538 R.string.no_storage_permission,
539 getString(R.string.app_name)),
540 Toast.LENGTH_SHORT)
541 .show();
542 }
543 }
544
545 private void createBackup() {
546 ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
547 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
548 builder.setMessage(R.string.backup_started_message);
549 builder.setPositiveButton(R.string.ok, null);
550 builder.create().show();
551 }
552
553 private void displayToast(final String msg) {
554 runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show());
555 }
556
557 private void reconnectAccounts() {
558 for (Account account : xmppConnectionService.getAccounts()) {
559 if (account.isEnabled()) {
560 xmppConnectionService.reconnectAccountInBackground(account);
561 }
562 }
563 }
564
565 public void refreshUiReal() {
566 // nothing to do. This Activity doesn't implement any listeners
567 }
568}