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