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