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