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