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