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 }
302
303 private void changeOmemoSettingSummary() {
304 ListPreference omemoPreference =
305 (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING);
306 if (omemoPreference != null) {
307 String value = omemoPreference.getValue();
308 switch (value) {
309 case "always":
310 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always);
311 break;
312 case "default_on":
313 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on);
314 break;
315 case "default_off":
316 omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off);
317 break;
318 }
319 } else {
320 Log.d(Config.LOGTAG, "unable to find preference named " + OMEMO_SETTING);
321 }
322 }
323
324 private boolean isCallable(final Intent i) {
325 return i != null
326 && getPackageManager()
327 .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY)
328 .size()
329 > 0;
330 }
331
332 private boolean cleanCache() {
333 Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
334 intent.setData(Uri.parse("package:" + getPackageName()));
335 startActivity(intent);
336 return true;
337 }
338
339 private boolean cleanPrivateStorage() {
340 for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) {
341 cleanPrivateFiles(type);
342 }
343 return true;
344 }
345
346 private void cleanPrivateFiles(final String type) {
347 try {
348 File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/");
349 File[] array = dir.listFiles();
350 if (array != null) {
351 for (int b = 0; b < array.length; b++) {
352 String name = array[b].getName().toLowerCase();
353 if (name.equals(".nomedia")) {
354 continue;
355 }
356 if (array[b].isFile()) {
357 array[b].delete();
358 }
359 }
360 }
361 } catch (Throwable e) {
362 Log.e("CleanCache", e.toString());
363 }
364 }
365
366 private boolean deleteOmemoIdentities() {
367 AlertDialog.Builder builder = new AlertDialog.Builder(this);
368 builder.setTitle(R.string.pref_delete_omemo_identities);
369 final List<CharSequence> accounts = new ArrayList<>();
370 for (Account account : xmppConnectionService.getAccounts()) {
371 if (account.isEnabled()) {
372 accounts.add(account.getJid().asBareJid().toString());
373 }
374 }
375 final boolean[] checkedItems = new boolean[accounts.size()];
376 builder.setMultiChoiceItems(
377 accounts.toArray(new CharSequence[accounts.size()]),
378 checkedItems,
379 (dialog, which, isChecked) -> {
380 checkedItems[which] = isChecked;
381 final AlertDialog alertDialog = (AlertDialog) dialog;
382 for (boolean item : checkedItems) {
383 if (item) {
384 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
385 return;
386 }
387 }
388 alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
389 });
390 builder.setNegativeButton(R.string.cancel, null);
391 builder.setPositiveButton(
392 R.string.delete_selected_keys,
393 (dialog, which) -> {
394 for (int i = 0; i < checkedItems.length; ++i) {
395 if (checkedItems[i]) {
396 try {
397 Jid jid = Jid.of(accounts.get(i).toString());
398 Account account = xmppConnectionService.findAccountByJid(jid);
399 if (account != null) {
400 account.getAxolotlService().regenerateKeys(true);
401 }
402 } catch (IllegalArgumentException e) {
403 //
404 }
405 }
406 }
407 });
408 AlertDialog dialog = builder.create();
409 dialog.show();
410 dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
411 return true;
412 }
413
414 @Override
415 public void onStop() {
416 super.onStop();
417 PreferenceManager.getDefaultSharedPreferences(this)
418 .unregisterOnSharedPreferenceChangeListener(this);
419 }
420
421 @Override
422 public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
423 final List<String> resendPresence =
424 Arrays.asList(
425 "confirm_messages",
426 DND_ON_SILENT_MODE,
427 AWAY_WHEN_SCREEN_IS_OFF,
428 "allow_message_correction",
429 TREAT_VIBRATE_AS_SILENT,
430 MANUALLY_CHANGE_PRESENCE,
431 BROADCAST_LAST_ACTIVITY);
432 if (name.equals(OMEMO_SETTING)) {
433 OmemoSetting.load(this, preferences);
434 changeOmemoSettingSummary();
435 } else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
436 xmppConnectionService.toggleForegroundService();
437 } else if (resendPresence.contains(name)) {
438 if (xmppConnectionServiceBound) {
439 if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
440 xmppConnectionService.toggleScreenEventReceiver();
441 }
442 xmppConnectionService.refreshAllPresences();
443 }
444 } else if (name.equals("dont_trust_system_cas")) {
445 xmppConnectionService.updateMemorizingTrustmanager();
446 reconnectAccounts();
447 } else if (name.equals("use_tor")) {
448 if (preferences.getBoolean(name, false)) {
449 displayToast(getString(R.string.audio_video_disabled_tor));
450 }
451 reconnectAccounts();
452 xmppConnectionService.reinitializeMuclumbusService();
453 } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
454 xmppConnectionService.expireOldMessages(true);
455 } else if (name.equals(THEME)) {
456 final int theme = findTheme();
457 if (this.mTheme != theme) {
458 recreate();
459 }
460 } else if (name.equals(PREVENT_SCREENSHOTS)) {
461 SettingsUtils.applyScreenshotPreventionSetting(this);
462 }
463 }
464
465 @Override
466 public void onResume() {
467 super.onResume();
468 SettingsUtils.applyScreenshotPreventionSetting(this);
469 }
470
471 @Override
472 public void onRequestPermissionsResult(
473 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
474 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
475 if (grantResults.length > 0)
476 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
477 if (requestCode == REQUEST_CREATE_BACKUP) {
478 createBackup();
479 }
480 } else {
481 Toast.makeText(
482 this,
483 getString(
484 R.string.no_storage_permission,
485 getString(R.string.app_name)),
486 Toast.LENGTH_SHORT)
487 .show();
488 }
489 }
490
491 private void createBackup() {
492 ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
493 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
494 builder.setMessage(R.string.backup_started_message);
495 builder.setPositiveButton(R.string.ok, null);
496 builder.create().show();
497 }
498
499 private void displayToast(final String msg) {
500 runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show());
501 }
502
503 private void reconnectAccounts() {
504 for (Account account : xmppConnectionService.getAccounts()) {
505 if (account.isEnabled()) {
506 xmppConnectionService.reconnectAccountInBackground(account);
507 }
508 }
509 }
510
511 public void refreshUiReal() {
512 // nothing to do. This Activity doesn't implement any listeners
513 }
514}