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