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