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