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