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