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