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