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