DialtactsActivity.java revision f0a95146bb220db05dd2104e76cd3e1cbab8002a
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.animation.LayoutTransition;
20import android.app.ActionBar;
21import android.app.Activity;
22import android.app.Fragment;
23import android.app.FragmentManager;
24import android.app.FragmentTransaction;
25import android.content.ActivityNotFoundException;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.res.TypedArray;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.provider.ContactsContract.Contacts;
36import android.provider.ContactsContract.Intents;
37import android.speech.RecognizerIntent;
38import android.support.v4.view.ViewPager;
39import android.telephony.TelephonyManager;
40import android.text.Editable;
41import android.text.TextUtils;
42import android.text.TextWatcher;
43import android.util.AttributeSet;
44import android.util.Log;
45import android.view.DragEvent;
46import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.View.OnDragListener;
51import android.view.animation.AccelerateInterpolator;
52import android.view.animation.Animation;
53import android.view.animation.Animation.AnimationListener;
54import android.view.animation.AnimationUtils;
55import android.view.animation.DecelerateInterpolator;
56import android.view.animation.Interpolator;
57import android.view.inputmethod.InputMethodManager;
58import android.widget.AbsListView.OnScrollListener;
59import android.widget.EditText;
60import android.widget.ImageButton;
61import android.widget.PopupMenu;
62import android.widget.RelativeLayout;
63import android.widget.Toast;
64
65import com.android.contacts.common.CallUtil;
66import com.android.contacts.common.activity.TransactionSafeActivity;
67import com.android.contacts.common.dialog.ClearFrequentsDialog;
68import com.android.contacts.common.interactions.ImportExportDialogFragment;
69import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
70import com.android.contacts.common.util.ViewUtil;
71import com.android.dialer.calllog.CallLogActivity;
72import com.android.dialer.database.DialerDatabaseHelper;
73import com.android.dialer.dialpad.DialpadFragment;
74import com.android.dialer.dialpad.SmartDialNameMatcher;
75import com.android.dialer.dialpad.SmartDialPrefix;
76import com.android.dialer.interactions.PhoneNumberInteraction;
77import com.android.dialer.list.DragDropController;
78import com.android.dialer.list.ListsFragment;
79import com.android.dialer.list.OnDragDropListener;
80import com.android.dialer.list.OnListFragmentScrolledListener;
81import com.android.dialer.list.SpeedDialFragment;
82import com.android.dialer.list.PhoneFavoriteSquareTileView;
83import com.android.dialer.list.RegularSearchFragment;
84import com.android.dialer.list.RemoveView;
85import com.android.dialer.list.SearchFragment;
86import com.android.dialer.list.SmartDialSearchFragment;
87import com.android.dialerbind.DatabaseHelperManager;
88import com.android.internal.telephony.ITelephony;
89
90import java.util.ArrayList;
91import java.util.List;
92
93/**
94 * The dialer tab's title is 'phone', a more common name (see strings.xml).
95 */
96public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
97        DialpadFragment.OnDialpadQueryChangedListener,
98        OnListFragmentScrolledListener,
99        DialpadFragment.HostInterface,
100        ListsFragment.HostInterface,
101        SpeedDialFragment.HostInterface,
102        OnDragDropListener,
103        OnPhoneNumberPickerActionListener,
104        ViewPager.OnPageChangeListener {
105    private static final String TAG = "DialtactsActivity";
106
107    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
108
109    public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
110
111    /** Used to open Call Setting */
112    private static final String PHONE_PACKAGE = "com.android.phone";
113    private static final String CALL_SETTINGS_CLASS_NAME =
114            "com.android.phone.CallFeaturesSetting";
115    /** @see #getCallOrigin() */
116    private static final String CALL_ORIGIN_DIALTACTS =
117            "com.android.dialer.DialtactsActivity";
118
119    private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
120    private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
121    private static final String KEY_SEARCH_QUERY = "search_query";
122    private static final String KEY_FIRST_LAUNCH = "first_launch";
123
124    private static final String TAG_DIALPAD_FRAGMENT = "dialpad";
125    private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
126    private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
127    private static final String TAG_FAVORITES_FRAGMENT = "favorites";
128
129    /**
130     * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
131     */
132    private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
133
134    private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
135
136    private static final int ANIMATION_DURATION = 250;
137
138    private RelativeLayout parentLayout;
139
140    /**
141     * Fragment containing the dialpad that slides into view
142     */
143    private DialpadFragment mDialpadFragment;
144
145    /**
146     * Fragment for searching phone numbers using the alphanumeric keyboard.
147     */
148    private RegularSearchFragment mRegularSearchFragment;
149
150    /**
151     * Fragment for searching phone numbers using the dialpad.
152     */
153    private SmartDialSearchFragment mSmartDialSearchFragment;
154
155    /**
156     * Fragment containing the speed dial list, recents list, and all contacts list.
157     */
158    private ListsFragment mListsFragment;
159
160    private View mFloatingActionButtonContainer;
161    private ImageButton mFloatingActionButton;
162
163    private View mFragmentsFrame;
164
165    private int mActionBarHeight;
166    private boolean mInDialpadSearch;
167    private boolean mInRegularSearch;
168    private boolean mClearSearchOnPause;
169    private boolean mIsDialpadShown;
170
171    /**
172     * The position of the currently selected tab in the attached {@link ListsFragment}.
173     */
174    private int mCurrentTabPosition = 0;
175
176    /**
177     * True if the dialpad is only temporarily showing due to being in call
178     */
179    private boolean mInCallDialpadUp;
180
181    /**
182     * True when this activity has been launched for the first time.
183     */
184    private boolean mFirstLaunch;
185
186    /**
187     * Search query to be applied to the SearchView in the ActionBar once
188     * onCreateOptionsMenu has been called.
189     */
190    private String mPendingSearchViewQuery;
191
192    private EditText mSearchView;
193    private View mSearchViewCloseButton;
194    private View mVoiceSearchButton;
195    /**
196     * View that contains the "Remove" dialog that shows up when the user long presses a contact.
197     * If the user releases a contact when hovering on top of this, the contact is unfavorited and
198     * removed from the speed dial list.
199     */
200    private View mRemoveViewContainer;
201
202    final Interpolator hideActionBarInterpolator = new AccelerateInterpolator(1.5f);
203    final Interpolator showActionBarInterpolator = new DecelerateInterpolator(1.5f);
204    private String mSearchQuery;
205
206    private DialerDatabaseHelper mDialerDatabaseHelper;
207    private DragDropController mDragDropController;
208
209    private int mDialerBackgroundColor;
210    private int mContactListBackgroundColor;
211
212    private class OverflowPopupMenu extends PopupMenu {
213        public OverflowPopupMenu(Context context, View anchor) {
214            super(context, anchor);
215        }
216
217        @Override
218        public void show() {
219            final Menu menu = getMenu();
220            final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
221            // TODO: Check mSpeedDialFragment.hasFrequents()
222            clearFrequents.setVisible(true);
223            super.show();
224        }
225    }
226
227    /**
228     * Listener that listens to drag events and sends their x and y coordinates to a
229     * {@link DragDropController}.
230     */
231    private class LayoutOnDragListener implements OnDragListener {
232        @Override
233        public boolean onDrag(View v, DragEvent event) {
234            if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
235                mDragDropController.handleDragHovered(v, (int) event.getX(),
236                        (int) event.getY());
237            }
238            return true;
239        }
240    }
241
242    /**
243     * Listener used to send search queries to the phone search fragment.
244     */
245    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
246        @Override
247        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
248        }
249
250        @Override
251        public void onTextChanged(CharSequence s, int start, int before, int count) {
252            final String newText = s.toString();
253            if (newText.equals(mSearchQuery)) {
254                // If the query hasn't changed (perhaps due to activity being destroyed
255                // and restored, or user launching the same DIAL intent twice), then there is
256                // no need to do anything here.
257                return;
258            }
259            mSearchQuery = newText;
260            if (DEBUG) {
261                Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText);
262            }
263
264            // Show search result with non-empty text. Show a bare list otherwise.
265            final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) ||
266                    (!mIsDialpadShown && mInRegularSearch);
267            if (!sameSearchMode) {
268                // call enterSearchUi only if we are switching search modes, or entering
269                // search ui for the first time
270                enterSearchUi(mIsDialpadShown, newText);
271            }
272
273            if (mIsDialpadShown && mSmartDialSearchFragment != null) {
274                mSmartDialSearchFragment.setQueryString(newText, false /* delaySelection */);
275            } else if (mRegularSearchFragment != null) {
276                mRegularSearchFragment.setQueryString(newText, false /* delaySelection */);
277            }
278
279            if (TextUtils.isEmpty(newText)) {
280                mSearchViewCloseButton.setVisibility(View.GONE);
281                mVoiceSearchButton.setVisibility(View.VISIBLE);
282            } else {
283                mSearchViewCloseButton.setVisibility(View.VISIBLE);
284                mVoiceSearchButton.setVisibility(View.GONE);
285            }
286        }
287
288        @Override
289        public void afterTextChanged(Editable s) {
290        }
291    };
292
293    @Override
294    protected void onCreate(Bundle savedInstanceState) {
295        super.onCreate(savedInstanceState);
296        mFirstLaunch = true;
297
298        setContentView(R.layout.dialtacts_activity);
299        getWindow().setBackgroundDrawable(null);
300
301        final ActionBar actionBar = getActionBar();
302        actionBar.setCustomView(R.layout.search_edittext);
303        actionBar.setDisplayShowCustomEnabled(true);
304
305        final View customView = actionBar.getCustomView();
306
307        mSearchView = (EditText) customView.findViewById(R.id.search_view);
308        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
309
310        mSearchViewCloseButton = customView.findViewById(R.id.search_close_button);
311        mSearchViewCloseButton.setOnClickListener(this);
312
313        final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
314                new int[] { android.R.attr.actionBarSize });
315        mActionBarHeight = (int) styledAttributes.getDimension(0, 0);
316        styledAttributes.recycle();
317
318        // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState
319        // is null. Otherwise the fragment manager takes care of recreating these fragments.
320        if (savedInstanceState == null) {
321            getFragmentManager().beginTransaction()
322                    .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
323                    .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT)
324                    .commit();
325        } else {
326            mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
327            mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
328            mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
329            mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
330        }
331
332        parentLayout = (RelativeLayout) findViewById(R.id.dialtacts_mainlayout);
333        parentLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
334        parentLayout.setOnDragListener(new LayoutOnDragListener());
335
336        mDialerBackgroundColor = getResources().getColor(R.color.background_dialer_light);
337        mContactListBackgroundColor =
338                getResources().getColor(R.color.contact_list_background_color);
339
340        mFragmentsFrame = findViewById(R.id.dialtacts_frame);
341
342        mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
343        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, getResources());
344
345        mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
346        mFloatingActionButton.setOnClickListener(this);
347
348        mRemoveViewContainer = findViewById(R.id.remove_view_container);
349
350        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
351        SmartDialPrefix.initializeNanpSettings(this);
352    }
353
354    @Override
355    protected void onResume() {
356        super.onResume();
357        if (mFirstLaunch) {
358            displayFragment(getIntent());
359        } else if (!phoneIsInUse() && mInCallDialpadUp) {
360            hideDialpadFragment(false, true);
361            mInCallDialpadUp = false;
362        }
363        mFirstLaunch = false;
364        prepareVoiceSearchButton();
365        mDialerDatabaseHelper.startSmartDialUpdateThread();
366    }
367
368    @Override
369    protected void onPause() {
370        if (mClearSearchOnPause) {
371            hideDialpadAndSearchUi();
372            mClearSearchOnPause = false;
373        }
374        super.onPause();
375    }
376
377    @Override
378    protected void onSaveInstanceState(Bundle outState) {
379        super.onSaveInstanceState(outState);
380        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
381        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
382        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
383        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
384    }
385
386    @Override
387    public void onAttachFragment(Fragment fragment) {
388        if (fragment instanceof DialpadFragment) {
389            mDialpadFragment = (DialpadFragment) fragment;
390            final FragmentTransaction transaction = getFragmentManager().beginTransaction();
391            transaction.hide(mDialpadFragment);
392            transaction.commit();
393        } else if (fragment instanceof SmartDialSearchFragment) {
394            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
395            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
396            mSmartDialSearchFragment.setShowEmptyListForNullQuery(true);
397            if (mFragmentsFrame != null) {
398                mFragmentsFrame.setAlpha(1.0f);
399            }
400        } else if (fragment instanceof SearchFragment) {
401            mRegularSearchFragment = (RegularSearchFragment) fragment;
402            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
403            mRegularSearchFragment.setShowEmptyListForNullQuery(true);
404            if (mFragmentsFrame != null) {
405                mFragmentsFrame.setAlpha(1.0f);
406            }
407        } else if (fragment instanceof ListsFragment) {
408            mListsFragment = (ListsFragment) fragment;
409            mListsFragment.addOnPageChangeListener(this);
410        }
411    }
412
413    protected void handleMenuSettings() {
414        openTelephonySetting(this);
415    }
416
417    public static void openTelephonySetting(Activity activity) {
418        final Intent settingsIntent = getCallSettingsIntent();
419        activity.startActivity(settingsIntent);
420    }
421
422    @Override
423    public void onClick(View view) {
424        switch (view.getId()) {
425            case R.id.floating_action_button:
426                if (!mIsDialpadShown) {
427                    mInCallDialpadUp = false;
428                    showDialpadFragment(true);
429                } else {
430                    // Dial button was pressed; tell the Dialpad fragment
431                    mDialpadFragment.dialButtonPressed();
432                }
433                break;
434            case R.id.search_close_button:
435                // Clear the search field
436                if (!TextUtils.isEmpty(mSearchView.getText())) {
437                    mDialpadFragment.clearDialpad();
438                    mSearchView.setText(null);
439                }
440                break;
441            case R.id.voice_search_button:
442                try {
443                    startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
444                            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
445                } catch (ActivityNotFoundException e) {
446                    Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
447                            Toast.LENGTH_SHORT).show();
448                }
449                break;
450            default: {
451                Log.wtf(TAG, "Unexpected onClick event from " + view);
452                break;
453            }
454        }
455    }
456
457    @Override
458    public boolean onOptionsItemSelected(MenuItem item) {
459        switch (item.getItemId()) {
460            case R.id.menu_history:
461                showCallHistory();
462                break;
463            case R.id.menu_add_contact:
464                try {
465                    startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
466                } catch (ActivityNotFoundException e) {
467                    Toast toast = Toast.makeText(this,
468                            R.string.add_contact_not_available,
469                            Toast.LENGTH_SHORT);
470                    toast.show();
471                }
472                break;
473            case R.id.menu_import_export:
474                // We hard-code the "contactsAreAvailable" argument because doing it properly would
475                // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
476                // now in Dialtacts for (potential) performance reasons. Compare with how it is
477                // done in {@link PeopleActivity}.
478                ImportExportDialogFragment.show(getFragmentManager(), true,
479                        DialtactsActivity.class);
480                return true;
481            case R.id.menu_clear_frequents:
482                // TODO: This should be enabled/disabled based on
483                // SpeedDialFragment.hasFrequents
484                ClearFrequentsDialog.show(getFragmentManager());
485                return true;
486            case R.id.menu_call_settings:
487                handleMenuSettings();
488                return true;
489        }
490        return false;
491    }
492
493    @Override
494    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
495        if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
496            if (resultCode == RESULT_OK) {
497                final ArrayList<String> matches = data.getStringArrayListExtra(
498                        RecognizerIntent.EXTRA_RESULTS);
499                if (matches.size() > 0) {
500                    final String match = matches.get(0);
501                    mSearchView.setText(match);
502                } else {
503                    Log.e(TAG, "Voice search - nothing heard");
504                }
505            } else {
506                Log.e(TAG, "Voice search failed");
507            }
508        }
509        super.onActivityResult(requestCode, resultCode, data);
510    }
511
512    /**
513     * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
514     * updates are handled by a callback which is invoked after the dialpad fragment is shown.
515     * @see #onDialpadShown
516     */
517    private void showDialpadFragment(boolean animate) {
518        if (mIsDialpadShown) {
519            return;
520        }
521        mIsDialpadShown = true;
522        mDialpadFragment.setAnimate(animate);
523
524        final FragmentTransaction ft = getFragmentManager().beginTransaction();
525        ft.show(mDialpadFragment);
526        ft.commit();
527    }
528
529    /**
530     * Callback from child DialpadFragment when the dialpad is shown.
531     */
532    public void onDialpadShown() {
533        updateFloatingActionButton();
534        if (mDialpadFragment.getAnimate()) {
535            Animation slideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in);
536            mDialpadFragment.getView().startAnimation(slideIn);
537        } else {
538            mDialpadFragment.setYFraction(0);
539        }
540
541        if (mListsFragment != null && mListsFragment.isResumed() && mListsFragment.isVisible()) {
542            // If the favorites fragment is showing, fade to blank.
543            mFragmentsFrame.animate().alpha(0.0f);
544            parentLayout.setBackgroundColor(mContactListBackgroundColor);
545        }
546
547        updateSearchFragmentPosition();
548        getActionBar().hide();
549    }
550
551    /**
552     * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in
553     * a callback after the hide animation ends.
554     * @see #commitDialpadFragmentHide
555     */
556    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
557        if (mDialpadFragment == null) {
558            return;
559        }
560        if (clearDialpad) {
561            mDialpadFragment.clearDialpad();
562        }
563        if (!mIsDialpadShown) {
564            return;
565        }
566        mIsDialpadShown = false;
567        mDialpadFragment.setAnimate(animate);
568
569        updateFloatingActionButton();
570        if (animate) {
571            Animation slideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out);
572            slideOut.setAnimationListener(new ActivityAnimationListener() {
573                @Override
574                public void onAnimationEnd(Animation animation) {
575                    commitDialpadFragmentHide();
576                }
577            });
578            mDialpadFragment.getView().startAnimation(slideOut);
579        } else {
580            commitDialpadFragmentHide();
581        }
582
583        if (mListsFragment != null && mListsFragment.isVisible()) {
584            mFragmentsFrame.animate().alpha(1.0f);
585            parentLayout.setBackgroundColor(mDialerBackgroundColor);
586        }
587
588        updateSearchFragmentPosition();
589        getActionBar().show();
590    }
591
592    /**
593     * Finishes hiding the dialpad fragment after any animations are completed.
594     */
595    private void commitDialpadFragmentHide() {
596        final FragmentTransaction ft = getFragmentManager().beginTransaction();
597        ft.hide(mDialpadFragment);
598        ft.commit();
599    }
600
601    private void updateSearchFragmentPosition() {
602        int translationValue = mIsDialpadShown ?  -mActionBarHeight : 0;
603        SearchFragment fragment = null;
604        if (mInDialpadSearch) {
605            fragment = mSmartDialSearchFragment;
606        } else if (mInRegularSearch) {
607            fragment = mRegularSearchFragment;
608        }
609        if (fragment != null && fragment.isVisible()) {
610            fragment.getView().animate().translationY(translationValue)
611                    .setInterpolator(hideActionBarInterpolator).setDuration(ANIMATION_DURATION);
612        }
613    }
614
615    private boolean getInSearchUi() {
616        return mInDialpadSearch || mInRegularSearch;
617    }
618
619    private void setNotInSearchUi() {
620        mInDialpadSearch = false;
621        mInRegularSearch = false;
622    }
623
624    private void hideDialpadAndSearchUi() {
625        mSearchView.setText(null);
626        hideDialpadFragment(false, true);
627    }
628
629    private void hideInputMethod(View view) {
630        final InputMethodManager imm = (InputMethodManager) getSystemService(
631                Context.INPUT_METHOD_SERVICE);
632        if (imm != null && view != null) {
633            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
634        }
635    }
636
637    private void prepareVoiceSearchButton() {
638        mVoiceSearchButton = getActionBar().getCustomView().findViewById(R.id.voice_search_button);
639        final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
640        if (canIntentBeHandled(voiceIntent)) {
641            mVoiceSearchButton.setVisibility(View.VISIBLE);
642            mVoiceSearchButton.setOnClickListener(this);
643        } else {
644            mVoiceSearchButton.setVisibility(View.GONE);
645        }
646    }
647
648    @Override
649    public boolean onCreateOptionsMenu(Menu menu) {
650        if (DEBUG) {
651            Log.d(TAG, "onCreateOptionsMenu");
652        }
653        MenuInflater inflater = getMenuInflater();
654        inflater.inflate(R.menu.dialtacts_options, menu);
655
656        if (mPendingSearchViewQuery != null) {
657            mSearchView.setText(mPendingSearchViewQuery);
658            mPendingSearchViewQuery = null;
659        }
660        return super.onCreateOptionsMenu(menu);
661    }
662
663    /**
664     * Returns true if the intent is due to hitting the green send key (hardware call button:
665     * KEYCODE_CALL) while in a call.
666     *
667     * @param intent the intent that launched this activity
668     * @return true if the intent is due to hitting the green send key while in a call
669     */
670    private boolean isSendKeyWhileInCall(Intent intent) {
671        // If there is a call in progress and the user launched the dialer by hitting the call
672        // button, go straight to the in-call screen.
673        final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
674
675        try {
676            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
677            if (callKey && phone != null && phone.showCallScreen()) {
678                return true;
679            }
680        } catch (RemoteException e) {
681            Log.e(TAG, "Failed to handle send while in call", e);
682        }
683
684        return false;
685    }
686
687    /**
688     * Sets the current tab based on the intent's request type
689     *
690     * @param intent Intent that contains information about which tab should be selected
691     */
692    private void displayFragment(Intent intent) {
693        // If we got here by hitting send and we're in call forward along to the in-call activity
694        if (isSendKeyWhileInCall(intent)) {
695            finish();
696            return;
697        }
698
699        if (mDialpadFragment != null) {
700            final boolean phoneIsInUse = phoneIsInUse();
701            if (phoneIsInUse || isDialIntent(intent)) {
702                mDialpadFragment.setStartedFromNewIntent(true);
703                if (phoneIsInUse && !mDialpadFragment.isVisible()) {
704                    mInCallDialpadUp = true;
705                }
706                showDialpadFragment(false);
707            }
708        }
709    }
710
711    @Override
712    public void onNewIntent(Intent newIntent) {
713        setIntent(newIntent);
714        displayFragment(newIntent);
715
716        invalidateOptionsMenu();
717    }
718
719    /** Returns true if the given intent contains a phone number to populate the dialer with */
720    private boolean isDialIntent(Intent intent) {
721        final String action = intent.getAction();
722        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
723            return true;
724        }
725        if (Intent.ACTION_VIEW.equals(action)) {
726            final Uri data = intent.getData();
727            if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) {
728                return true;
729            }
730        }
731        return false;
732    }
733
734    /**
735     * Returns an appropriate call origin for this Activity. May return null when no call origin
736     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
737     * for remembering the tab in which the user made a phone call, so the external app's DIAL
738     * request should not be counted.)
739     */
740    public String getCallOrigin() {
741        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
742    }
743
744    /**
745     * Shows the search fragment
746     */
747    private void enterSearchUi(boolean smartDialSearch, String query) {
748        if (getFragmentManager().isDestroyed()) {
749            // Weird race condition where fragment is doing work after the activity is destroyed
750            // due to talkback being on (b/10209937). Just return since we can't do any
751            // constructive here.
752            return;
753        }
754
755        if (DEBUG) {
756            Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
757        }
758
759        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
760        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
761            transaction.remove(mSmartDialSearchFragment);
762        } else if (mInRegularSearch && mRegularSearchFragment != null) {
763            transaction.remove(mRegularSearchFragment);
764        }
765
766        final String tag;
767        if (smartDialSearch) {
768            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
769        } else {
770            tag = TAG_REGULAR_SEARCH_FRAGMENT;
771        }
772        mInDialpadSearch = smartDialSearch;
773        mInRegularSearch = !smartDialSearch;
774
775        SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
776        if (fragment == null) {
777            if (smartDialSearch) {
778                fragment = new SmartDialSearchFragment();
779            } else {
780                fragment = new RegularSearchFragment();
781            }
782        }
783        // DialtactsActivity will provide the options menu
784        fragment.setHasOptionsMenu(false);
785        transaction.replace(R.id.dialtacts_frame, fragment, tag);
786        transaction.addToBackStack(null);
787        fragment.setQueryString(query, false /* delaySelection */);
788        transaction.commit();
789    }
790
791    /**
792     * Hides the search fragment
793     */
794    private void exitSearchUi() {
795        // See related bug in enterSearchUI();
796        if (getFragmentManager().isDestroyed()) {
797            return;
798        }
799        // Go all the way back to the favorites fragment, regardless of how many times we
800        // transitioned between search fragments
801        getFragmentManager().popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
802        setNotInSearchUi();
803
804        if (mIsDialpadShown) {
805            mFragmentsFrame.setAlpha(0);
806        }
807    }
808
809    /** Returns an Intent to launch Call Settings screen */
810    public static Intent getCallSettingsIntent() {
811        final Intent intent = new Intent(Intent.ACTION_MAIN);
812        intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
813        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
814        return intent;
815    }
816
817    @Override
818    public void onBackPressed() {
819        if (mIsDialpadShown) {
820            hideDialpadFragment(true, false);
821        } else if (getInSearchUi()) {
822            mSearchView.setText(null);
823            mDialpadFragment.clearDialpad();
824            exitSearchUi();
825        } else {
826            super.onBackPressed();
827        }
828    }
829
830    @Override
831    public void onDialpadQueryChanged(String query) {
832        if (mSmartDialSearchFragment != null) {
833            mSmartDialSearchFragment.setAddToContactNumber(query);
834        }
835        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
836                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
837
838        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
839            if (DEBUG) {
840                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
841            }
842            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
843                // This callback can happen if the dialpad fragment is recreated because of
844                // activity destruction. In that case, don't update the search view because
845                // that would bring the user back to the search fragment regardless of the
846                // previous state of the application. Instead, just return here and let the
847                // fragment manager correctly figure out whatever fragment was last displayed.
848                if (!TextUtils.isEmpty(normalizedQuery)) {
849                    mPendingSearchViewQuery = normalizedQuery;
850                }
851                return;
852            }
853            mSearchView.setText(normalizedQuery);
854        }
855    }
856
857    @Override
858    public void onListFragmentScrollStateChange(int scrollState) {
859        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
860            hideDialpadFragment(true, false);
861            hideInputMethod(getCurrentFocus());
862        }
863    }
864
865    @Override
866    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
867                                     int totalItemCount) {
868        // TODO: No-op for now. This should eventually show/hide the actionBar based on
869        // interactions with the ListsFragments.
870    }
871
872    @Override
873    public void setFloatingActionButtonVisible(boolean visible) {
874        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
875    }
876
877    private boolean phoneIsInUse() {
878        final TelephonyManager tm = (TelephonyManager) getSystemService(
879                Context.TELEPHONY_SERVICE);
880        return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
881    }
882
883    public static Intent getAddNumberToContactIntent(CharSequence text) {
884        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
885        intent.putExtra(Intents.Insert.PHONE, text);
886        intent.setType(Contacts.CONTENT_ITEM_TYPE);
887        return intent;
888    }
889
890    private boolean canIntentBeHandled(Intent intent) {
891        final PackageManager packageManager = getPackageManager();
892        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
893                PackageManager.MATCH_DEFAULT_ONLY);
894        return resolveInfo != null && resolveInfo.size() > 0;
895    }
896
897    @Override
898    public void showCallHistory() {
899        // Use explicit CallLogActivity intent instead of ACTION_VIEW +
900        // CONTENT_TYPE, so that we always open our call log from our dialer
901        final Intent intent = new Intent(this, CallLogActivity.class);
902        startActivity(intent);
903    }
904
905    /**
906     * Called when the user has long-pressed a contact tile to start a drag operation.
907     */
908    @Override
909    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
910        getActionBar().hide();
911        mRemoveViewContainer.setVisibility(View.VISIBLE);
912    }
913
914    @Override
915    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
916    }
917
918    /**
919     * Called when the user has released a contact tile after long-pressing it.
920     */
921    @Override
922    public void onDragFinished(int x, int y) {
923        getActionBar().show();
924        mRemoveViewContainer.setVisibility(View.GONE);
925    }
926
927    @Override
928    public void onDroppedOnRemove() {}
929
930    /**
931     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
932     * once it has been attached to the activity.
933     */
934    @Override
935    public void setDragDropController(DragDropController dragController) {
936        mDragDropController = dragController;
937        ((RemoveView) findViewById(R.id.remove_view))
938                .setDragDropController(dragController);
939    }
940
941    @Override
942    public void onPickPhoneNumberAction(Uri dataUri) {
943        // Specify call-origin so that users will see the previous tab instead of
944        // CallLog screen (search UI will be automatically exited).
945        PhoneNumberInteraction.startInteractionForPhoneCall(
946                DialtactsActivity.this, dataUri, getCallOrigin());
947        mClearSearchOnPause = true;
948    }
949
950    @Override
951    public void onCallNumberDirectly(String phoneNumber) {
952        Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
953        startActivity(intent);
954        mClearSearchOnPause = true;
955    }
956
957    @Override
958    public void onShortcutIntentCreated(Intent intent) {
959        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
960    }
961
962    @Override
963    public void onHomeInActionBarSelected() {
964        exitSearchUi();
965    }
966
967    public int getActionBarHeight() {
968        return mActionBarHeight;
969    }
970
971    @Override
972    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
973
974    }
975
976    @Override
977    public void onPageSelected(int position) {
978        mCurrentTabPosition = position;
979        // If the dialpad is showing, the floating action button should always be middle aligned.
980        if (!mIsDialpadShown) {
981            alignFloatingActionButtonByTab(mCurrentTabPosition);
982        }
983    }
984
985    @Override
986    public void onPageScrollStateChanged(int state) {
987    }
988
989    private void updateFloatingActionButton() {
990        if (mIsDialpadShown) {
991            mFloatingActionButton.setImageResource(R.drawable.fab_ic_call);
992            mFloatingActionButton.setContentDescription(
993                    getResources().getString(R.string.description_dial_button));
994            alignFloatingActionButtonMiddle();
995        } else {
996            mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial);
997            mFloatingActionButton.setContentDescription(
998                    getResources().getString(R.string.action_menu_dialpad_button));
999            alignFloatingActionButtonByTab(mCurrentTabPosition);
1000        }
1001    }
1002
1003    private void alignFloatingActionButtonByTab(int position) {
1004        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
1005            alignFloatingActionButtonMiddle();
1006        } else {
1007            alignFloatingActionButtonRight();
1008        }
1009    }
1010
1011    private void alignFloatingActionButtonRight() {
1012        final RelativeLayout.LayoutParams params =
1013                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1014        params.removeRule(RelativeLayout.CENTER_HORIZONTAL);
1015        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1016        mFloatingActionButtonContainer.setLayoutParams(params);
1017    }
1018
1019    private void alignFloatingActionButtonMiddle() {
1020        final RelativeLayout.LayoutParams params =
1021                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1022        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1023        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
1024        mFloatingActionButtonContainer.setLayoutParams(params);
1025    }
1026
1027    /**
1028     * Convenience class which implements AnimationListener interface as null-op methods.
1029     */
1030    private class ActivityAnimationListener implements AnimationListener {
1031        @Override
1032        public void onAnimationStart(Animation animation) {
1033        }
1034
1035        @Override
1036        public void onAnimationEnd(Animation animation) {
1037        }
1038
1039        @Override
1040        public void onAnimationRepeat(Animation animation) {
1041        }
1042    }
1043}
1044