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