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