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