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