StartConversationActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.annotation.TargetApi;
   6import android.app.ActionBar;
   7import android.app.ActionBar.Tab;
   8import android.app.ActionBar.TabListener;
   9import android.app.AlertDialog;
  10import android.app.Dialog;
  11import android.app.Fragment;
  12import android.app.FragmentManager;
  13import android.app.FragmentTransaction;
  14import android.app.ListFragment;
  15import android.app.PendingIntent;
  16import android.content.ActivityNotFoundException;
  17import android.content.Context;
  18import android.content.DialogInterface;
  19import android.content.DialogInterface.OnClickListener;
  20import android.content.Intent;
  21import android.content.pm.PackageManager;
  22import android.net.Uri;
  23import android.nfc.NdefMessage;
  24import android.nfc.NdefRecord;
  25import android.nfc.NfcAdapter;
  26import android.os.Build;
  27import android.os.Bundle;
  28import android.os.Parcelable;
  29import android.support.v4.view.PagerAdapter;
  30import android.support.v4.view.ViewPager;
  31import android.text.Editable;
  32import android.text.SpannableString;
  33import android.text.Spanned;
  34import android.text.TextWatcher;
  35import android.text.style.TypefaceSpan;
  36import android.util.Pair;
  37import android.view.ContextMenu;
  38import android.view.ContextMenu.ContextMenuInfo;
  39import android.view.KeyEvent;
  40import android.view.Menu;
  41import android.view.MenuItem;
  42import android.view.View;
  43import android.view.ViewGroup;
  44import android.view.inputmethod.InputMethodManager;
  45import android.widget.AdapterView;
  46import android.widget.AdapterView.AdapterContextMenuInfo;
  47import android.widget.AdapterView.OnItemClickListener;
  48import android.widget.ArrayAdapter;
  49import android.widget.AutoCompleteTextView;
  50import android.widget.CheckBox;
  51import android.widget.Checkable;
  52import android.widget.EditText;
  53import android.widget.ListView;
  54import android.widget.Spinner;
  55import android.widget.TextView;
  56import android.widget.Toast;
  57
  58import com.google.zxing.integration.android.IntentIntegrator;
  59import com.google.zxing.integration.android.IntentResult;
  60
  61import java.util.ArrayList;
  62import java.util.Arrays;
  63import java.util.Collections;
  64import java.util.List;
  65import java.util.concurrent.atomic.AtomicBoolean;
  66
  67import eu.siacs.conversations.Config;
  68import eu.siacs.conversations.R;
  69import eu.siacs.conversations.entities.Account;
  70import eu.siacs.conversations.entities.Bookmark;
  71import eu.siacs.conversations.entities.Contact;
  72import eu.siacs.conversations.entities.Conversation;
  73import eu.siacs.conversations.entities.ListItem;
  74import eu.siacs.conversations.entities.Presence;
  75import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
  76import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
  77import eu.siacs.conversations.ui.adapter.ListItemAdapter;
  78import eu.siacs.conversations.utils.XmppUri;
  79import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  80import eu.siacs.conversations.xmpp.XmppConnection;
  81import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  82import eu.siacs.conversations.xmpp.jid.Jid;
  83
  84public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist {
  85
  86    public int conference_context_id;
  87    public int contact_context_id;
  88    private Tab mContactsTab;
  89    private Tab mConferencesTab;
  90    private ViewPager mViewPager;
  91    private ListPagerAdapter mListPagerAdapter;
  92    private List<ListItem> contacts = new ArrayList<>();
  93    private ArrayAdapter<ListItem> mContactsAdapter;
  94    private List<ListItem> conferences = new ArrayList<>();
  95    private ArrayAdapter<ListItem> mConferenceAdapter;
  96    private List<String> mActivatedAccounts = new ArrayList<>();
  97    private List<String> mKnownHosts;
  98    private List<String> mKnownConferenceHosts;
  99    private Invite mPendingInvite = null;
 100    private EditText mSearchEditText;
 101    private AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
 102    private final int REQUEST_SYNC_CONTACTS = 0x3b28cf;
 103    private final int REQUEST_CREATE_CONFERENCE = 0x3b39da;
 104    private Dialog mCurrentDialog = null;
 105
 106    private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
 107
 108        @Override
 109        public boolean onMenuItemActionExpand(MenuItem item) {
 110            mSearchEditText.post(new Runnable() {
 111
 112                @Override
 113                public void run() {
 114                    mSearchEditText.requestFocus();
 115                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 116                    imm.showSoftInput(mSearchEditText,
 117                            InputMethodManager.SHOW_IMPLICIT);
 118                }
 119            });
 120
 121            return true;
 122        }
 123
 124        @Override
 125        public boolean onMenuItemActionCollapse(MenuItem item) {
 126            hideKeyboard();
 127            mSearchEditText.setText("");
 128            filter(null);
 129            return true;
 130        }
 131    };
 132    private boolean mHideOfflineContacts = false;
 133    private TabListener mTabListener = new TabListener() {
 134
 135        @Override
 136        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
 137            return;
 138        }
 139
 140        @Override
 141        public void onTabSelected(Tab tab, FragmentTransaction ft) {
 142            mViewPager.setCurrentItem(tab.getPosition());
 143            onTabChanged();
 144        }
 145
 146        @Override
 147        public void onTabReselected(Tab tab, FragmentTransaction ft) {
 148            return;
 149        }
 150    };
 151    private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
 152        @Override
 153        public void onPageSelected(int position) {
 154            if (getActionBar() != null) {
 155                getActionBar().setSelectedNavigationItem(position);
 156            }
 157            onTabChanged();
 158        }
 159    };
 160    private TextWatcher mSearchTextWatcher = new TextWatcher() {
 161
 162        @Override
 163        public void afterTextChanged(Editable editable) {
 164            filter(editable.toString());
 165        }
 166
 167        @Override
 168        public void beforeTextChanged(CharSequence s, int start, int count,
 169                                      int after) {
 170        }
 171
 172        @Override
 173        public void onTextChanged(CharSequence s, int start, int before, int count) {
 174        }
 175    };
 176
 177    private TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
 178        @Override
 179        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 180            int pos = getActionBar().getSelectedNavigationIndex();
 181            if (pos == 0) {
 182                if (contacts.size() == 1) {
 183                    openConversationForContact((Contact) contacts.get(0));
 184                    return true;
 185                }
 186            } else {
 187                if (conferences.size() == 1) {
 188                    openConversationsForBookmark((Bookmark) conferences.get(0));
 189                    return true;
 190                }
 191            }
 192            hideKeyboard();
 193            mListPagerAdapter.requestFocus(pos);
 194            return true;
 195        }
 196    };
 197    private MenuItem mMenuSearchView;
 198    private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
 199        @Override
 200        public void onTagClicked(String tag) {
 201            if (mMenuSearchView != null) {
 202                mMenuSearchView.expandActionView();
 203                mSearchEditText.setText("");
 204                mSearchEditText.append(tag);
 205                filter(tag);
 206            }
 207        }
 208    };
 209    private String mInitialJid;
 210    private Pair<Integer, Intent> mPostponedActivityResult;
 211    private UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
 212        @Override
 213        public void success(final Conversation conversation) {
 214            runOnUiThread(new Runnable() {
 215                @Override
 216                public void run() {
 217                    hideToast();
 218                    switchToConversation(conversation);
 219                }
 220            });
 221        }
 222
 223        @Override
 224        public void error(final int errorCode, Conversation object) {
 225            runOnUiThread(new Runnable() {
 226                @Override
 227                public void run() {
 228                    replaceToast(getString(errorCode));
 229                }
 230            });
 231        }
 232
 233        @Override
 234        public void userInputRequried(PendingIntent pi, Conversation object) {
 235
 236        }
 237    };
 238    private Toast mToast;
 239
 240    protected void hideToast() {
 241        if (mToast != null) {
 242            mToast.cancel();
 243        }
 244    }
 245
 246    protected void replaceToast(String msg) {
 247        hideToast();
 248        mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
 249        mToast.show();
 250    }
 251
 252    @Override
 253    public void onRosterUpdate() {
 254        this.refreshUi();
 255    }
 256
 257    @Override
 258    public void onCreate(Bundle savedInstanceState) {
 259        super.onCreate(savedInstanceState);
 260        setContentView(R.layout.activity_start_conversation);
 261        mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager);
 262        ActionBar actionBar = getActionBar();
 263        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
 264
 265        mContactsTab = actionBar.newTab().setText(R.string.contacts)
 266                .setTabListener(mTabListener);
 267        mConferencesTab = actionBar.newTab().setText(R.string.conferences)
 268                .setTabListener(mTabListener);
 269        actionBar.addTab(mContactsTab);
 270        actionBar.addTab(mConferencesTab);
 271
 272        mViewPager.setOnPageChangeListener(mOnPageChangeListener);
 273        mListPagerAdapter = new ListPagerAdapter(getFragmentManager());
 274        mViewPager.setAdapter(mListPagerAdapter);
 275
 276        mConferenceAdapter = new ListItemAdapter(this, conferences);
 277        mContactsAdapter = new ListItemAdapter(this, contacts);
 278        ((ListItemAdapter) mContactsAdapter).setOnTagClickedListener(this.mOnTagClickedListener);
 279        this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false);
 280
 281    }
 282
 283    @Override
 284    public void onStart() {
 285        super.onStart();
 286        final int theme = findTheme();
 287        if (this.mTheme != theme) {
 288            recreate();
 289        } else {
 290            askForContactsPermissions();
 291        }
 292    }
 293
 294    @Override
 295    public void onStop() {
 296        if (mCurrentDialog != null) {
 297            mCurrentDialog.dismiss();
 298        }
 299        super.onStop();
 300    }
 301
 302    @Override
 303    public void onNewIntent(Intent intent) {
 304        if (xmppConnectionServiceBound) {
 305            handleIntent(intent);
 306        } else {
 307            setIntent(intent);
 308        }
 309    }
 310
 311    protected void openConversationForContact(int position) {
 312        Contact contact = (Contact) contacts.get(position);
 313        openConversationForContact(contact);
 314    }
 315
 316    protected void openConversationForContact(Contact contact) {
 317        Conversation conversation = xmppConnectionService
 318                .findOrCreateConversation(contact.getAccount(),
 319                        contact.getJid(), false, true);
 320        switchToConversation(conversation);
 321    }
 322
 323    protected void openConversationForContact() {
 324        int position = contact_context_id;
 325        openConversationForContact(position);
 326    }
 327
 328    protected void openConversationForBookmark() {
 329        openConversationForBookmark(conference_context_id);
 330    }
 331
 332    protected void openConversationForBookmark(int position) {
 333        Bookmark bookmark = (Bookmark) conferences.get(position);
 334        openConversationsForBookmark(bookmark);
 335    }
 336
 337    protected void shareBookmarkUri() {
 338        shareBookmarkUri(conference_context_id);
 339    }
 340
 341    protected void shareBookmarkUri(int position) {
 342        Bookmark bookmark = (Bookmark) conferences.get(position);
 343        Intent shareIntent = new Intent();
 344        shareIntent.setAction(Intent.ACTION_SEND);
 345        shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:"+bookmark.getJid().toBareJid().toString()+"?join");
 346        shareIntent.setType("text/plain");
 347        try {
 348            startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
 349        } catch (ActivityNotFoundException e) {
 350            Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
 351        }
 352    }
 353
 354    protected void openConversationsForBookmark(Bookmark bookmark) {
 355        Jid jid = bookmark.getJid();
 356        if (jid == null) {
 357            Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
 358            return;
 359        }
 360        Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
 361        conversation.setBookmark(bookmark);
 362        if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", true)) {
 363            bookmark.setAutojoin(true);
 364            xmppConnectionService.pushBookmarks(bookmark.getAccount());
 365        }
 366        switchToConversation(conversation);
 367    }
 368
 369    protected void openDetailsForContact() {
 370        int position = contact_context_id;
 371        Contact contact = (Contact) contacts.get(position);
 372        switchToContactDetails(contact);
 373    }
 374
 375    protected void toggleContactBlock() {
 376        final int position = contact_context_id;
 377        BlockContactDialog.show(this, (Contact) contacts.get(position));
 378    }
 379
 380    protected void deleteContact() {
 381        final int position = contact_context_id;
 382        final Contact contact = (Contact) contacts.get(position);
 383        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 384        builder.setNegativeButton(R.string.cancel, null);
 385        builder.setTitle(R.string.action_delete_contact);
 386        builder.setMessage(getString(R.string.remove_contact_text,
 387                contact.getJid()));
 388        builder.setPositiveButton(R.string.delete, new OnClickListener() {
 389
 390            @Override
 391            public void onClick(DialogInterface dialog, int which) {
 392                xmppConnectionService.deleteContactOnServer(contact);
 393                filter(mSearchEditText.getText().toString());
 394            }
 395        });
 396        builder.create().show();
 397    }
 398
 399    protected void deleteConference() {
 400        int position = conference_context_id;
 401        final Bookmark bookmark = (Bookmark) conferences.get(position);
 402
 403        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 404        builder.setNegativeButton(R.string.cancel, null);
 405        builder.setTitle(R.string.delete_bookmark);
 406        builder.setMessage(getString(R.string.remove_bookmark_text,
 407                bookmark.getJid()));
 408        builder.setPositiveButton(R.string.delete, new OnClickListener() {
 409
 410            @Override
 411            public void onClick(DialogInterface dialog, int which) {
 412                bookmark.unregisterConversation();
 413                Account account = bookmark.getAccount();
 414                account.getBookmarks().remove(bookmark);
 415                xmppConnectionService.pushBookmarks(account);
 416                filter(mSearchEditText.getText().toString());
 417            }
 418        });
 419        builder.create().show();
 420
 421    }
 422
 423    @SuppressLint("InflateParams")
 424    protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
 425        EnterJidDialog dialog = new EnterJidDialog(
 426                this, mKnownHosts, mActivatedAccounts,
 427                getString(R.string.create_contact), getString(R.string.create),
 428                prefilledJid, null, invite == null || !invite.hasFingerprints()
 429        );
 430
 431        dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
 432            @Override
 433            public boolean onEnterJidDialogPositive(Jid accountJid, Jid contactJid) throws EnterJidDialog.JidError {
 434                if (!xmppConnectionServiceBound) {
 435                    return false;
 436                }
 437
 438                final Account account = xmppConnectionService.findAccountByJid(accountJid);
 439                if (account == null) {
 440                    return true;
 441                }
 442
 443                final Contact contact = account.getRoster().getContact(contactJid);
 444                if (contact.showInRoster()) {
 445                    throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
 446                } else {
 447                    xmppConnectionService.createContact(contact);
 448                    if (invite != null && invite.hasFingerprints()) {
 449                        xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
 450                    }
 451                    switchToConversation(contact, invite == null ? null : invite.getBody());
 452                    return true;
 453                }
 454            }
 455        });
 456
 457        mCurrentDialog = dialog.show();
 458    }
 459
 460    @SuppressLint("InflateParams")
 461    protected void showJoinConferenceDialog(final String prefilledJid) {
 462        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 463        builder.setTitle(R.string.join_conference);
 464        final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null);
 465        final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
 466        final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
 467        final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
 468        jabberIdDesc.setText(R.string.conference_address);
 469        jid.setHint(R.string.conference_address_example);
 470        jid.setAdapter(new KnownHostsAdapter(this, R.layout.simple_list_item, mKnownConferenceHosts));
 471        if (prefilledJid != null) {
 472            jid.append(prefilledJid);
 473        }
 474        populateAccountSpinner(this, mActivatedAccounts, spinner);
 475        final Checkable bookmarkCheckBox = (CheckBox) dialogView
 476                .findViewById(R.id.bookmark);
 477        builder.setView(dialogView);
 478        builder.setNegativeButton(R.string.cancel, null);
 479        builder.setPositiveButton(R.string.join, null);
 480        final AlertDialog dialog = builder.create();
 481        dialog.show();
 482        mCurrentDialog = dialog;
 483        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
 484                new View.OnClickListener() {
 485
 486                    @Override
 487                    public void onClick(final View v) {
 488                        if (!xmppConnectionServiceBound) {
 489                            return;
 490                        }
 491                        final Account account = getSelectedAccount(spinner);
 492                        if (account == null) {
 493                            return;
 494                        }
 495                        final Jid conferenceJid;
 496                        try {
 497                            conferenceJid = Jid.fromString(jid.getText().toString());
 498                        } catch (final InvalidJidException e) {
 499                            jid.setError(getString(R.string.invalid_jid));
 500                            return;
 501                        }
 502
 503                        if (bookmarkCheckBox.isChecked()) {
 504                            if (account.hasBookmarkFor(conferenceJid)) {
 505                                jid.setError(getString(R.string.bookmark_already_exists));
 506                            } else {
 507                                final Bookmark bookmark = new Bookmark(account, conferenceJid.toBareJid());
 508                                bookmark.setAutojoin(getPreferences().getBoolean("autojoin", true));
 509                                String nick = conferenceJid.getResourcepart();
 510                                if (nick != null && !nick.isEmpty()) {
 511                                    bookmark.setNick(nick);
 512                                }
 513                                account.getBookmarks().add(bookmark);
 514                                xmppConnectionService.pushBookmarks(account);
 515                                final Conversation conversation = xmppConnectionService
 516                                        .findOrCreateConversation(account, conferenceJid, true, true, true);
 517                                conversation.setBookmark(bookmark);
 518                                dialog.dismiss();
 519                                mCurrentDialog = null;
 520                                switchToConversation(conversation);
 521                            }
 522                        } else {
 523                            final Conversation conversation = xmppConnectionService
 524                                    .findOrCreateConversation(account,conferenceJid, true, true, true);
 525                            dialog.dismiss();
 526                            mCurrentDialog = null;
 527                            switchToConversation(conversation);
 528                        }
 529                    }
 530                });
 531    }
 532
 533    private void showCreateConferenceDialog() {
 534        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 535        builder.setTitle(R.string.create_conference);
 536        final View dialogView = getLayoutInflater().inflate(R.layout.create_conference_dialog, null);
 537        final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
 538        final EditText subject = (EditText) dialogView.findViewById(R.id.subject);
 539        populateAccountSpinner(this, mActivatedAccounts, spinner);
 540        builder.setView(dialogView);
 541        builder.setPositiveButton(R.string.choose_participants, new OnClickListener() {
 542            @Override
 543            public void onClick(DialogInterface dialog, int which) {
 544                if (!xmppConnectionServiceBound) {
 545                    return;
 546                }
 547                final Account account = getSelectedAccount(spinner);
 548                if (account == null) {
 549                    return;
 550                }
 551                Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
 552                intent.putExtra("multiple", true);
 553                intent.putExtra("show_enter_jid", true);
 554                intent.putExtra("subject", subject.getText().toString());
 555                intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
 556                intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
 557                startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
 558            }
 559        });
 560        builder.setNegativeButton(R.string.cancel, null);
 561        mCurrentDialog = builder.create();
 562        mCurrentDialog.show();
 563    }
 564
 565    private Account getSelectedAccount(Spinner spinner) {
 566        if (!spinner.isEnabled()) {
 567            return null;
 568        }
 569        Jid jid;
 570        try {
 571            if (Config.DOMAIN_LOCK != null) {
 572                jid = Jid.fromParts((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
 573            } else {
 574                jid = Jid.fromString((String) spinner.getSelectedItem());
 575            }
 576        } catch (final InvalidJidException e) {
 577            return null;
 578        }
 579        return xmppConnectionService.findAccountByJid(jid);
 580    }
 581
 582    protected void switchToConversation(Contact contact, String body) {
 583        Conversation conversation = xmppConnectionService
 584                .findOrCreateConversation(contact.getAccount(),
 585                        contact.getJid(),false,true);
 586        switchToConversation(conversation, body, false);
 587    }
 588
 589    public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
 590        if (accounts.size() > 0) {
 591            ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
 592            adapter.setDropDownViewResource(R.layout.simple_list_item);
 593            spinner.setAdapter(adapter);
 594            spinner.setEnabled(true);
 595        } else {
 596            ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
 597                    R.layout.simple_list_item,
 598                    Arrays.asList(new String[]{context.getString(R.string.no_accounts)}));
 599            adapter.setDropDownViewResource(R.layout.simple_list_item);
 600            spinner.setAdapter(adapter);
 601            spinner.setEnabled(false);
 602        }
 603    }
 604
 605    @Override
 606    public boolean onCreateOptionsMenu(Menu menu) {
 607        getMenuInflater().inflate(R.menu.start_conversation, menu);
 608        MenuItem menuCreateContact = menu.findItem(R.id.action_create_contact);
 609        MenuItem menuCreateConference = menu.findItem(R.id.action_conference);
 610        MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
 611        menuHideOffline.setChecked(this.mHideOfflineContacts);
 612        mMenuSearchView = menu.findItem(R.id.action_search);
 613        mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
 614        View mSearchView = mMenuSearchView.getActionView();
 615        mSearchEditText = (EditText) mSearchView
 616                .findViewById(R.id.search_field);
 617        mSearchEditText.addTextChangedListener(mSearchTextWatcher);
 618        mSearchEditText.setOnEditorActionListener(mSearchDone);
 619        if (getActionBar().getSelectedNavigationIndex() == 0) {
 620            menuCreateConference.setVisible(false);
 621        } else {
 622            menuCreateContact.setVisible(false);
 623        }
 624        if (mInitialJid != null) {
 625            mMenuSearchView.expandActionView();
 626            mSearchEditText.append(mInitialJid);
 627            filter(mInitialJid);
 628        }
 629        return super.onCreateOptionsMenu(menu);
 630    }
 631
 632    @Override
 633    public boolean onOptionsItemSelected(MenuItem item) {
 634        switch (item.getItemId()) {
 635            case R.id.action_create_contact:
 636                showCreateContactDialog(null, null);
 637                return true;
 638            case R.id.action_join_conference:
 639                showJoinConferenceDialog(null);
 640                return true;
 641            case R.id.action_create_conference:
 642                showCreateConferenceDialog();
 643                return true;
 644            case R.id.action_scan_qr_code:
 645                new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
 646                return true;
 647            case R.id.action_hide_offline:
 648                mHideOfflineContacts = !item.isChecked();
 649                getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit();
 650                if (mSearchEditText != null) {
 651                    filter(mSearchEditText.getText().toString());
 652                }
 653                invalidateOptionsMenu();
 654        }
 655        return super.onOptionsItemSelected(item);
 656    }
 657
 658    @Override
 659    public boolean onKeyUp(int keyCode, KeyEvent event) {
 660        if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
 661            openSearch();
 662            return true;
 663        }
 664        int c = event.getUnicodeChar();
 665        if (c > 32) {
 666            if (mSearchEditText != null && !mSearchEditText.isFocused()) {
 667                openSearch();
 668                mSearchEditText.append(Character.toString((char) c));
 669                return true;
 670            }
 671        }
 672        return super.onKeyUp(keyCode, event);
 673    }
 674
 675    private void openSearch() {
 676        if (mMenuSearchView != null) {
 677            mMenuSearchView.expandActionView();
 678        }
 679    }
 680
 681    @Override
 682    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
 683        if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
 684            IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
 685            if (scanResult != null && scanResult.getFormatName() != null) {
 686                String data = scanResult.getContents();
 687                Invite invite = new Invite(data);
 688                if (xmppConnectionServiceBound) {
 689                    invite.invite();
 690                } else if (invite.getJid() != null) {
 691                    this.mPendingInvite = invite;
 692                } else {
 693                    this.mPendingInvite = null;
 694                }
 695            }
 696        } else if (resultCode == RESULT_OK) {
 697            if (xmppConnectionServiceBound) {
 698                this.mPostponedActivityResult = null;
 699                if (requestCode == REQUEST_CREATE_CONFERENCE) {
 700                    Account account = extractAccount(intent);
 701                    final String subject = intent.getStringExtra("subject");
 702                    List<Jid> jids = new ArrayList<>();
 703                    if (intent.getBooleanExtra("multiple", false)) {
 704                        String[] toAdd = intent.getStringArrayExtra("contacts");
 705                        for (String item : toAdd) {
 706                            try {
 707                                jids.add(Jid.fromString(item));
 708                            } catch (InvalidJidException e) {
 709                                //ignored
 710                            }
 711                        }
 712                    } else {
 713                        try {
 714                            jids.add(Jid.fromString(intent.getStringExtra("contact")));
 715                        } catch (Exception e) {
 716                            //ignored
 717                        }
 718                    }
 719                    if (account != null && jids.size() > 0) {
 720                        if (xmppConnectionService.createAdhocConference(account, subject, jids, mAdhocConferenceCallback)) {
 721                            mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 722                            mToast.show();
 723                        }
 724                    }
 725                }
 726            } else {
 727                this.mPostponedActivityResult = new Pair<>(requestCode, intent);
 728            }
 729        }
 730        super.onActivityResult(requestCode, requestCode, intent);
 731    }
 732
 733    private void askForContactsPermissions() {
 734        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 735            if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
 736                if (mRequestedContactsPermission.compareAndSet(false, true)) {
 737                    if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
 738                        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 739                        builder.setTitle(R.string.sync_with_contacts);
 740                        builder.setMessage(R.string.sync_with_contacts_long);
 741                        builder.setPositiveButton(R.string.next, new OnClickListener() {
 742                            @Override
 743                            public void onClick(DialogInterface dialog, int which) {
 744                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 745                                    requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 746                                }
 747                            }
 748                        });
 749                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
 750                            builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
 751                                @Override
 752                                public void onDismiss(DialogInterface dialog) {
 753                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 754                                        requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 755                                    }
 756                                }
 757                            });
 758                        }
 759                        builder.create().show();
 760                    } else {
 761                        requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0);
 762                    }
 763                }
 764            }
 765        }
 766    }
 767
 768    @Override
 769    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
 770        if (grantResults.length > 0)
 771            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 772                if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
 773                    xmppConnectionService.loadPhoneContacts();
 774                }
 775            }
 776    }
 777
 778    @Override
 779    protected void onBackendConnected() {
 780        if (mPostponedActivityResult != null) {
 781            onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
 782            this.mPostponedActivityResult = null;
 783        }
 784        this.mActivatedAccounts.clear();
 785        for (Account account : xmppConnectionService.getAccounts()) {
 786            if (account.getStatus() != Account.State.DISABLED) {
 787                if (Config.DOMAIN_LOCK != null) {
 788                    this.mActivatedAccounts.add(account.getJid().getLocalpart());
 789                } else {
 790                    this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
 791                }
 792            }
 793        }
 794        final Intent intent = getIntent();
 795        final ActionBar ab = getActionBar();
 796        boolean init = intent != null && intent.getBooleanExtra("init", false);
 797        boolean noConversations = xmppConnectionService.getConversations().size() == 0;
 798        if ((init || noConversations) && ab != null) {
 799            ab.setDisplayShowHomeEnabled(false);
 800            ab.setDisplayHomeAsUpEnabled(false);
 801            ab.setHomeButtonEnabled(false);
 802        }
 803        this.mKnownHosts = xmppConnectionService.getKnownHosts();
 804        this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts();
 805        if (this.mPendingInvite != null) {
 806            mPendingInvite.invite();
 807            this.mPendingInvite = null;
 808            filter(null);
 809        } else if (!handleIntent(getIntent())) {
 810            if (mSearchEditText != null) {
 811                filter(mSearchEditText.getText().toString());
 812            } else {
 813                filter(null);
 814            }
 815        } else {
 816            filter(null);
 817        }
 818        setIntent(null);
 819    }
 820
 821    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
 822    Invite getInviteJellyBean(NdefRecord record) {
 823        return new Invite(record.toUri());
 824    }
 825
 826    protected boolean handleIntent(Intent intent) {
 827        if (intent == null || intent.getAction() == null) {
 828            return false;
 829        }
 830        switch (intent.getAction()) {
 831            case Intent.ACTION_SENDTO:
 832            case Intent.ACTION_VIEW:
 833                Uri uri = intent.getData();
 834                if (uri != null) {
 835                    return new Invite(intent.getData(),false).invite();
 836                } else {
 837                    return false;
 838                }
 839            case NfcAdapter.ACTION_NDEF_DISCOVERED:
 840                for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
 841                    if (message instanceof NdefMessage) {
 842                        for (NdefRecord record : ((NdefMessage) message).getRecords()) {
 843                            switch (record.getTnf()) {
 844                                case NdefRecord.TNF_WELL_KNOWN:
 845                                    if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) {
 846                                        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
 847                                            return getInviteJellyBean(record).invite();
 848                                        } else {
 849                                            byte[] payload = record.getPayload();
 850                                            if (payload[0] == 0) {
 851                                                return new Invite(Uri.parse(new String(Arrays.copyOfRange(
 852                                                        payload, 1, payload.length)))).invite();
 853                                            }
 854                                        }
 855                                    }
 856                            }
 857                        }
 858                    }
 859                }
 860        }
 861        return false;
 862    }
 863
 864    private boolean handleJid(Invite invite) {
 865        Account account = xmppConnectionService.findAccountByJid(invite.getJid());
 866        if (account != null && !account.isOptionSet(Account.OPTION_DISABLED)) {
 867            if (invite.hasFingerprints() && xmppConnectionService.verifyFingerprints(account,invite.getFingerprints())) {
 868                Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
 869            }
 870            switchToAccount(account);
 871            finish();
 872            return true;
 873        }
 874        List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
 875        if (invite.isMuc()) {
 876            Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
 877            if (muc != null) {
 878                switchToConversation(muc,invite.getBody(),false);
 879                return true;
 880            } else {
 881                showJoinConferenceDialog(invite.getJid().toBareJid().toString());
 882                return false;
 883            }
 884        } else if (contacts.size() == 0) {
 885            showCreateContactDialog(invite.getJid().toString(), invite);
 886            return false;
 887        } else if (contacts.size() == 1) {
 888            Contact contact = contacts.get(0);
 889            if (!invite.isSafeSource() && invite.hasFingerprints()) {
 890                displayVerificationWarningDialog(contact,invite);
 891            } else {
 892                if (invite.hasFingerprints()) {
 893                    if(xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
 894                        Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
 895                    }
 896                }
 897                switchToConversation(contact, invite.getBody());
 898            }
 899            return true;
 900        } else {
 901            if (mMenuSearchView != null) {
 902                mMenuSearchView.expandActionView();
 903                mSearchEditText.setText("");
 904                mSearchEditText.append(invite.getJid().toString());
 905                filter(invite.getJid().toString());
 906            } else {
 907                mInitialJid = invite.getJid().toString();
 908            }
 909            return true;
 910        }
 911    }
 912
 913    private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
 914        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 915        builder.setTitle(R.string.verify_omemo_keys);
 916        View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
 917        final CheckBox isTrustedSource = (CheckBox) view.findViewById(R.id.trusted_source);
 918        TextView warning = (TextView) view.findViewById(R.id.warning);
 919        String jid = contact.getJid().toBareJid().toString();
 920        SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source,jid,contact.getDisplayName()));
 921        int start = spannable.toString().indexOf(jid);
 922        if (start >= 0) {
 923            spannable.setSpan(new TypefaceSpan("monospace"),start,start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 924        }
 925        warning.setText(spannable);
 926        builder.setView(view);
 927        builder.setPositiveButton(R.string.confirm, new OnClickListener() {
 928            @Override
 929            public void onClick(DialogInterface dialog, int which) {
 930                if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
 931                    xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
 932                }
 933                switchToConversation(contact, invite.getBody());
 934            }
 935        });
 936        builder.setNegativeButton(R.string.cancel, new OnClickListener() {
 937            @Override
 938            public void onClick(DialogInterface dialog, int which) {
 939                StartConversationActivity.this.finish();
 940            }
 941        });
 942        AlertDialog dialog = builder.create();
 943        dialog.setCanceledOnTouchOutside(false);
 944        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
 945            @Override
 946            public void onCancel(DialogInterface dialog) {
 947                StartConversationActivity.this.finish();
 948            }
 949        });
 950        dialog.show();
 951    }
 952
 953    protected void filter(String needle) {
 954        if (xmppConnectionServiceBound) {
 955            this.filterContacts(needle);
 956            this.filterConferences(needle);
 957        }
 958    }
 959
 960    protected void filterContacts(String needle) {
 961        this.contacts.clear();
 962        for (Account account : xmppConnectionService.getAccounts()) {
 963            if (account.getStatus() != Account.State.DISABLED) {
 964                for (Contact contact : account.getRoster().getContacts()) {
 965                    Presence.Status s = contact.getShownStatus();
 966                    if (contact.showInRoster() && contact.match(this, needle)
 967                            && (!this.mHideOfflineContacts
 968                            || (needle != null && !needle.trim().isEmpty())
 969                            || s.compareTo(Presence.Status.OFFLINE) < 0)) {
 970                        this.contacts.add(contact);
 971                    }
 972                }
 973            }
 974        }
 975        Collections.sort(this.contacts);
 976        mContactsAdapter.notifyDataSetChanged();
 977    }
 978
 979    protected void filterConferences(String needle) {
 980        this.conferences.clear();
 981        for (Account account : xmppConnectionService.getAccounts()) {
 982            if (account.getStatus() != Account.State.DISABLED) {
 983                for (Bookmark bookmark : account.getBookmarks()) {
 984                    if (bookmark.match(this, needle)) {
 985                        this.conferences.add(bookmark);
 986                    }
 987                }
 988            }
 989        }
 990        Collections.sort(this.conferences);
 991        mConferenceAdapter.notifyDataSetChanged();
 992    }
 993
 994    private void onTabChanged() {
 995        invalidateOptionsMenu();
 996    }
 997
 998    @Override
 999    public void OnUpdateBlocklist(final Status status) {
1000        refreshUi();
1001    }
1002
1003    @Override
1004    protected void refreshUiReal() {
1005        if (mSearchEditText != null) {
1006            filter(mSearchEditText.getText().toString());
1007        }
1008    }
1009
1010    public class ListPagerAdapter extends PagerAdapter {
1011        FragmentManager fragmentManager;
1012        MyListFragment[] fragments;
1013
1014        public ListPagerAdapter(FragmentManager fm) {
1015            fragmentManager = fm;
1016            fragments = new MyListFragment[2];
1017        }
1018
1019        public void requestFocus(int pos) {
1020            if (fragments.length > pos) {
1021                fragments[pos].getListView().requestFocus();
1022            }
1023        }
1024
1025        @Override
1026        public void destroyItem(ViewGroup container, int position, Object object) {
1027            assert (0 <= position && position < fragments.length);
1028            FragmentTransaction trans = fragmentManager.beginTransaction();
1029            trans.remove(fragments[position]);
1030            trans.commit();
1031            fragments[position] = null;
1032        }
1033
1034        @Override
1035        public Fragment instantiateItem(ViewGroup container, int position) {
1036            Fragment fragment = getItem(position);
1037            FragmentTransaction trans = fragmentManager.beginTransaction();
1038            trans.add(container.getId(), fragment, "fragment:" + position);
1039            trans.commit();
1040            return fragment;
1041        }
1042
1043        @Override
1044        public int getCount() {
1045            return fragments.length;
1046        }
1047
1048        @Override
1049        public boolean isViewFromObject(View view, Object fragment) {
1050            return ((Fragment) fragment).getView() == view;
1051        }
1052
1053        public Fragment getItem(int position) {
1054            assert (0 <= position && position < fragments.length);
1055            if (fragments[position] == null) {
1056                final MyListFragment listFragment = new MyListFragment();
1057                if (position == 1) {
1058                    listFragment.setListAdapter(mConferenceAdapter);
1059                    listFragment.setContextMenu(R.menu.conference_context);
1060                    listFragment.setOnListItemClickListener(new OnItemClickListener() {
1061
1062                        @Override
1063                        public void onItemClick(AdapterView<?> arg0, View arg1,
1064                                                int position, long arg3) {
1065                            openConversationForBookmark(position);
1066                        }
1067                    });
1068                } else {
1069
1070                    listFragment.setListAdapter(mContactsAdapter);
1071                    listFragment.setContextMenu(R.menu.contact_context);
1072                    listFragment.setOnListItemClickListener(new OnItemClickListener() {
1073
1074                        @Override
1075                        public void onItemClick(AdapterView<?> arg0, View arg1,
1076                                                int position, long arg3) {
1077                            openConversationForContact(position);
1078                        }
1079                    });
1080                }
1081                fragments[position] = listFragment;
1082            }
1083            return fragments[position];
1084        }
1085    }
1086
1087    public static class MyListFragment extends ListFragment {
1088        private AdapterView.OnItemClickListener mOnItemClickListener;
1089        private int mResContextMenu;
1090
1091        public void setContextMenu(final int res) {
1092            this.mResContextMenu = res;
1093        }
1094
1095        @Override
1096        public void onListItemClick(final ListView l, final View v, final int position, final long id) {
1097            if (mOnItemClickListener != null) {
1098                mOnItemClickListener.onItemClick(l, v, position, id);
1099            }
1100        }
1101
1102        public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1103            this.mOnItemClickListener = l;
1104        }
1105
1106        @Override
1107        public void onViewCreated(final View view, final Bundle savedInstanceState) {
1108            super.onViewCreated(view, savedInstanceState);
1109            registerForContextMenu(getListView());
1110            getListView().setFastScrollEnabled(true);
1111        }
1112
1113        @Override
1114        public void onCreateContextMenu(final ContextMenu menu, final View v,
1115                                        final ContextMenuInfo menuInfo) {
1116            super.onCreateContextMenu(menu, v, menuInfo);
1117            final StartConversationActivity activity = (StartConversationActivity) getActivity();
1118            activity.getMenuInflater().inflate(mResContextMenu, menu);
1119            final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1120            if (mResContextMenu == R.menu.conference_context) {
1121                activity.conference_context_id = acmi.position;
1122            } else if (mResContextMenu == R.menu.contact_context) {
1123                activity.contact_context_id = acmi.position;
1124                final Contact contact = (Contact) activity.contacts.get(acmi.position);
1125                final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1126                final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1127                if (contact.isSelf()) {
1128                    showContactDetailsItem.setVisible(false);
1129                }
1130                XmppConnection xmpp = contact.getAccount().getXmppConnection();
1131                if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1132                    if (contact.isBlocked()) {
1133                        blockUnblockItem.setTitle(R.string.unblock_contact);
1134                    } else {
1135                        blockUnblockItem.setTitle(R.string.block_contact);
1136                    }
1137                } else {
1138                    blockUnblockItem.setVisible(false);
1139                }
1140            }
1141        }
1142
1143        @Override
1144        public boolean onContextItemSelected(final MenuItem item) {
1145            StartConversationActivity activity = (StartConversationActivity) getActivity();
1146            switch (item.getItemId()) {
1147                case R.id.context_start_conversation:
1148                    activity.openConversationForContact();
1149                    break;
1150                case R.id.context_contact_details:
1151                    activity.openDetailsForContact();
1152                    break;
1153                case R.id.context_contact_block_unblock:
1154                    activity.toggleContactBlock();
1155                    break;
1156                case R.id.context_delete_contact:
1157                    activity.deleteContact();
1158                    break;
1159                case R.id.context_join_conference:
1160                    activity.openConversationForBookmark();
1161                    break;
1162                case R.id.context_share_uri:
1163                    activity.shareBookmarkUri();
1164                    break;
1165                case R.id.context_delete_conference:
1166                    activity.deleteConference();
1167            }
1168            return true;
1169        }
1170    }
1171
1172    private class Invite extends XmppUri {
1173
1174        public Invite(final Uri uri) {
1175            super(uri);
1176        }
1177
1178        public Invite(final String uri) {
1179            super(uri);
1180        }
1181
1182        public Invite(Uri uri, boolean safeSource) {
1183            super(uri,safeSource);
1184        }
1185
1186        boolean invite() {
1187            if (getJid() != null) {
1188                return handleJid(this);
1189            }
1190            return false;
1191        }
1192
1193        public boolean isMuc() {
1194            return muc;
1195        }
1196    }
1197}