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