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