ChooseContactActivity.java

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