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