ManageAccountActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.Manifest;
  4import android.content.ActivityNotFoundException;
  5import android.content.ComponentName;
  6import android.content.Intent;
  7import android.content.pm.PackageManager;
  8import android.os.Build;
  9import android.os.Bundle;
 10import android.security.KeyChain;
 11import android.security.KeyChainAliasCallback;
 12import android.util.Pair;
 13import android.view.ContextMenu;
 14import android.view.ContextMenu.ContextMenuInfo;
 15import android.view.Menu;
 16import android.view.MenuItem;
 17import android.view.View;
 18import android.widget.AdapterView.AdapterContextMenuInfo;
 19import android.widget.ListView;
 20import android.widget.Toast;
 21
 22import androidx.annotation.NonNull;
 23import androidx.appcompat.app.ActionBar;
 24import androidx.appcompat.app.AlertDialog;
 25
 26import org.openintents.openpgp.util.OpenPgpApi;
 27
 28import java.util.ArrayList;
 29import java.util.List;
 30import java.util.concurrent.atomic.AtomicBoolean;
 31
 32import eu.siacs.conversations.Config;
 33import eu.siacs.conversations.R;
 34import eu.siacs.conversations.entities.Account;
 35import eu.siacs.conversations.entities.Contact;
 36import eu.siacs.conversations.services.XmppConnectionService;
 37import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 38import eu.siacs.conversations.ui.adapter.AccountAdapter;
 39import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 40import eu.siacs.conversations.utils.Resolver;
 41import eu.siacs.conversations.xmpp.Jid;
 42import eu.siacs.conversations.xmpp.XmppConnection;
 43
 44import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
 45import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 46
 47public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
 48
 49    private final String STATE_SELECTED_ACCOUNT = "selected_account";
 50
 51    private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
 52    private static final int REQUEST_MICROPHONE = 0x63fb1;
 53
 54    protected Account selectedAccount = null;
 55    protected Jid selectedAccountJid = null;
 56
 57    protected final List<Account> accountList = new ArrayList<>();
 58    protected ListView accountListView;
 59    protected AccountAdapter mAccountAdapter;
 60    protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
 61    protected Intent mMicIntent = null;
 62
 63    protected Pair<Integer, Intent> mPostponedActivityResult = null;
 64
 65    @Override
 66    public void onAccountUpdate() {
 67        refreshUi();
 68    }
 69
 70    @Override
 71    protected void refreshUiReal() {
 72        synchronized (this.accountList) {
 73            accountList.clear();
 74            accountList.addAll(xmppConnectionService.getAccounts());
 75        }
 76        ActionBar actionBar = getSupportActionBar();
 77        if (actionBar != null) {
 78            actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
 79            actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
 80        }
 81        invalidateOptionsMenu();
 82        mAccountAdapter.notifyDataSetChanged();
 83
 84        findViewById(R.id.phone_accounts).setVisibility(View.GONE);
 85        findViewById(R.id.phone_accounts).setOnClickListener((View v) -> {
 86            mMicIntent = new Intent();
 87            mMicIntent.setComponent(new ComponentName("com.android.server.telecom",
 88                "com.android.server.telecom.settings.EnableAccountPreferenceActivity"));
 89            requestMicPermission();
 90        });
 91        findViewById(R.id.phone_accounts_settings).setOnClickListener((View v) -> {
 92            mMicIntent = new Intent(android.telecom.TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
 93            requestMicPermission();
 94        });
 95
 96        if (Build.VERSION.SDK_INT < 23) return;
 97        if (Build.VERSION.SDK_INT >= 33) {
 98            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
 99        } else {
100            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
101        }
102
103        final var hasPhoneAccounts = xmppConnectionService.getAccounts().stream().anyMatch(a -> a.getGateways("pstn").size() > 0);
104        if (hasPhoneAccounts) {
105            findViewById(R.id.phone_accounts).setVisibility(View.VISIBLE);
106        }
107    }
108
109    @Override
110    protected void onCreate(Bundle savedInstanceState) {
111
112        super.onCreate(savedInstanceState);
113
114        setContentView(R.layout.activity_manage_accounts);
115        Activities.setStatusAndNavigationBarColors(this, findViewById(android.R.id.content));
116        setSupportActionBar(findViewById(R.id.toolbar));
117        configureActionBar(getSupportActionBar());
118        if (savedInstanceState != null) {
119            String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
120            if (jid != null) {
121                try {
122                    this.selectedAccountJid = Jid.ofEscaped(jid);
123                } catch (IllegalArgumentException e) {
124                    this.selectedAccountJid = null;
125                }
126            }
127        }
128
129        accountListView = findViewById(R.id.account_list);
130        this.mAccountAdapter = new AccountAdapter(this, accountList);
131        accountListView.setAdapter(this.mAccountAdapter);
132        accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
133        registerForContextMenu(accountListView);
134    }
135
136    @Override
137    public void onSaveInstanceState(final Bundle savedInstanceState) {
138        if (selectedAccount != null) {
139            savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString());
140        }
141        super.onSaveInstanceState(savedInstanceState);
142    }
143
144    @Override
145    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
146        super.onCreateContextMenu(menu, v, menuInfo);
147        ManageAccountActivity.this.getMenuInflater().inflate(
148                R.menu.manageaccounts_context, menu);
149        AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
150        this.selectedAccount = accountList.get(acmi.position);
151        if (this.selectedAccount.isEnabled()) {
152            menu.findItem(R.id.mgmt_account_enable).setVisible(false);
153            menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
154        } else {
155            menu.findItem(R.id.mgmt_account_disable).setVisible(false);
156            menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
157            menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
158        }
159        menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toEscapedString());
160    }
161
162    @Override
163    protected void onBackendConnected() {
164        if (selectedAccountJid != null) {
165            this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
166        }
167        refreshUiReal();
168        if (this.mPostponedActivityResult != null) {
169            this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
170        }
171        if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
172            if (mInvokedAddAccount.compareAndSet(false, true)) {
173                addAccountFromKey();
174            }
175        }
176    }
177
178    @Override
179    public boolean onCreateOptionsMenu(Menu menu) {
180        getMenuInflater().inflate(R.menu.manageaccounts, menu);
181        MenuItem enableAll = menu.findItem(R.id.action_enable_all);
182        MenuItem addAccount = menu.findItem(R.id.action_add_account);
183        MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
184
185        if (Config.X509_VERIFICATION) {
186            addAccount.setVisible(false);
187            addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
188        }
189
190        if (!accountsLeftToEnable()) {
191            enableAll.setVisible(false);
192        }
193        MenuItem disableAll = menu.findItem(R.id.action_disable_all);
194        if (!accountsLeftToDisable()) {
195            disableAll.setVisible(false);
196        }
197        return true;
198    }
199
200    @Override
201    public boolean onContextItemSelected(MenuItem item) {
202        switch (item.getItemId()) {
203            case R.id.mgmt_account_publish_avatar:
204                publishAvatar(selectedAccount);
205                return true;
206            case R.id.mgmt_account_disable:
207                disableAccount(selectedAccount);
208                return true;
209            case R.id.mgmt_account_enable:
210                enableAccount(selectedAccount);
211                return true;
212            case R.id.mgmt_account_delete:
213                deleteAccount(selectedAccount);
214                return true;
215            case R.id.mgmt_account_announce_pgp:
216                publishOpenPGPPublicKey(selectedAccount);
217                return true;
218            default:
219                return super.onContextItemSelected(item);
220        }
221    }
222
223    @Override
224    public boolean onOptionsItemSelected(MenuItem item) {
225        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
226            return false;
227        }
228        switch (item.getItemId()) {
229            case R.id.action_add_account:
230                startActivity(new Intent(this, EditAccountActivity.class));
231                break;
232            case R.id.action_import_backup:
233                if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
234                    startActivity(new Intent(this, ImportBackupActivity.class));
235                }
236                break;
237            case R.id.action_disable_all:
238                disableAllAccounts();
239                break;
240            case R.id.action_enable_all:
241                enableAllAccounts();
242                break;
243            case R.id.action_add_account_with_cert:
244                addAccountFromKey();
245                break;
246            default:
247                break;
248        }
249        return super.onOptionsItemSelected(item);
250    }
251
252    private void requestMicPermission() {
253        final String[] permissions;
254        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
255            permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
256        } else {
257            permissions = new String[]{Manifest.permission.RECORD_AUDIO};
258        }
259        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
260            if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED && shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
261                final AlertDialog.Builder builder = new AlertDialog.Builder(this);
262                builder.setTitle("Dialler Integration");
263                builder.setMessage("You will be asked to grant microphone permission, which is needed for the dialler integration to function.");
264                builder.setPositiveButton("I Understand", (dialog, which) -> {
265                    requestPermissions(permissions, REQUEST_MICROPHONE);
266                });
267                builder.setCancelable(true);
268                final AlertDialog dialog = builder.create();
269                dialog.setCanceledOnTouchOutside(true);
270                dialog.show();
271            } else {
272                requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_MICROPHONE);
273            }
274        }
275    }
276
277    @Override
278    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
279        if (grantResults.length > 0) {
280            if (allGranted(grantResults)) {
281                switch (requestCode) {
282                    case REQUEST_MICROPHONE:
283                        try {
284                            startActivity(mMicIntent);
285                        } catch (final android.content.ActivityNotFoundException e) {
286                            Toast.makeText(this, "Your OS has blocked dialler integration", Toast.LENGTH_SHORT).show();
287                        }
288                        mMicIntent = null;
289                        return;
290                    case REQUEST_IMPORT_BACKUP:
291                        startActivity(new Intent(this, ImportBackupActivity.class));
292                        break;
293                }
294            } else {
295                if (requestCode == REQUEST_MICROPHONE) {
296                    Toast.makeText(this, "Microphone access was denied", Toast.LENGTH_SHORT).show();
297                } else {
298                    Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
299                }
300            }
301        }
302        if (writeGranted(grantResults, permissions)) {
303            if (xmppConnectionService != null) {
304                xmppConnectionService.restartFileObserver();
305            }
306        }
307    }
308
309    @Override
310    public boolean onNavigateUp() {
311        if (xmppConnectionService.getConversations().size() == 0) {
312            Intent contactsIntent = new Intent(this,
313                    StartConversationActivity.class);
314            contactsIntent.setFlags(
315                    // if activity exists in stack, pop the stack and go back to it
316                    Intent.FLAG_ACTIVITY_CLEAR_TOP |
317                            // otherwise, make a new task for it
318                            Intent.FLAG_ACTIVITY_NEW_TASK |
319                            // don't use the new activity animation; finish
320                            // animation runs instead
321                            Intent.FLAG_ACTIVITY_NO_ANIMATION);
322            startActivity(contactsIntent);
323            finish();
324            return true;
325        } else {
326            return super.onNavigateUp();
327        }
328    }
329
330    @Override
331    public void onClickTglAccountState(Account account, boolean enable) {
332        if (enable) {
333            enableAccount(account);
334        } else {
335            disableAccount(account);
336        }
337    }
338
339    private void addAccountFromKey() {
340        try {
341            KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
342        } catch (ActivityNotFoundException e) {
343            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
344        }
345    }
346
347    private void publishAvatar(Account account) {
348        Intent intent = new Intent(getApplicationContext(),
349                PublishProfilePictureActivity.class);
350        intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
351        startActivity(intent);
352    }
353
354    private void disableAllAccounts() {
355        List<Account> list = new ArrayList<>();
356        synchronized (this.accountList) {
357            for (Account account : this.accountList) {
358                if (account.isEnabled()) {
359                    list.add(account);
360                }
361            }
362        }
363        for (Account account : list) {
364            disableAccount(account);
365        }
366    }
367
368    private boolean accountsLeftToDisable() {
369        synchronized (this.accountList) {
370            for (Account account : this.accountList) {
371                if (account.isEnabled()) {
372                    return true;
373                }
374            }
375            return false;
376        }
377    }
378
379    private boolean accountsLeftToEnable() {
380        synchronized (this.accountList) {
381            for (Account account : this.accountList) {
382                if (!account.isEnabled()) {
383                    return true;
384                }
385            }
386            return false;
387        }
388    }
389
390    private void enableAllAccounts() {
391        List<Account> list = new ArrayList<>();
392        synchronized (this.accountList) {
393            for (Account account : this.accountList) {
394                if (!account.isEnabled()) {
395                    list.add(account);
396                }
397            }
398        }
399        for (Account account : list) {
400            enableAccount(account);
401        }
402    }
403
404    private void disableAccount(Account account) {
405        Resolver.clearCache();
406        account.setOption(Account.OPTION_DISABLED, true);
407        if (!xmppConnectionService.updateAccount(account)) {
408            Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
409        }
410    }
411
412    private void enableAccount(Account account) {
413        account.setOption(Account.OPTION_DISABLED, false);
414        final XmppConnection connection = account.getXmppConnection();
415        if (connection != null) {
416            connection.resetEverything();
417        }
418        if (!xmppConnectionService.updateAccount(account)) {
419            Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
420        }
421    }
422
423    private void publishOpenPGPPublicKey(Account account) {
424        if (ManageAccountActivity.this.hasPgp()) {
425            announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
426        } else {
427            this.showInstallPgpDialog();
428        }
429    }
430
431    @Override
432    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
433        super.onActivityResult(requestCode, resultCode, data);
434        if (resultCode == RESULT_OK) {
435            if (xmppConnectionServiceBound) {
436                if (requestCode == REQUEST_CHOOSE_PGP_ID) {
437                    if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
438                        selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
439                        announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
440                    } else {
441                        choosePgpSignId(selectedAccount);
442                    }
443                } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
444                    announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
445                }
446                this.mPostponedActivityResult = null;
447            } else {
448                this.mPostponedActivityResult = new Pair<>(requestCode, data);
449            }
450        }
451    }
452
453    @Override
454    public void alias(final String alias) {
455        if (alias != null) {
456            xmppConnectionService.createAccountFromKey(alias, this);
457        }
458    }
459
460    @Override
461    public void onAccountCreated(final Account account) {
462        final Intent intent = new Intent(this, EditAccountActivity.class);
463        intent.putExtra("jid", account.getJid().asBareJid().toString());
464        intent.putExtra("init", true);
465        startActivity(intent);
466    }
467
468    @Override
469    public void informUser(final int r) {
470        runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
471    }
472}