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