ManageAccountActivity.java

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