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