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