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