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