DialtactsActivity.java revision 7cc6149445fbfaaef33b3a351e76829390a87226
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dialer;
18
19import android.app.ActionBar;
20import android.app.Fragment;
21import android.app.FragmentTransaction;
22import android.content.ActivityNotFoundException;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Trace;
32import android.speech.RecognizerIntent;
33import android.support.v4.view.ViewPager;
34import android.telecom.PhoneAccount;
35import android.telecom.TelecomManager;
36import android.text.Editable;
37import android.text.TextUtils;
38import android.text.TextWatcher;
39import android.util.Log;
40import android.view.DragEvent;
41import android.view.Gravity;
42import android.view.KeyEvent;
43import android.view.Menu;
44import android.view.MenuItem;
45import android.view.MotionEvent;
46import android.view.View;
47import android.view.View.OnDragListener;
48import android.view.View.OnTouchListener;
49import android.view.ViewTreeObserver;
50import android.view.animation.Animation;
51import android.view.animation.AnimationUtils;
52import android.widget.AbsListView.OnScrollListener;
53import android.widget.EditText;
54import android.widget.FrameLayout;
55import android.widget.ImageButton;
56import android.widget.PopupMenu;
57import android.widget.Toast;
58
59import com.android.contacts.common.activity.TransactionSafeActivity;
60import com.android.contacts.common.dialog.ClearFrequentsDialog;
61import com.android.contacts.common.interactions.ImportExportDialogFragment;
62import com.android.contacts.common.interactions.TouchPointManager;
63import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
64import com.android.contacts.common.util.PermissionsUtil;
65import com.android.contacts.common.widget.FloatingActionButtonController;
66import com.android.contacts.commonbind.analytics.AnalyticsUtil;
67import com.android.dialer.calllog.CallLogActivity;
68import com.android.dialer.database.DialerDatabaseHelper;
69import com.android.dialer.dialpad.DialpadFragment;
70import com.android.dialer.dialpad.SmartDialNameMatcher;
71import com.android.dialer.dialpad.SmartDialPrefix;
72import com.android.dialer.interactions.PhoneNumberInteraction;
73import com.android.dialer.list.DragDropController;
74import com.android.dialer.list.ListsFragment;
75import com.android.dialer.list.OnDragDropListener;
76import com.android.dialer.list.OnListFragmentScrolledListener;
77import com.android.dialer.list.PhoneFavoriteSquareTileView;
78import com.android.dialer.list.RegularSearchFragment;
79import com.android.dialer.list.SearchFragment;
80import com.android.dialer.list.SmartDialSearchFragment;
81import com.android.dialer.list.SpeedDialFragment;
82import com.android.dialer.settings.DialerSettingsActivity;
83import com.android.dialer.util.IntentUtil;
84import com.android.dialer.util.DialerUtils;
85import com.android.dialer.widget.ActionBarController;
86import com.android.dialer.widget.SearchEditTextLayout;
87import com.android.dialer.widget.SearchEditTextLayout.OnBackButtonClickedListener;
88import com.android.dialerbind.DatabaseHelperManager;
89import com.android.phone.common.animation.AnimUtils;
90import com.android.phone.common.animation.AnimationListenerAdapter;
91
92import junit.framework.Assert;
93
94import java.util.ArrayList;
95import java.util.List;
96
97/**
98 * The dialer tab's title is 'phone', a more common name (see strings.xml).
99 */
100public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
101        DialpadFragment.OnDialpadQueryChangedListener,
102        OnListFragmentScrolledListener,
103        ListsFragment.HostInterface,
104        SpeedDialFragment.HostInterface,
105        SearchFragment.HostInterface,
106        OnDragDropListener,
107        OnPhoneNumberPickerActionListener,
108        PopupMenu.OnMenuItemClickListener,
109        ViewPager.OnPageChangeListener,
110        ActionBarController.ActivityUi {
111    private static final String TAG = "DialtactsActivity";
112
113    public static final boolean DEBUG = false;
114
115    public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
116
117    /** @see #getCallOrigin() */
118    private static final String CALL_ORIGIN_DIALTACTS =
119            "com.android.dialer.DialtactsActivity";
120
121    private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
122    private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
123    private static final String KEY_SEARCH_QUERY = "search_query";
124    private static final String KEY_FIRST_LAUNCH = "first_launch";
125    private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown";
126
127    private static final String TAG_DIALPAD_FRAGMENT = "dialpad";
128    private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
129    private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
130    private static final String TAG_FAVORITES_FRAGMENT = "favorites";
131
132    /**
133     * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
134     */
135    private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
136
137    private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
138
139    private FrameLayout mParentLayout;
140
141    /**
142     * Fragment containing the dialpad that slides into view
143     */
144    protected DialpadFragment mDialpadFragment;
145
146    /**
147     * Fragment for searching phone numbers using the alphanumeric keyboard.
148     */
149    private RegularSearchFragment mRegularSearchFragment;
150
151    /**
152     * Fragment for searching phone numbers using the dialpad.
153     */
154    private SmartDialSearchFragment mSmartDialSearchFragment;
155
156    /**
157     * Animation that slides in.
158     */
159    private Animation mSlideIn;
160
161    /**
162     * Animation that slides out.
163     */
164    private Animation mSlideOut;
165
166    AnimationListenerAdapter mSlideInListener = new AnimationListenerAdapter() {
167        @Override
168        public void onAnimationEnd(Animation animation) {
169            if (!isInSearchUi()) {
170                enterSearchUi(true /* isSmartDial */, mSearchQuery, false);
171            }
172        }
173    };
174
175    /**
176     * Listener for after slide out animation completes on dialer fragment.
177     */
178    AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() {
179        @Override
180        public void onAnimationEnd(Animation animation) {
181            commitDialpadFragmentHide();
182        }
183    };
184
185    /**
186     * Fragment containing the speed dial list, recents list, and all contacts list.
187     */
188    private ListsFragment mListsFragment;
189
190    /**
191     * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can
192     * be commited.
193     */
194    private boolean mStateSaved;
195    private boolean mIsRestarting;
196    private boolean mInDialpadSearch;
197    private boolean mInRegularSearch;
198    private boolean mClearSearchOnPause;
199    private boolean mIsDialpadShown;
200    private boolean mShowDialpadOnResume;
201
202    /**
203     * Whether or not the device is in landscape orientation.
204     */
205    private boolean mIsLandscape;
206
207    /**
208     * True if the dialpad is only temporarily showing due to being in call
209     */
210    private boolean mInCallDialpadUp;
211
212    /**
213     * True when this activity has been launched for the first time.
214     */
215    private boolean mFirstLaunch;
216
217    /**
218     * Search query to be applied to the SearchView in the ActionBar once
219     * onCreateOptionsMenu has been called.
220     */
221    private String mPendingSearchViewQuery;
222
223    private PopupMenu mOverflowMenu;
224    private EditText mSearchView;
225    private View mVoiceSearchButton;
226
227    private String mSearchQuery;
228
229    private DialerDatabaseHelper mDialerDatabaseHelper;
230    private DragDropController mDragDropController;
231    private ActionBarController mActionBarController;
232
233    private FloatingActionButtonController mFloatingActionButtonController;
234
235    private int mActionBarHeight;
236
237    /**
238     * The text returned from a voice search query.  Set in {@link #onActivityResult} and used in
239     * {@link #onResume()} to populate the search box.
240     */
241    private String mVoiceSearchQuery;
242
243    protected class OptionsPopupMenu extends PopupMenu {
244        public OptionsPopupMenu(Context context, View anchor) {
245            super(context, anchor, Gravity.END);
246        }
247
248        @Override
249        public void show() {
250            final boolean hasContactsPermission =
251                    PermissionsUtil.hasContactsPermissions(DialtactsActivity.this);
252            final Menu menu = getMenu();
253            final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
254            clearFrequents.setVisible(mListsFragment != null &&
255                    mListsFragment.getSpeedDialFragment() != null &&
256                    mListsFragment.getSpeedDialFragment().hasFrequents() && hasContactsPermission);
257
258            menu.findItem(R.id.menu_import_export).setVisible(hasContactsPermission);
259            menu.findItem(R.id.menu_add_contact).setVisible(hasContactsPermission);
260
261            menu.findItem(R.id.menu_history).setVisible(
262                    PermissionsUtil.hasPhonePermissions(DialtactsActivity.this));
263            super.show();
264        }
265    }
266
267    /**
268     * Listener that listens to drag events and sends their x and y coordinates to a
269     * {@link DragDropController}.
270     */
271    private class LayoutOnDragListener implements OnDragListener {
272        @Override
273        public boolean onDrag(View v, DragEvent event) {
274            if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
275                mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY());
276            }
277            return true;
278        }
279    }
280
281    /**
282     * Listener used to send search queries to the phone search fragment.
283     */
284    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
285        @Override
286        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
287        }
288
289        @Override
290        public void onTextChanged(CharSequence s, int start, int before, int count) {
291            final String newText = s.toString();
292            if (newText.equals(mSearchQuery)) {
293                // If the query hasn't changed (perhaps due to activity being destroyed
294                // and restored, or user launching the same DIAL intent twice), then there is
295                // no need to do anything here.
296                return;
297            }
298            if (DEBUG) {
299                Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText);
300                Log.d(TAG, "Previous Query: " + mSearchQuery);
301            }
302            mSearchQuery = newText;
303
304            // Show search fragment only when the query string is changed to non-empty text.
305            if (!TextUtils.isEmpty(newText)) {
306                // Call enterSearchUi only if we are switching search modes, or showing a search
307                // fragment for the first time.
308                final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) ||
309                        (!mIsDialpadShown && mInRegularSearch);
310                if (!sameSearchMode) {
311                    enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */);
312                }
313            }
314
315            if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
316                mSmartDialSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
317            } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
318                mRegularSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
319            }
320        }
321
322        @Override
323        public void afterTextChanged(Editable s) {
324        }
325    };
326
327
328    /**
329     * Open the search UI when the user clicks on the search box.
330     */
331    private final View.OnClickListener mSearchViewOnClickListener = new View.OnClickListener() {
332        @Override
333        public void onClick(View v) {
334            if (!isInSearchUi()) {
335                mActionBarController.onSearchBoxTapped();
336                enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString(),
337                        true /* animate */);
338            }
339        }
340    };
341
342    /**
343     * If the search term is empty and the user closes the soft keyboard, close the search UI.
344     */
345    private final View.OnKeyListener mSearchEditTextLayoutListener = new View.OnKeyListener() {
346        @Override
347        public boolean onKey(View v, int keyCode, KeyEvent event) {
348            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN &&
349                    TextUtils.isEmpty(mSearchView.getText().toString())) {
350                maybeExitSearchUi();
351            }
352            return false;
353        }
354    };
355
356    @Override
357    public boolean dispatchTouchEvent(MotionEvent ev) {
358        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
359            TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
360        }
361        return super.dispatchTouchEvent(ev);
362
363    }
364
365    @Override
366    protected void onCreate(Bundle savedInstanceState) {
367        Trace.beginSection(TAG + " onCreate");
368        super.onCreate(savedInstanceState);
369
370        mFirstLaunch = true;
371
372        final Resources resources = getResources();
373        mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large);
374
375        Trace.beginSection(TAG + " setContentView");
376        setContentView(R.layout.dialtacts_activity);
377        Trace.endSection();
378        getWindow().setBackgroundDrawable(null);
379
380        Trace.beginSection(TAG + " setup Views");
381        final ActionBar actionBar = getActionBar();
382        actionBar.setCustomView(R.layout.search_edittext);
383        actionBar.setDisplayShowCustomEnabled(true);
384        actionBar.setBackgroundDrawable(null);
385
386        SearchEditTextLayout searchEditTextLayout =
387                (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container);
388        searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener);
389
390        mActionBarController = new ActionBarController(this, searchEditTextLayout);
391
392        mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
393        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
394        mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button);
395        searchEditTextLayout.findViewById(R.id.search_magnifying_glass)
396                .setOnClickListener(mSearchViewOnClickListener);
397        searchEditTextLayout.findViewById(R.id.search_box_start_search)
398                .setOnClickListener(mSearchViewOnClickListener);
399        searchEditTextLayout.setOnBackButtonClickedListener(new OnBackButtonClickedListener() {
400            @Override
401            public void onBackButtonClicked() {
402                onBackPressed();
403            }
404        });
405
406        mIsLandscape = getResources().getConfiguration().orientation
407                == Configuration.ORIENTATION_LANDSCAPE;
408
409        final View floatingActionButtonContainer = findViewById(
410                R.id.floating_action_button_container);
411        ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
412        floatingActionButton.setOnClickListener(this);
413        mFloatingActionButtonController = new FloatingActionButtonController(this,
414                floatingActionButtonContainer, floatingActionButton);
415
416        ImageButton optionsMenuButton =
417                (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button);
418        optionsMenuButton.setOnClickListener(this);
419        mOverflowMenu = buildOptionsMenu(searchEditTextLayout);
420        optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener());
421
422        // Add the favorites fragment but only if savedInstanceState is null. Otherwise the
423        // fragment manager is responsible for recreating it.
424        if (savedInstanceState == null) {
425            getFragmentManager().beginTransaction()
426                    .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
427                    .commit();
428        } else {
429            mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
430            mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
431            mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
432            mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
433            mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN);
434            mActionBarController.restoreInstanceState(savedInstanceState);
435        }
436
437        final boolean isLayoutRtl = DialerUtils.isRtl();
438        if (mIsLandscape) {
439            mSlideIn = AnimationUtils.loadAnimation(this,
440                    isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
441            mSlideOut = AnimationUtils.loadAnimation(this,
442                    isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
443        } else {
444            mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom);
445            mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom);
446        }
447
448        mSlideIn.setInterpolator(AnimUtils.EASE_IN);
449        mSlideOut.setInterpolator(AnimUtils.EASE_OUT);
450
451        mSlideIn.setAnimationListener(mSlideInListener);
452        mSlideOut.setAnimationListener(mSlideOutListener);
453
454        mParentLayout = (FrameLayout) findViewById(R.id.dialtacts_mainlayout);
455        mParentLayout.setOnDragListener(new LayoutOnDragListener());
456        floatingActionButtonContainer.getViewTreeObserver().addOnGlobalLayoutListener(
457                new ViewTreeObserver.OnGlobalLayoutListener() {
458                    @Override
459                    public void onGlobalLayout() {
460                        final ViewTreeObserver observer =
461                                floatingActionButtonContainer.getViewTreeObserver();
462                        if (!observer.isAlive()) {
463                            return;
464                        }
465                        observer.removeOnGlobalLayoutListener(this);
466                        int screenWidth = mParentLayout.getWidth();
467                        mFloatingActionButtonController.setScreenWidth(screenWidth);
468                        updateFloatingActionButtonControllerAlignment(false /* animate */);
469                    }
470                });
471
472        setupActivityOverlay();
473
474        Trace.endSection();
475
476        Trace.beginSection(TAG + " initialize smart dialing");
477        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
478        SmartDialPrefix.initializeNanpSettings(this);
479        Trace.endSection();
480        Trace.endSection();
481    }
482
483    private void setupActivityOverlay() {
484        final View activityOverlay = findViewById(R.id.activity_overlay);
485        activityOverlay.setOnTouchListener(new OnTouchListener() {
486            @Override
487            public boolean onTouch(View v, MotionEvent event) {
488                if (!mIsDialpadShown) {
489                    maybeExitSearchUi();
490                }
491                return false;
492            }
493        });
494    }
495
496    @Override
497    protected void onResume() {
498        Trace.beginSection(TAG + " onResume");
499        super.onResume();
500        mStateSaved = false;
501        if (mFirstLaunch) {
502            displayFragment(getIntent());
503        } else if (!phoneIsInUse() && mInCallDialpadUp) {
504            hideDialpadFragment(false, true);
505            mInCallDialpadUp = false;
506        } else if (mShowDialpadOnResume) {
507            showDialpadFragment(false);
508            mShowDialpadOnResume = false;
509        }
510
511        // If there was a voice query result returned in the {@link #onActivityResult} callback, it
512        // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be
513        // shown until onResume has completed.  Active the search UI and set the search term now.
514        if (!TextUtils.isEmpty(mVoiceSearchQuery)) {
515            mActionBarController.onSearchBoxTapped();
516            mSearchView.setText(mVoiceSearchQuery);
517            mVoiceSearchQuery = null;
518        }
519
520        mFirstLaunch = false;
521
522        if (mIsRestarting) {
523            // This is only called when the activity goes from resumed -> paused -> resumed, so it
524            // will not cause an extra view to be sent out on rotation
525            if (mIsDialpadShown) {
526                AnalyticsUtil.sendScreenView(mDialpadFragment, this);
527            }
528            mIsRestarting = false;
529        }
530        prepareVoiceSearchButton();
531        mDialerDatabaseHelper.startSmartDialUpdateThread();
532        updateFloatingActionButtonControllerAlignment(false /* animate */);
533        Trace.endSection();
534    }
535
536    @Override
537    protected void onRestart() {
538        super.onRestart();
539        mIsRestarting = true;
540    }
541
542    @Override
543    protected void onPause() {
544        if (mClearSearchOnPause) {
545            hideDialpadAndSearchUi();
546            mClearSearchOnPause = false;
547        }
548        if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) {
549            commitDialpadFragmentHide();
550        }
551        super.onPause();
552    }
553
554    @Override
555    protected void onSaveInstanceState(Bundle outState) {
556        super.onSaveInstanceState(outState);
557        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
558        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
559        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
560        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
561        outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
562        mActionBarController.saveInstanceState(outState);
563        mStateSaved = true;
564    }
565
566    @Override
567    public void onAttachFragment(Fragment fragment) {
568        if (fragment instanceof DialpadFragment) {
569            mDialpadFragment = (DialpadFragment) fragment;
570            if (!mIsDialpadShown && !mShowDialpadOnResume) {
571                final FragmentTransaction transaction = getFragmentManager().beginTransaction();
572                transaction.hide(mDialpadFragment);
573                transaction.commit();
574            }
575        } else if (fragment instanceof SmartDialSearchFragment) {
576            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
577            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
578        } else if (fragment instanceof SearchFragment) {
579            mRegularSearchFragment = (RegularSearchFragment) fragment;
580            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
581        } else if (fragment instanceof ListsFragment) {
582            mListsFragment = (ListsFragment) fragment;
583            mListsFragment.addOnPageChangeListener(this);
584        }
585    }
586
587    protected void handleMenuSettings() {
588        final Intent intent = new Intent(this, DialerSettingsActivity.class);
589        startActivity(intent);
590    }
591
592    @Override
593    public void onClick(View view) {
594        switch (view.getId()) {
595            case R.id.floating_action_button:
596                if (mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_ALL_CONTACTS) {
597                    DialerUtils.startActivityWithErrorToast(
598                            this,
599                            IntentUtil.getNewContactIntent(),
600                            R.string.add_contact_not_available);
601                } else if (!mIsDialpadShown) {
602                    mInCallDialpadUp = false;
603                    showDialpadFragment(true);
604                }
605                break;
606            case R.id.voice_search_button:
607                try {
608                    startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
609                            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
610                } catch (ActivityNotFoundException e) {
611                    Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
612                            Toast.LENGTH_SHORT).show();
613                }
614                break;
615            case R.id.dialtacts_options_menu_button:
616                mOverflowMenu.show();
617                break;
618            default: {
619                Log.wtf(TAG, "Unexpected onClick event from " + view);
620                break;
621            }
622        }
623    }
624
625    @Override
626    public boolean onMenuItemClick(MenuItem item) {
627        switch (item.getItemId()) {
628            case R.id.menu_history:
629                // Use explicit CallLogActivity intent instead of ACTION_VIEW +
630                // CONTENT_TYPE, so that we always open our call log from our dialer
631                final Intent intent = new Intent(this, CallLogActivity.class);
632                startActivity(intent);
633                break;
634            case R.id.menu_add_contact:
635                DialerUtils.startActivityWithErrorToast(
636                        this,
637                        IntentUtil.getNewContactIntent(),
638                        R.string.add_contact_not_available);
639                break;
640            case R.id.menu_import_export:
641                // We hard-code the "contactsAreAvailable" argument because doing it properly would
642                // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
643                // now in Dialtacts for (potential) performance reasons. Compare with how it is
644                // done in {@link PeopleActivity}.
645                ImportExportDialogFragment.show(getFragmentManager(), true,
646                        DialtactsActivity.class);
647                return true;
648            case R.id.menu_clear_frequents:
649                ClearFrequentsDialog.show(getFragmentManager());
650                return true;
651            case R.id.menu_call_settings:
652                handleMenuSettings();
653                return true;
654        }
655        return false;
656    }
657
658    @Override
659    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
660        if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
661            if (resultCode == RESULT_OK) {
662                final ArrayList<String> matches = data.getStringArrayListExtra(
663                        RecognizerIntent.EXTRA_RESULTS);
664                if (matches.size() > 0) {
665                    final String match = matches.get(0);
666                    mVoiceSearchQuery = match;
667                } else {
668                    Log.e(TAG, "Voice search - nothing heard");
669                }
670            } else {
671                Log.e(TAG, "Voice search failed");
672            }
673        }
674        super.onActivityResult(requestCode, resultCode, data);
675    }
676
677    /**
678     * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
679     * updates are handled by a callback which is invoked after the dialpad fragment is shown.
680     * @see #onDialpadShown
681     */
682    private void showDialpadFragment(boolean animate) {
683        if (mIsDialpadShown || mStateSaved) {
684            return;
685        }
686        mIsDialpadShown = true;
687
688        mListsFragment.setUserVisibleHint(false);
689
690        final FragmentTransaction ft = getFragmentManager().beginTransaction();
691        if (mDialpadFragment == null) {
692            mDialpadFragment = new DialpadFragment();
693            ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT);
694        } else {
695            ft.show(mDialpadFragment);
696        }
697
698        mDialpadFragment.setAnimate(animate);
699        AnalyticsUtil.sendScreenView(mDialpadFragment);
700        ft.commit();
701
702        if (animate) {
703            mFloatingActionButtonController.scaleOut();
704        } else {
705            mFloatingActionButtonController.setVisible(false);
706        }
707        mActionBarController.onDialpadUp();
708
709        mListsFragment.getView().animate().alpha(0).withLayer();
710    }
711
712    /**
713     * Callback from child DialpadFragment when the dialpad is shown.
714     */
715    public void onDialpadShown() {
716        Assert.assertNotNull(mDialpadFragment);
717        if (mDialpadFragment.getAnimate()) {
718            mDialpadFragment.getView().startAnimation(mSlideIn);
719        } else {
720            mDialpadFragment.setYFraction(0);
721        }
722
723        updateSearchFragmentPosition();
724    }
725
726    /**
727     * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in
728     * a callback after the hide animation ends.
729     * @see #commitDialpadFragmentHide
730     */
731    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
732        if (mDialpadFragment == null || mDialpadFragment.getView() == null) {
733            return;
734        }
735        if (clearDialpad) {
736            mDialpadFragment.clearDialpad();
737        }
738        if (!mIsDialpadShown) {
739            return;
740        }
741        mIsDialpadShown = false;
742        mDialpadFragment.setAnimate(animate);
743        mListsFragment.setUserVisibleHint(true);
744        mListsFragment.sendScreenViewForCurrentPosition();
745
746        updateSearchFragmentPosition();
747
748        updateFloatingActionButtonControllerAlignment(animate);
749        if (animate) {
750            mDialpadFragment.getView().startAnimation(mSlideOut);
751        } else {
752            commitDialpadFragmentHide();
753        }
754
755        mActionBarController.onDialpadDown();
756
757        if (isInSearchUi()) {
758            if (TextUtils.isEmpty(mSearchQuery)) {
759                exitSearchUi();
760            }
761        }
762    }
763
764    /**
765     * Finishes hiding the dialpad fragment after any animations are completed.
766     */
767    private void commitDialpadFragmentHide() {
768        if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) {
769            final FragmentTransaction ft = getFragmentManager().beginTransaction();
770            ft.hide(mDialpadFragment);
771            ft.commit();
772        }
773        mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);
774    }
775
776    private void updateSearchFragmentPosition() {
777        SearchFragment fragment = null;
778        if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
779            fragment = mSmartDialSearchFragment;
780        } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
781            fragment = mRegularSearchFragment;
782        }
783        if (fragment != null && fragment.isVisible()) {
784            fragment.updatePosition(true /* animate */);
785        }
786    }
787
788    @Override
789    public boolean isInSearchUi() {
790        return mInDialpadSearch || mInRegularSearch;
791    }
792
793    @Override
794    public boolean hasSearchQuery() {
795        return !TextUtils.isEmpty(mSearchQuery);
796    }
797
798    @Override
799    public boolean shouldShowActionBar() {
800        return mListsFragment.shouldShowActionBar();
801    }
802
803    private void setNotInSearchUi() {
804        mInDialpadSearch = false;
805        mInRegularSearch = false;
806    }
807
808    private void hideDialpadAndSearchUi() {
809        if (mIsDialpadShown) {
810            hideDialpadFragment(false, true);
811        } else {
812            exitSearchUi();
813        }
814    }
815
816    private void prepareVoiceSearchButton() {
817        final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
818        if (canIntentBeHandled(voiceIntent)) {
819            mVoiceSearchButton.setVisibility(View.VISIBLE);
820            mVoiceSearchButton.setOnClickListener(this);
821        } else {
822            mVoiceSearchButton.setVisibility(View.GONE);
823        }
824    }
825
826    protected OptionsPopupMenu buildOptionsMenu(View invoker) {
827        final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker);
828        popupMenu.inflate(R.menu.dialtacts_options);
829        popupMenu.setOnMenuItemClickListener(this);
830        return popupMenu;
831    }
832
833    @Override
834    public boolean onCreateOptionsMenu(Menu menu) {
835        if (mPendingSearchViewQuery != null) {
836            mSearchView.setText(mPendingSearchViewQuery);
837            mPendingSearchViewQuery = null;
838        }
839        if (mActionBarController != null) {
840            mActionBarController.restoreActionBarOffset();
841        }
842        return false;
843    }
844
845    /**
846     * Returns true if the intent is due to hitting the green send key (hardware call button:
847     * KEYCODE_CALL) while in a call.
848     *
849     * @param intent the intent that launched this activity
850     * @return true if the intent is due to hitting the green send key while in a call
851     */
852    private boolean isSendKeyWhileInCall(Intent intent) {
853        // If there is a call in progress and the user launched the dialer by hitting the call
854        // button, go straight to the in-call screen.
855        final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
856
857        if (callKey) {
858            getTelecomManager().showInCallScreen(false);
859            return true;
860        }
861
862        return false;
863    }
864
865    /**
866     * Sets the current tab based on the intent's request type
867     *
868     * @param intent Intent that contains information about which tab should be selected
869     */
870    private void displayFragment(Intent intent) {
871        // If we got here by hitting send and we're in call forward along to the in-call activity
872        if (isSendKeyWhileInCall(intent)) {
873            finish();
874            return;
875        }
876
877        final boolean phoneIsInUse = phoneIsInUse();
878        if (phoneIsInUse || (intent.getData() !=  null && isDialIntent(intent))) {
879            showDialpadFragment(false);
880            mDialpadFragment.setStartedFromNewIntent(true);
881            if (phoneIsInUse && !mDialpadFragment.isVisible()) {
882                mInCallDialpadUp = true;
883            }
884        }
885    }
886
887    @Override
888    public void onNewIntent(Intent newIntent) {
889        setIntent(newIntent);
890        mStateSaved = false;
891        displayFragment(newIntent);
892
893        invalidateOptionsMenu();
894    }
895
896    /** Returns true if the given intent contains a phone number to populate the dialer with */
897    private boolean isDialIntent(Intent intent) {
898        final String action = intent.getAction();
899        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
900            return true;
901        }
902        if (Intent.ACTION_VIEW.equals(action)) {
903            final Uri data = intent.getData();
904            if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) {
905                return true;
906            }
907        }
908        return false;
909    }
910
911    /**
912     * Returns an appropriate call origin for this Activity. May return null when no call origin
913     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
914     * for remembering the tab in which the user made a phone call, so the external app's DIAL
915     * request should not be counted.)
916     */
917    public String getCallOrigin() {
918        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
919    }
920
921    /**
922     * Shows the search fragment
923     */
924    private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) {
925        if (mStateSaved || getFragmentManager().isDestroyed()) {
926            // Weird race condition where fragment is doing work after the activity is destroyed
927            // due to talkback being on (b/10209937). Just return since we can't do any
928            // constructive here.
929            return;
930        }
931
932        if (DEBUG) {
933            Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
934        }
935
936        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
937        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
938            transaction.remove(mSmartDialSearchFragment);
939        } else if (mInRegularSearch && mRegularSearchFragment != null) {
940            transaction.remove(mRegularSearchFragment);
941        }
942
943        final String tag;
944        if (smartDialSearch) {
945            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
946        } else {
947            tag = TAG_REGULAR_SEARCH_FRAGMENT;
948        }
949        mInDialpadSearch = smartDialSearch;
950        mInRegularSearch = !smartDialSearch;
951
952        SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
953        if (animate) {
954            transaction.setCustomAnimations(android.R.animator.fade_in, 0);
955        } else {
956            transaction.setTransition(FragmentTransaction.TRANSIT_NONE);
957        }
958        if (fragment == null) {
959            if (smartDialSearch) {
960                fragment = new SmartDialSearchFragment();
961            } else {
962                fragment = new RegularSearchFragment();
963            }
964            transaction.add(R.id.dialtacts_frame, fragment, tag);
965        } else {
966            transaction.show(fragment);
967        }
968        // DialtactsActivity will provide the options menu
969        fragment.setHasOptionsMenu(false);
970        fragment.setShowEmptyListForNullQuery(true);
971        if (!smartDialSearch) {
972            fragment.setQueryString(query, false /* delaySelection */);
973        }
974        transaction.commit();
975
976        if (animate) {
977            mListsFragment.getView().animate().alpha(0).withLayer();
978        }
979        mListsFragment.setUserVisibleHint(false);
980    }
981
982    /**
983     * Hides the search fragment
984     */
985    private void exitSearchUi() {
986        // See related bug in enterSearchUI();
987        if (getFragmentManager().isDestroyed() || mStateSaved) {
988            return;
989        }
990
991        mSearchView.setText(null);
992
993        if (mDialpadFragment != null) {
994            mDialpadFragment.clearDialpad();
995        }
996
997        setNotInSearchUi();
998
999        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
1000        if (mSmartDialSearchFragment != null) {
1001            transaction.remove(mSmartDialSearchFragment);
1002        }
1003        if (mRegularSearchFragment != null) {
1004            transaction.remove(mRegularSearchFragment);
1005        }
1006        transaction.commit();
1007
1008        mListsFragment.getView().animate().alpha(1).withLayer();
1009
1010        if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
1011            // If the dialpad fragment wasn't previously visible, then send a screen view because
1012            // we are exiting regular search. Otherwise, the screen view will be sent by
1013            // {@link #hideDialpadFragment}.
1014            mListsFragment.sendScreenViewForCurrentPosition();
1015            mListsFragment.setUserVisibleHint(true);
1016        }
1017
1018        mActionBarController.onSearchUiExited();
1019    }
1020
1021    @Override
1022    public void onBackPressed() {
1023        if (mStateSaved) {
1024            return;
1025        }
1026        if (mIsDialpadShown) {
1027            if (TextUtils.isEmpty(mSearchQuery) ||
1028                    (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()
1029                            && mSmartDialSearchFragment.getAdapter().getCount() == 0)) {
1030                exitSearchUi();
1031            }
1032            hideDialpadFragment(true, false);
1033        } else if (isInSearchUi()) {
1034            exitSearchUi();
1035            DialerUtils.hideInputMethod(mParentLayout);
1036        } else {
1037            super.onBackPressed();
1038        }
1039    }
1040
1041    /**
1042     * @return True if the search UI was exited, false otherwise
1043     */
1044    private boolean maybeExitSearchUi() {
1045        if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) {
1046            exitSearchUi();
1047            DialerUtils.hideInputMethod(mParentLayout);
1048            return true;
1049        }
1050        return false;
1051    }
1052
1053    @Override
1054    public void onDialpadQueryChanged(String query) {
1055        if (mSmartDialSearchFragment != null) {
1056            mSmartDialSearchFragment.setAddToContactNumber(query);
1057        }
1058        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
1059                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
1060
1061        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
1062            if (DEBUG) {
1063                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
1064            }
1065            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
1066                // This callback can happen if the dialpad fragment is recreated because of
1067                // activity destruction. In that case, don't update the search view because
1068                // that would bring the user back to the search fragment regardless of the
1069                // previous state of the application. Instead, just return here and let the
1070                // fragment manager correctly figure out whatever fragment was last displayed.
1071                if (!TextUtils.isEmpty(normalizedQuery)) {
1072                    mPendingSearchViewQuery = normalizedQuery;
1073                }
1074                return;
1075            }
1076            mSearchView.setText(normalizedQuery);
1077        }
1078    }
1079
1080    @Override
1081    public void onListFragmentScrollStateChange(int scrollState) {
1082        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
1083            hideDialpadFragment(true, false);
1084            DialerUtils.hideInputMethod(mParentLayout);
1085        }
1086    }
1087
1088    @Override
1089    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
1090                                     int totalItemCount) {
1091        // TODO: No-op for now. This should eventually show/hide the actionBar based on
1092        // interactions with the ListsFragments.
1093    }
1094
1095    private boolean phoneIsInUse() {
1096        return getTelecomManager().isInCall();
1097    }
1098
1099    private boolean canIntentBeHandled(Intent intent) {
1100        final PackageManager packageManager = getPackageManager();
1101        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
1102                PackageManager.MATCH_DEFAULT_ONLY);
1103        return resolveInfo != null && resolveInfo.size() > 0;
1104    }
1105
1106    /**
1107     * Called when the user has long-pressed a contact tile to start a drag operation.
1108     */
1109    @Override
1110    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
1111        mListsFragment.showRemoveView(true);
1112    }
1113
1114    @Override
1115    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
1116    }
1117
1118    /**
1119     * Called when the user has released a contact tile after long-pressing it.
1120     */
1121    @Override
1122    public void onDragFinished(int x, int y) {
1123        mListsFragment.showRemoveView(false);
1124    }
1125
1126    @Override
1127    public void onDroppedOnRemove() {}
1128
1129    /**
1130     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
1131     * once it has been attached to the activity.
1132     */
1133    @Override
1134    public void setDragDropController(DragDropController dragController) {
1135        mDragDropController = dragController;
1136        mListsFragment.getRemoveView().setDragDropController(dragController);
1137    }
1138
1139    @Override
1140    public void onPickPhoneNumberAction(Uri dataUri) {
1141        // Specify call-origin so that users will see the previous tab instead of
1142        // CallLog screen (search UI will be automatically exited).
1143        PhoneNumberInteraction.startInteractionForPhoneCall(
1144                DialtactsActivity.this, dataUri, getCallOrigin());
1145        mClearSearchOnPause = true;
1146    }
1147
1148    @Override
1149    public void onCallNumberDirectly(String phoneNumber) {
1150        onCallNumberDirectly(phoneNumber, false /* isVideoCall */);
1151    }
1152
1153    @Override
1154    public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) {
1155        Intent intent = isVideoCall ?
1156                IntentUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) :
1157                IntentUtil.getCallIntent(phoneNumber, getCallOrigin());
1158        DialerUtils.startActivityWithErrorToast(this, intent);
1159        mClearSearchOnPause = true;
1160    }
1161
1162    @Override
1163    public void onShortcutIntentCreated(Intent intent) {
1164        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
1165    }
1166
1167    @Override
1168    public void onHomeInActionBarSelected() {
1169        exitSearchUi();
1170    }
1171
1172    @Override
1173    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
1174        int tabIndex = mListsFragment.getCurrentTabIndex();
1175
1176        // Scroll the button from center to end when moving from the Speed Dial to Recents tab.
1177        // In RTL, scroll when the current tab is Recents instead of Speed Dial, because the order
1178        // of the tabs is reversed and the ViewPager returns the left tab position during scroll.
1179        boolean isRtl = DialerUtils.isRtl();
1180        if (!isRtl && tabIndex == ListsFragment.TAB_INDEX_SPEED_DIAL && !mIsLandscape) {
1181            mFloatingActionButtonController.onPageScrolled(positionOffset);
1182        } else if (isRtl && tabIndex == ListsFragment.TAB_INDEX_RECENTS && !mIsLandscape) {
1183            mFloatingActionButtonController.onPageScrolled(1 - positionOffset);
1184        } else if (tabIndex != ListsFragment.TAB_INDEX_SPEED_DIAL) {
1185            mFloatingActionButtonController.onPageScrolled(1);
1186        }
1187    }
1188
1189    @Override
1190    public void onPageSelected(int position) {
1191        int tabIndex = mListsFragment.getCurrentTabIndex();
1192        if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS) {
1193            mFloatingActionButtonController.changeIcon(
1194                    getResources().getDrawable(R.drawable.ic_person_add_24dp),
1195                    getResources().getString(R.string.search_shortcut_create_new_contact));
1196        } else {
1197            mFloatingActionButtonController.changeIcon(
1198                    getResources().getDrawable(R.drawable.fab_ic_dial),
1199                    getResources().getString(R.string.action_menu_dialpad_button));
1200        }
1201    }
1202
1203    @Override
1204    public void onPageScrollStateChanged(int state) {
1205    }
1206
1207    private TelecomManager getTelecomManager() {
1208        return (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
1209    }
1210
1211    @Override
1212    public boolean isActionBarShowing() {
1213        return mActionBarController.isActionBarShowing();
1214    }
1215
1216    @Override
1217    public ActionBarController getActionBarController() {
1218        return mActionBarController;
1219    }
1220
1221    public boolean isDialpadShown() {
1222        return mIsDialpadShown;
1223    }
1224
1225    @Override
1226    public int getActionBarHideOffset() {
1227        return getActionBar().getHideOffset();
1228    }
1229
1230    @Override
1231    public void setActionBarHideOffset(int offset) {
1232        getActionBar().setHideOffset(offset);
1233    }
1234
1235    @Override
1236    public int getActionBarHeight() {
1237        return mActionBarHeight;
1238    }
1239
1240    /**
1241     * Updates controller based on currently known information.
1242     *
1243     * @param animate Whether or not to animate the transition.
1244     */
1245    private void updateFloatingActionButtonControllerAlignment(boolean animate) {
1246        int align = (!mIsLandscape &&
1247                mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) ?
1248                FloatingActionButtonController.ALIGN_MIDDLE :
1249                        FloatingActionButtonController.ALIGN_END;
1250        mFloatingActionButtonController.align(align, 0 /* offsetX */, 0 /* offsetY */, animate);
1251    }
1252}
1253