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}