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 stickerDir.setOnPreferenceClickListener((p) -> {
379 Intent intent = ((StorageManager) getSystemService(Context.STORAGE_SERVICE)).getPrimaryStorageVolume().createOpenDocumentTreeIntent();
380 startActivityForResult(Intent.createChooser(intent, "Choose sticker location"), 0);
381 return true;
382 });
383 }
384
385 private void changeOmemoSettingSummary() {
386 final ListPreference omemoPreference =
387 (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING);
388 if (omemoPreference == null) {
389 return;
390 }
391 final String value = omemoPreference.getValue();
392 switch (value) {
393 case "always":
394 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always);
395 break;
396 case "default_on":
397 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on);
398 break;
399 case "default_off":
400 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off);
401 break;
402 }
403 }
404
405 private boolean isCallable(final Intent i) {
406 return i != null
407 && getPackageManager()
408 .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY)
409 .size()
410 > 0;
411 }
412
413 private boolean cleanCache() {
414 Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
415 intent.setData(Uri.parse("package:" + getPackageName()));
416 startActivity(intent);
417 return true;
418 }
419
420 private boolean cleanPrivateStorage() {
421 for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) {
422 cleanPrivateFiles(type);
423 }
424 return true;
425 }
426
427 private void cleanPrivateFiles(final String type) {
428 try {
429 File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/");
430 File[] array = dir.listFiles();
431 if (array != null) {
432 for (int b = 0; b < array.length; b++) {
433 String name = array[b].getName().toLowerCase();
434 if (name.equals(".nomedia")) {
435 continue;
436 }
437 if (array[b].isFile()) {
438 array[b].delete();
439 }
440 }
441 }
442 } catch (Throwable e) {
443 Log.e("CleanCache", e.toString());
444 }
445 }
446
447 private boolean deleteOmemoIdentities() {
448 AlertDialog.Builder builder = new AlertDialog.Builder(this);
449 builder.setTitle(R.string.pref_delete_omemo_identities);
450 final List<CharSequence> accounts = new ArrayList<>();
451 for (Account account : xmppConnectionService.getAccounts()) {
452 if (account.isEnabled()) {
453 accounts.add(account.getJid().asBareJid().toString());
454 }
455 }
456 final boolean[] checkedItems = new boolean[accounts.size()];
457 builder.setMultiChoiceItems(
458 accounts.toArray(new CharSequence[accounts.size()]),
459 checkedItems,
460 (dialog, which, isChecked) -> {
461 checkedItems[which] = isChecked;
462 final AlertDialog alertDialog = (AlertDialog) dialog;
463 for (boolean item : checkedItems) {
464 if (item) {
465 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
466 return;
467 }
468 }
469 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
470 });
471 builder.setNegativeButton(R.string.cancel, null);
472 builder.setPositiveButton(
473 R.string.delete_selected_keys,
474 (dialog, which) -> {
475 for (int i = 0; i < checkedItems.length; ++i) {
476 if (checkedItems[i]) {
477 try {
478 Jid jid = Jid.of(accounts.get(i).toString());
479 Account account = xmppConnectionService.findAccountByJid(jid);
480 if (account != null) {
481 account.getAxolotlService().regenerateKeys(true);
482 }
483 } catch (IllegalArgumentException e) {
484 //
485 }
486 }
487 }
488 });
489 AlertDialog dialog = builder.create();
490 dialog.show();
491 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
492 return true;
493 }
494
495 @Override
496 public void onStop() {
497 super.onStop();
498 PreferenceManager.getDefaultSharedPreferences(this)
499 .unregisterOnSharedPreferenceChangeListener(this);
500 }
501
502 @Override
503 public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
504 final List<String> resendPresence =
505 Arrays.asList(
506 "confirm_messages",
507 DND_ON_SILENT_MODE,
508 AWAY_WHEN_SCREEN_IS_OFF,
509 "allow_message_correction",
510 TREAT_VIBRATE_AS_SILENT,
511 MANUALLY_CHANGE_PRESENCE,
512 BROADCAST_LAST_ACTIVITY);
513 if (name.equals(OMEMO_SETTING)) {
514 OmemoSetting.load(this, preferences);
515 changeOmemoSettingSummary();
516 } else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
517 xmppConnectionService.toggleForegroundService();
518 } else if (resendPresence.contains(name)) {
519 if (xmppConnectionServiceBound) {
520 if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
521 xmppConnectionService.toggleScreenEventReceiver();
522 }
523 xmppConnectionService.refreshAllPresences();
524 }
525 } else if (name.equals("dont_trust_system_cas")) {
526 xmppConnectionService.updateMemorizingTrustmanager();
527 reconnectAccounts();
528 } else if (name.equals("use_tor")) {
529 if (preferences.getBoolean(name, false)) {
530 displayToast(getString(R.string.audio_video_disabled_tor));
531 }
532 reconnectAccounts();
533 xmppConnectionService.reinitializeMuclumbusService();
534 } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
535 xmppConnectionService.expireOldMessages(true);
536 } else if (name.equals(THEME)) {
537 final int theme = findTheme();
538 if (this.mTheme != theme) {
539 xmppConnectionService.setTheme(theme);
540 recreate();
541 }
542 } else if (name.equals(PREVENT_SCREENSHOTS)) {
543 SettingsUtils.applyScreenshotPreventionSetting(this);
544 } else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
545 if (xmppConnectionService.reconfigurePushDistributor()) {
546 xmppConnectionService.renewUnifiedPushEndpoints();
547 }
548 }
549 }
550
551 @Override
552 public void onResume() {
553 super.onResume();
554 SettingsUtils.applyScreenshotPreventionSetting(this);
555 }
556
557 @Override
558 public void onRequestPermissionsResult(
559 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
560 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
561 if (grantResults.length > 0)
562 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
563 if (requestCode == REQUEST_CREATE_BACKUP) {
564 createBackup();
565 }
566 } else {
567 Toast.makeText(
568 this,
569 getString(
570 R.string.no_storage_permission,
571 getString(R.string.app_name)),
572 Toast.LENGTH_SHORT)
573 .show();
574 }
575 }
576
577 private void createBackup() {
578 new AlertDialog.Builder(this)
579 .setTitle("Create Backup")
580 .setMessage("Export extra Cheogram-only data (backup will not import into other apps then)?")
581 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
582 createBackup(true);
583 })
584 .setNegativeButton(R.string.no, (dialog, whichButton) -> {
585 createBackup(false);
586 }).show();
587 }
588
589 private void createBackup(boolean withCheogramDb) {
590 Intent intent = new Intent(this, ExportBackupService.class);
591 intent.putExtra("cheogram_db", withCheogramDb);
592 ContextCompat.startForegroundService(this, intent);
593 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
594 builder.setMessage(R.string.backup_started_message);
595 builder.setPositiveButton(R.string.ok, null);
596 builder.create().show();
597 }
598
599 private void displayToast(final String msg) {
600 runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show());
601 }
602
603 private void reconnectAccounts() {
604 for (Account account : xmppConnectionService.getAccounts()) {
605 if (account.isEnabled()) {
606 xmppConnectionService.reconnectAccountInBackground(account);
607 }
608 }
609 }
610
611 public void refreshUiReal() {
612 // nothing to do. This Activity doesn't implement any listeners
613 }
614}