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