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