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) -> switchToAccount(accountList.get(position)));
140
141
142 LayoutInflater inflater = getLayoutInflater();
143 phoneAccountsFooter = (RelativeLayout) inflater.inflate(
144 R.layout.footer_manage_phone_accounts,
145 accountListView,
146 false
147 );
148 accountListView.addFooterView(phoneAccountsFooter);
149
150 registerForContextMenu(accountListView);
151 }
152
153 @Override
154 public void onSaveInstanceState(final Bundle savedInstanceState) {
155 if (selectedAccount != null) {
156 savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toString());
157 }
158 super.onSaveInstanceState(savedInstanceState);
159 }
160
161 @Override
162 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
163 super.onCreateContextMenu(menu, v, menuInfo);
164 ManageAccountActivity.this.getMenuInflater().inflate(
165 R.menu.manageaccounts_context, menu);
166 AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
167 this.selectedAccount = accountList.get(acmi.position);
168 if (this.selectedAccount.isEnabled()) {
169 menu.findItem(R.id.mgmt_account_enable).setVisible(false);
170 menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
171 } else {
172 menu.findItem(R.id.mgmt_account_disable).setVisible(false);
173 menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
174 menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
175 }
176 menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toString());
177 }
178
179 @Override
180 protected void onBackendConnected() {
181 if (selectedAccountJid != null) {
182 this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
183 }
184 refreshUiReal();
185 if (this.mPostponedActivityResult != null) {
186 this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
187 }
188 if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
189 if (mInvokedAddAccount.compareAndSet(false, true)) {
190 addAccountFromKey();
191 }
192 }
193 }
194
195 @Override
196 public boolean onCreateOptionsMenu(Menu menu) {
197 getMenuInflater().inflate(R.menu.manageaccounts, menu);
198 MenuItem enableAll = menu.findItem(R.id.action_enable_all);
199 MenuItem addAccount = menu.findItem(R.id.action_add_account);
200 MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
201
202 if (Config.X509_VERIFICATION) {
203 addAccount.setVisible(false);
204 addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
205 }
206
207 if (!accountsLeftToEnable()) {
208 enableAll.setVisible(false);
209 }
210 MenuItem disableAll = menu.findItem(R.id.action_disable_all);
211 if (!accountsLeftToDisable()) {
212 disableAll.setVisible(false);
213 }
214 return true;
215 }
216
217 @Override
218 public boolean onContextItemSelected(MenuItem item) {
219 switch (item.getItemId()) {
220 case R.id.mgmt_account_publish_avatar:
221 publishAvatar(selectedAccount);
222 return true;
223 case R.id.mgmt_account_disable:
224 disableAccount(selectedAccount);
225 return true;
226 case R.id.mgmt_account_enable:
227 enableAccount(selectedAccount);
228 return true;
229 case R.id.mgmt_account_delete:
230 deleteAccount(selectedAccount);
231 return true;
232 case R.id.mgmt_account_announce_pgp:
233 publishOpenPGPPublicKey(selectedAccount);
234 return true;
235 default:
236 return super.onContextItemSelected(item);
237 }
238 }
239
240 @Override
241 public boolean onOptionsItemSelected(MenuItem item) {
242 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
243 return false;
244 }
245 switch (item.getItemId()) {
246 case R.id.action_add_account:
247 startActivity(new Intent(this, EditAccountActivity.class));
248 break;
249 case R.id.action_import_backup:
250 if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
251 startActivity(new Intent(this, ImportBackupActivity.class));
252 }
253 break;
254 case R.id.action_disable_all:
255 disableAllAccounts();
256 break;
257 case R.id.action_enable_all:
258 enableAllAccounts();
259 break;
260 case R.id.action_add_account_with_cert:
261 addAccountFromKey();
262 break;
263 default:
264 break;
265 }
266 return super.onOptionsItemSelected(item);
267 }
268
269 private void requestMicPermission() {
270 final String[] permissions;
271 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
272 permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
273 } else {
274 permissions = new String[]{Manifest.permission.RECORD_AUDIO};
275 }
276 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
277 if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED && shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
278 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
279 builder.setTitle("Dialler Integration");
280 builder.setMessage("You will be asked to grant microphone permission, which is needed for the dialler integration to function.");
281 builder.setPositiveButton("I Understand", (dialog, which) -> {
282 requestPermissions(permissions, REQUEST_MICROPHONE);
283 });
284 builder.setCancelable(true);
285 final AlertDialog dialog = builder.create();
286 dialog.setCanceledOnTouchOutside(true);
287 dialog.show();
288 } else {
289 requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_MICROPHONE);
290 }
291 }
292 }
293
294 @Override
295 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
296 if (grantResults.length > 0) {
297 if (allGranted(grantResults)) {
298 switch (requestCode) {
299 case REQUEST_MICROPHONE:
300 try {
301 startActivity(mMicIntent);
302 } catch (final android.content.ActivityNotFoundException e) {
303 Toast.makeText(this, "Your OS has blocked dialler integration", Toast.LENGTH_SHORT).show();
304 }
305 mMicIntent = null;
306 return;
307 case REQUEST_IMPORT_BACKUP:
308 startActivity(new Intent(this, ImportBackupActivity.class));
309 break;
310 }
311 } else {
312 if (requestCode == REQUEST_MICROPHONE) {
313 Toast.makeText(this, "Microphone access was denied", Toast.LENGTH_SHORT).show();
314 } else {
315 Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
316 }
317 }
318 }
319 if (writeGranted(grantResults, permissions)) {
320 if (xmppConnectionService != null) {
321 xmppConnectionService.restartFileObserver();
322 }
323 }
324 }
325
326 @Override
327 public boolean onNavigateUp() {
328 if (xmppConnectionService.getConversations().size() == 0) {
329 Intent contactsIntent = new Intent(this,
330 StartConversationActivity.class);
331 contactsIntent.setFlags(
332 // if activity exists in stack, pop the stack and go back to it
333 Intent.FLAG_ACTIVITY_CLEAR_TOP |
334 // otherwise, make a new task for it
335 Intent.FLAG_ACTIVITY_NEW_TASK |
336 // don't use the new activity animation; finish
337 // animation runs instead
338 Intent.FLAG_ACTIVITY_NO_ANIMATION);
339 startActivity(contactsIntent);
340 finish();
341 return true;
342 } else {
343 return super.onNavigateUp();
344 }
345 }
346
347 @Override
348 public void onClickTglAccountState(Account account, boolean enable) {
349 if (enable) {
350 enableAccount(account);
351 } else {
352 disableAccount(account);
353 }
354 }
355
356 private void addAccountFromKey() {
357 try {
358 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
359 } catch (ActivityNotFoundException e) {
360 Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
361 }
362 }
363
364 private void publishAvatar(Account account) {
365 Intent intent = new Intent(getApplicationContext(),
366 PublishProfilePictureActivity.class);
367 intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
368 startActivity(intent);
369 }
370
371 private void disableAllAccounts() {
372 List<Account> list = new ArrayList<>();
373 synchronized (this.accountList) {
374 for (Account account : this.accountList) {
375 if (account.isEnabled()) {
376 list.add(account);
377 }
378 }
379 }
380 for (Account account : list) {
381 disableAccount(account);
382 }
383 }
384
385 private boolean accountsLeftToDisable() {
386 synchronized (this.accountList) {
387 for (Account account : this.accountList) {
388 if (account.isEnabled()) {
389 return true;
390 }
391 }
392 return false;
393 }
394 }
395
396 private boolean accountsLeftToEnable() {
397 synchronized (this.accountList) {
398 for (Account account : this.accountList) {
399 if (!account.isEnabled()) {
400 return true;
401 }
402 }
403 return false;
404 }
405 }
406
407 private void enableAllAccounts() {
408 List<Account> list = new ArrayList<>();
409 synchronized (this.accountList) {
410 for (Account account : this.accountList) {
411 if (!account.isEnabled()) {
412 list.add(account);
413 }
414 }
415 }
416 for (Account account : list) {
417 enableAccount(account);
418 }
419 }
420
421 private void disableAccount(Account account) {
422 Resolver.clearCache();
423 account.setOption(Account.OPTION_DISABLED, true);
424 if (!xmppConnectionService.updateAccount(account)) {
425 Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
426 }
427 }
428
429 private void enableAccount(Account account) {
430 account.setOption(Account.OPTION_DISABLED, false);
431 final XmppConnection connection = account.getXmppConnection();
432 if (connection != null) {
433 connection.resetEverything();
434 }
435 if (!xmppConnectionService.updateAccount(account)) {
436 Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
437 }
438 }
439
440 private void publishOpenPGPPublicKey(Account account) {
441 if (ManageAccountActivity.this.hasPgp()) {
442 announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
443 } else {
444 this.showInstallPgpDialog();
445 }
446 }
447
448 @Override
449 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
450 super.onActivityResult(requestCode, resultCode, data);
451 if (resultCode == RESULT_OK) {
452 if (xmppConnectionServiceBound) {
453 if (requestCode == REQUEST_CHOOSE_PGP_ID) {
454 if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
455 selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
456 announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
457 } else {
458 choosePgpSignId(selectedAccount);
459 }
460 } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
461 announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
462 }
463 this.mPostponedActivityResult = null;
464 } else {
465 this.mPostponedActivityResult = new Pair<>(requestCode, data);
466 }
467 }
468 }
469
470 @Override
471 public void alias(final String alias) {
472 if (alias != null) {
473 xmppConnectionService.createAccountFromKey(alias, this);
474 }
475 }
476
477 @Override
478 public void onAccountCreated(final Account account) {
479 final Intent intent = new Intent(this, EditAccountActivity.class);
480 intent.putExtra("jid", account.getJid().asBareJid().toString());
481 intent.putExtra("init", true);
482 startActivity(intent);
483 }
484
485 @Override
486 public void informUser(final int r) {
487 runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
488 }
489}