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}