1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.content.Context;
5import android.content.Intent;
6import android.content.SharedPreferences;
7import android.os.Bundle;
8import android.view.ActionMode;
9import android.view.KeyEvent;
10import android.view.Menu;
11import android.view.MenuItem;
12import android.view.SoundEffectConstants;
13import android.view.View;
14import android.view.inputmethod.InputMethodManager;
15import android.widget.AbsListView.MultiChoiceModeListener;
16import android.widget.AdapterView;
17import android.widget.ListView;
18import android.widget.TextView;
19import androidx.annotation.NonNull;
20import androidx.annotation.StringRes;
21import androidx.appcompat.app.ActionBar;
22import androidx.fragment.app.Fragment;
23import androidx.fragment.app.FragmentTransaction;
24import com.google.common.base.Strings;
25import eu.siacs.conversations.R;
26import eu.siacs.conversations.entities.Account;
27import eu.siacs.conversations.entities.Contact;
28import eu.siacs.conversations.entities.Conversation;
29import eu.siacs.conversations.entities.ListItem;
30import eu.siacs.conversations.entities.MucOptions;
31import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
32import eu.siacs.conversations.ui.util.ActivityResult;
33import eu.siacs.conversations.ui.util.PendingItem;
34import eu.siacs.conversations.utils.XmppUri;
35import eu.siacs.conversations.xmpp.Jid;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Collections;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Set;
42
43public class ChooseContactActivity extends AbstractSearchableListItemActivity
44 implements MultiChoiceModeListener, AdapterView.OnItemClickListener {
45 public static final String EXTRA_TITLE_RES_ID = "extra_title_res_id";
46 public static final String EXTRA_GROUP_CHAT_NAME = "extra_group_chat_name";
47 public static final String EXTRA_SELECT_MULTIPLE = "extra_select_multiple";
48 public static final String EXTRA_SHOW_ENTER_JID = "extra_show_enter_jid";
49 public static final String EXTRA_CONVERSATION = "extra_conversation";
50 private static final String EXTRA_FILTERED_CONTACTS = "extra_filtered_contacts";
51 private final ArrayList<String> mActivatedAccounts = new ArrayList<>();
52 private final Set<String> selected = new HashSet<>();
53 private Set<String> filterContacts;
54 private Set<ListItem> extraContacts = new HashSet<>();
55
56 private boolean showEnterJid = false;
57 private boolean startSearching = false;
58 private boolean multiple = false;
59
60 private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
61
62 public static Intent create(Activity activity, Conversation conversation) {
63 final Intent intent = new Intent(activity, ChooseContactActivity.class);
64 List<String> contacts = new ArrayList<>();
65 if (conversation.getMode() == Conversation.MODE_MULTI) {
66 for (MucOptions.User user : conversation.getMucOptions().getUsers(false)) {
67 Jid jid = user.getRealJid();
68 if (jid != null) {
69 contacts.add(jid.asBareJid().toString());
70 }
71 }
72 } else {
73 contacts.add(conversation.getJid().asBareJid().toString());
74 }
75 intent.putExtra(EXTRA_FILTERED_CONTACTS, contacts.toArray(new String[0]));
76 intent.putExtra(EXTRA_CONVERSATION, conversation.getUuid());
77 intent.putExtra(EXTRA_SELECT_MULTIPLE, true);
78 intent.putExtra(EXTRA_SHOW_ENTER_JID, true);
79 intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
80 return intent;
81 }
82
83 public static List<Jid> extractJabberIds(Intent result) {
84 List<Jid> jabberIds = new ArrayList<>();
85 try {
86 if (result.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false)) {
87 String[] toAdd = result.getStringArrayExtra("contacts");
88 for (String item : toAdd) {
89 jabberIds.add(Jid.of(item));
90 }
91 } else {
92 jabberIds.add(Jid.of(result.getStringExtra("contact")));
93 }
94 return jabberIds;
95 } catch (IllegalArgumentException e) {
96 return jabberIds;
97 }
98 }
99
100 @Override
101 public void onCreate(final Bundle savedInstanceState) {
102 super.onCreate(savedInstanceState);
103 filterContacts = new HashSet<>();
104 if (savedInstanceState != null) {
105 String[] selectedContacts = savedInstanceState.getStringArray("selected_contacts");
106 if (selectedContacts != null) {
107 selected.clear();
108 selected.addAll(Arrays.asList(selectedContacts));
109 }
110 }
111
112 String[] contacts = getIntent().getStringArrayExtra(EXTRA_FILTERED_CONTACTS);
113 if (contacts != null) {
114 Collections.addAll(filterContacts, contacts);
115 }
116
117 Intent intent = getIntent();
118
119 multiple = intent.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false);
120 if (multiple) {
121 getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
122 getListView().setMultiChoiceModeListener(this);
123 }
124
125 getListView().setOnItemClickListener(this);
126 this.showEnterJid = intent.getBooleanExtra(EXTRA_SHOW_ENTER_JID, false);
127 this.binding.fab.setOnClickListener(this::onFabClicked);
128 if (this.showEnterJid) {
129 this.binding.fab.show();
130 } else {
131 binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
132 }
133
134 final SharedPreferences preferences = getPreferences();
135 this.startSearching =
136 intent.getBooleanExtra("direct_search", false)
137 && preferences.getBoolean(
138 "start_searching",
139 getResources().getBoolean(R.bool.start_searching));
140
141 getListItemAdapter().refreshSettings();
142 getListItemAdapter().setOnTagClickedListener((tag) -> {
143 if (mMenuSearchView != null) {
144 mMenuSearchView.expandActionView();
145 mSearchEditText.setText("");
146 mSearchEditText.append(tag);
147 filterContacts(tag);
148 }
149 });
150 }
151
152 private void onFabClicked(View v) {
153 if (selected.isEmpty()) {
154 showEnterJidDialog(null);
155 } else {
156 submitSelection();
157 }
158 }
159
160 @Override
161 public boolean colorCodeAccounts() {
162 return mActivatedAccounts.size() > 1;
163 }
164
165 @Override
166 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
167 return false;
168 }
169
170 @Override
171 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
172 mode.setTitle(getTitleFromIntent());
173 binding.chooseContactList.setFastScrollEnabled(false);
174 binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
175 binding.fab.show();
176 final View view = getSearchEditText();
177 final InputMethodManager imm =
178 (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
179 if (view != null && imm != null) {
180 imm.hideSoftInputFromWindow(
181 getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
182 }
183 return true;
184 }
185
186 @Override
187 public void onDestroyActionMode(ActionMode mode) {
188 this.binding.fab.setImageResource(R.drawable.ic_person_add_24dp);
189 if (this.showEnterJid) {
190 this.binding.fab.show();
191 } else {
192 this.binding.fab.hide();
193 }
194 binding.chooseContactList.setFastScrollEnabled(true);
195 selected.clear();
196 }
197
198 @Override
199 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
200 return false;
201 }
202
203 private void submitSelection() {
204 final Intent request = getIntent();
205 final Intent data = new Intent();
206 data.putExtra("contacts", getSelectedContactJids());
207 data.putExtra(EXTRA_SELECT_MULTIPLE, true);
208 data.putExtra(EXTRA_ACCOUNT, request.getStringExtra(EXTRA_ACCOUNT));
209 copy(request, data);
210 setResult(RESULT_OK, data);
211 finish();
212 }
213
214 private static void copy(Intent from, Intent to) {
215 to.putExtra(EXTRA_CONVERSATION, from.getStringExtra(EXTRA_CONVERSATION));
216 to.putExtra(EXTRA_GROUP_CHAT_NAME, from.getStringExtra(EXTRA_GROUP_CHAT_NAME));
217 }
218
219 @Override
220 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
221 if (selected.size() != 0) {
222 getListView().playSoundEffect(SoundEffectConstants.CLICK);
223 }
224 getListItemAdapter().notifyDataSetChanged();
225 Contact item = (Contact) getListItems().get(position);
226 if (checked) {
227 selected.add(item.getJid().toString());
228 } else {
229 selected.remove(item.getJid().toString());
230 }
231 }
232
233 @Override
234 public void onStart() {
235 super.onStart();
236 ActionBar bar = getSupportActionBar();
237 if (bar != null) {
238 try {
239 bar.setTitle(getTitleFromIntent());
240 } catch (Exception e) {
241 bar.setTitle(R.string.title_activity_choose_contact);
242 }
243 }
244 }
245
246 public @StringRes int getTitleFromIntent() {
247 final Intent intent = getIntent();
248 boolean multiple = intent != null && intent.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false);
249 @StringRes
250 int fallback =
251 multiple
252 ? R.string.title_activity_choose_contacts
253 : R.string.title_activity_choose_contact;
254 return intent != null ? intent.getIntExtra(EXTRA_TITLE_RES_ID, fallback) : fallback;
255 }
256
257 @Override
258 public boolean onCreateOptionsMenu(final Menu menu) {
259 super.onCreateOptionsMenu(menu);
260 final Intent i = getIntent();
261 boolean showEnterJid = i != null && i.getBooleanExtra(EXTRA_SHOW_ENTER_JID, false);
262 menu.findItem(R.id.action_scan_qr_code)
263 .setVisible(isCameraFeatureAvailable() && showEnterJid);
264 MenuItem mMenuSearchView = menu.findItem(R.id.action_search);
265 if (startSearching) {
266 mMenuSearchView.expandActionView();
267 }
268 return true;
269 }
270
271 @Override
272 public void onSaveInstanceState(Bundle savedInstanceState) {
273 savedInstanceState.putStringArray("selected_contacts", getSelectedContactJids());
274 super.onSaveInstanceState(savedInstanceState);
275 }
276
277 @Override
278 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
279 if (multiple) {
280 return false;
281 } else {
282 List<ListItem> items = getListItems();
283 if (items.size() == 1) {
284 onListItemClicked(items.get(0));
285 return true;
286 }
287 return false;
288 }
289 }
290
291 protected void filterContacts(final String needle) {
292 getListItems().clear();
293 if (xmppConnectionService == null) {
294 getListItemAdapter().notifyDataSetChanged();
295 return;
296 }
297 final var accounts = new ArrayList<Account>();
298 for (final var account : xmppConnectionService.getAccounts()) {
299 if (mActivatedAccounts.contains(account.getJid().asBareJid().toString())) accounts.add(account);
300 }
301 for (final var contact : extraContacts) {
302 if (!filterContacts.contains(contact.getJid().asBareJid().toString())
303 && contact.match(this, needle)) {
304 getListItems().add(contact);
305 }
306 }
307 for (final Account account : accounts) {
308 for (final Contact contact : account.getRoster().getContacts()) {
309 if (contact.showInContactList() &&
310 !filterContacts.contains(contact.getJid().asBareJid().toString())
311 && contact.match(this, needle)) {
312 getListItems().add(contact);
313 }
314 }
315
316 final Contact self = new Contact(account.getSelfContact());
317 self.setSystemName("Note to Self");
318 if (self.match(this, needle)) {
319 getListItems().add(self);
320 }
321 }
322 Collections.sort(getListItems());
323 getListItemAdapter().notifyDataSetChanged();
324 for (int i = 0; i < getListItemAdapter().getCount(); i++) {
325 getListView().setItemChecked(i, selected.contains(getListItemAdapter().getItem(i).getJid().toString()));
326 }
327 }
328
329 private String[] getSelectedContactJids() {
330 return selected.toArray(new String[0]);
331 }
332
333 public void refreshUiReal() {
334 // nothing to do. This Activity doesn't implement any listeners
335 }
336
337 @Override
338 public boolean onOptionsItemSelected(MenuItem item) {
339 switch (item.getItemId()) {
340 case R.id.action_scan_qr_code:
341 ScanActivity.scan(this);
342 return true;
343 }
344 return super.onOptionsItemSelected(item);
345 }
346
347 protected void showEnterJidDialog(XmppUri uri) {
348 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
349 Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog");
350 if (prev != null) {
351 ft.remove(prev);
352 }
353 ft.addToBackStack(null);
354 Jid jid = uri == null ? null : uri.getJid();
355 EnterJidDialog dialog = EnterJidDialog.newInstance(
356 mActivatedAccounts,
357 getString(R.string.enter_contact),
358 getString(R.string.select),
359 null,
360 jid == null ? null : jid.asBareJid().toString(),
361 getIntent().getStringExtra(EXTRA_ACCOUNT),
362 true,
363 false,
364 EnterJidDialog.SanityCheck.NO
365 );
366
367 dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
368 for (final Account account : xmppConnectionService.getAccounts()) {
369 if (account.getJid().asBareJid().equals(accountJid)) {
370 final var contact = account.getRoster().getContact(contactJid);
371 if (multiple) {
372 extraContacts.add(contact);
373 selected.add(contactJid.toString());
374 if (mMenuSearchView != null) {
375 binding.fab.postDelayed(() -> {
376 mMenuSearchView.expandActionView();
377 mSearchEditText.setText("");
378 mSearchEditText.append(contactJid.toString());
379 }, 200L);
380 filterContacts(contactJid.toString());
381 binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
382 }
383 } else {
384 onListItemClicked(contact);
385 }
386 }
387 }
388
389 return true;
390 });
391
392 dialog.show(ft, "dialog");
393 }
394
395 @Override
396 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
397 super.onActivityResult(requestCode, requestCode, intent);
398 ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, intent);
399 if (xmppConnectionService != null) {
400 handleActivityResult(activityResult);
401 } else {
402 this.postponedActivityResult.push(activityResult);
403 }
404 }
405
406 private void handleActivityResult(ActivityResult activityResult) {
407 if (activityResult.resultCode == RESULT_OK
408 && activityResult.requestCode == ScanActivity.REQUEST_SCAN_QR_CODE) {
409 String result = activityResult.data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
410 XmppUri uri = new XmppUri(Strings.nullToEmpty(result));
411 if (uri.isValidJid()) {
412 showEnterJidDialog(uri);
413 }
414 }
415 }
416
417 @Override
418 protected void onBackendConnected() {
419 this.mActivatedAccounts.clear();
420 final var selected = getIntent().getStringExtra(EXTRA_ACCOUNT);
421 for (final Account account : xmppConnectionService.getAccounts()) {
422 if (account.isEnabled() && (selected == null || selected.equals(account.getJid().asBareJid().toString()))) {
423 this.mActivatedAccounts.add(account.getJid().asBareJid().toString());
424 }
425 }
426 filterContacts();
427 ActivityResult activityResult = this.postponedActivityResult.pop();
428 if (activityResult != null) {
429 handleActivityResult(activityResult);
430 }
431 final Fragment fragment =
432 getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
433 if (fragment instanceof OnBackendConnected) {
434 ((OnBackendConnected) fragment).onBackendConnected();
435 }
436 }
437
438 @Override
439 public void onRequestPermissionsResult(
440 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
441 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
442 ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
443 }
444
445 @Override
446 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
447 if (multiple) {
448 if (getListView().isItemChecked(position)) {
449 selected.add(getListItemAdapter().getItem(position).getJid().toString());
450 } else {
451 selected.remove(getListItemAdapter().getItem(position).getJid().toString());
452 }
453
454 if (selected.isEmpty()) {
455 this.binding.fab.setImageResource(R.drawable.ic_person_add_24dp);
456 if (this.showEnterJid) {
457 this.binding.fab.show();
458 } else {
459 this.binding.fab.hide();
460 }
461 } else {
462 binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
463 binding.fab.show();
464 }
465
466 return;
467 }
468 final InputMethodManager imm =
469 (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
470 imm.hideSoftInputFromWindow(
471 getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
472 final ListItem mListItem = getListItems().get(position);
473 onListItemClicked(mListItem);
474 }
475
476 private void onListItemClicked(ListItem item) {
477 final Intent request = getIntent();
478 final Intent data = new Intent();
479 data.putExtra("contact", item.getJid().toString());
480 String account = request.getStringExtra(EXTRA_ACCOUNT);
481 if (account == null && item instanceof Contact) {
482 account = ((Contact) item).getAccount().getJid().asBareJid().toString();
483 }
484 data.putExtra(EXTRA_ACCOUNT, account);
485 data.putExtra(EXTRA_SELECT_MULTIPLE, false);
486 copy(request, data);
487 setResult(RESULT_OK, data);
488 finish();
489 }
490}