SettingsActivity.java

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