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