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