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