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