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 try {
311 final Intent fallback = new Intent();
312 fallback.setComponent(new ComponentName("com.google.android.telecomui",
313 "com.android.server.telecomui.settings.EnableAccountPreferenceActivity"));
314 startActivity(fallback);
315 } catch (final android.content.ActivityNotFoundException e2) {
316 Toast.makeText(this, "Your OS has blocked dialler integration", Toast.LENGTH_SHORT).show();
317 }
318 }
319 mMicIntent = null;
320 return;
321 case REQUEST_IMPORT_BACKUP:
322 startActivity(new Intent(this, ImportBackupActivity.class));
323 break;
324 }
325 } else {
326 if (requestCode == REQUEST_MICROPHONE) {
327 Toast.makeText(this, "Microphone access was denied", Toast.LENGTH_SHORT).show();
328 } else {
329 Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
330 }
331 }
332 }
333 if (writeGranted(grantResults, permissions)) {
334 if (xmppConnectionService != null) {
335 xmppConnectionService.restartFileObserver();
336 }
337 }
338 }
339
340 @Override
341 public boolean onNavigateUp() {
342 if (xmppConnectionService.getConversations().size() == 0) {
343 Intent contactsIntent = new Intent(this,
344 StartConversationActivity.class);
345 contactsIntent.setFlags(
346 // if activity exists in stack, pop the stack and go back to it
347 Intent.FLAG_ACTIVITY_CLEAR_TOP |
348 // otherwise, make a new task for it
349 Intent.FLAG_ACTIVITY_NEW_TASK |
350 // don't use the new activity animation; finish
351 // animation runs instead
352 Intent.FLAG_ACTIVITY_NO_ANIMATION);
353 startActivity(contactsIntent);
354 finish();
355 return true;
356 } else {
357 return super.onNavigateUp();
358 }
359 }
360
361 @Override
362 public void onClickTglAccountState(Account account, boolean enable) {
363 if (enable) {
364 enableAccount(account);
365 } else {
366 disableAccount(account);
367 }
368 }
369
370 private void addAccountFromKey() {
371 try {
372 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
373 } catch (ActivityNotFoundException e) {
374 Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
375 }
376 }
377
378 private void publishAvatar(Account account) {
379 Intent intent = new Intent(getApplicationContext(),
380 PublishProfilePictureActivity.class);
381 intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
382 startActivity(intent);
383 }
384
385 private void disableAllAccounts() {
386 List<Account> list = new ArrayList<>();
387 synchronized (this.accountList) {
388 for (Account account : this.accountList) {
389 if (account.isEnabled()) {
390 list.add(account);
391 }
392 }
393 }
394 for (Account account : list) {
395 disableAccount(account);
396 }
397 }
398
399 private boolean accountsLeftToDisable() {
400 synchronized (this.accountList) {
401 for (Account account : this.accountList) {
402 if (account.isEnabled()) {
403 return true;
404 }
405 }
406 return false;
407 }
408 }
409
410 private boolean accountsLeftToEnable() {
411 synchronized (this.accountList) {
412 for (Account account : this.accountList) {
413 if (!account.isEnabled()) {
414 return true;
415 }
416 }
417 return false;
418 }
419 }
420
421 private void enableAllAccounts() {
422 List<Account> list = new ArrayList<>();
423 synchronized (this.accountList) {
424 for (Account account : this.accountList) {
425 if (!account.isEnabled()) {
426 list.add(account);
427 }
428 }
429 }
430 for (Account account : list) {
431 enableAccount(account);
432 }
433 }
434
435 private void disableAccount(Account account) {
436 Resolver.clearCache();
437 account.setOption(Account.OPTION_DISABLED, true);
438 if (!xmppConnectionService.updateAccount(account)) {
439 Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
440 }
441 }
442
443 private void enableAccount(Account account) {
444 account.setOption(Account.OPTION_DISABLED, false);
445 final XmppConnection connection = account.getXmppConnection();
446 if (connection != null) {
447 connection.resetEverything();
448 }
449 if (!xmppConnectionService.updateAccount(account)) {
450 Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
451 }
452 }
453
454 private void publishOpenPGPPublicKey(Account account) {
455 if (ManageAccountActivity.this.hasPgp()) {
456 announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
457 } else {
458 this.showInstallPgpDialog();
459 }
460 }
461
462 @Override
463 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
464 super.onActivityResult(requestCode, resultCode, data);
465 if (resultCode == RESULT_OK) {
466 if (xmppConnectionServiceBound) {
467 if (requestCode == REQUEST_CHOOSE_PGP_ID) {
468 if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
469 selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
470 announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
471 } else {
472 choosePgpSignId(selectedAccount);
473 }
474 } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
475 announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
476 }
477 this.mPostponedActivityResult = null;
478 } else {
479 this.mPostponedActivityResult = new Pair<>(requestCode, data);
480 }
481 }
482 }
483
484 @Override
485 public void alias(final String alias) {
486 if (alias != null) {
487 xmppConnectionService.createAccountFromKey(alias, this);
488 }
489 }
490
491 @Override
492 public void onAccountCreated(final Account account) {
493 final Intent intent = new Intent(this, EditAccountActivity.class);
494 intent.putExtra("jid", account.getJid().asBareJid().toString());
495 intent.putExtra("init", true);
496 startActivity(intent);
497 }
498
499 @Override
500 public void informUser(final int r) {
501 runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
502 }
503}