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