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