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