DialtactsActivity.java revision 752956e3add8b9fff107e1637120a74a61a5fead
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                final boolean dialpadSearch = mIsDialpadShown;
264
265                // Show search result with non-empty text. Show a bare list otherwise.
266                if (TextUtils.isEmpty(newText) && getInSearchUi()) {
267                    exitSearchUi();
268                    mSearchViewCloseButton.setVisibility(View.GONE);
269                    mVoiceSearchButton.setVisibility(View.VISIBLE);
270                    return;
271                } else if (!TextUtils.isEmpty(newText)) {
272                    final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) ||
273                            (!dialpadSearch && mInRegularSearch);
274                    if (!sameSearchMode) {
275                        // call enterSearchUi only if we are switching search modes, or entering
276                        // search ui for the first time
277                        enterSearchUi(dialpadSearch, newText);
278                    }
279
280                    if (dialpadSearch && mSmartDialSearchFragment != null) {
281                            mSmartDialSearchFragment.setQueryString(newText, false);
282                    } else if (mRegularSearchFragment != null) {
283                        mRegularSearchFragment.setQueryString(newText, false);
284                    }
285                    mSearchViewCloseButton.setVisibility(View.VISIBLE);
286                    mVoiceSearchButton.setVisibility(View.GONE);
287                    return;
288                }
289                return;
290            }
291
292            @Override
293            public void afterTextChanged(Editable s) {
294            }
295    };
296
297    @Override
298    protected void onCreate(Bundle savedInstanceState) {
299        super.onCreate(savedInstanceState);
300        mFirstLaunch = true;
301
302        setContentView(R.layout.dialtacts_activity);
303        getWindow().setBackgroundDrawable(null);
304
305        final ActionBar actionBar = getActionBar();
306        actionBar.setCustomView(R.layout.search_edittext);
307        actionBar.setDisplayShowCustomEnabled(true);
308
309        final View customView = actionBar.getCustomView();
310
311        mSearchView = (EditText) customView.findViewById(R.id.search_view);
312        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
313
314        mSearchViewCloseButton = customView.findViewById(R.id.search_close_button);
315        mSearchViewCloseButton.setOnClickListener(this);
316
317        final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
318                new int[] { android.R.attr.actionBarSize });
319        mActionBarHeight = (int) styledAttributes.getDimension(0, 0);
320        styledAttributes.recycle();
321
322        // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState
323        // is null. Otherwise the fragment manager takes care of recreating these fragments.
324        if (savedInstanceState == null) {
325            getFragmentManager().beginTransaction()
326                    .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
327                    .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT)
328                    .commit();
329        } else {
330            mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
331            mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
332            mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
333            mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
334        }
335
336        parentLayout = (RelativeLayout) findViewById(R.id.dialtacts_mainlayout);
337        parentLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
338        parentLayout.setOnDragListener(new LayoutOnDragListener());
339
340        mDialerBackgroundColor = getResources().getColor(R.color.background_dialer_light);
341        mContactListBackgroundColor =
342                getResources().getColor(R.color.contact_list_background_color);
343
344        mFragmentsFrame = findViewById(R.id.dialtacts_frame);
345
346        mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
347        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, getResources());
348
349        mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
350        mFloatingActionButton.setOnClickListener(this);
351
352        mRemoveViewContainer = findViewById(R.id.remove_view_container);
353
354        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
355        SmartDialPrefix.initializeNanpSettings(this);
356    }
357
358    @Override
359    protected void onResume() {
360        super.onResume();
361        if (mFirstLaunch) {
362            displayFragment(getIntent());
363        } else if (!phoneIsInUse() && mInCallDialpadUp) {
364            hideDialpadFragment(false, true);
365            mInCallDialpadUp = false;
366        }
367        mFirstLaunch = false;
368        prepareVoiceSearchButton();
369        mDialerDatabaseHelper.startSmartDialUpdateThread();
370    }
371
372    @Override
373    protected void onPause() {
374        if (mClearSearchOnPause) {
375            hideDialpadAndSearchUi();
376            mClearSearchOnPause = false;
377        }
378        super.onPause();
379    }
380
381    @Override
382    protected void onSaveInstanceState(Bundle outState) {
383        super.onSaveInstanceState(outState);
384        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
385        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
386        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
387        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
388    }
389
390    @Override
391    public void onAttachFragment(Fragment fragment) {
392        if (fragment instanceof DialpadFragment) {
393            mDialpadFragment = (DialpadFragment) fragment;
394            final FragmentTransaction transaction = getFragmentManager().beginTransaction();
395            transaction.hide(mDialpadFragment);
396            transaction.commit();
397        } else if (fragment instanceof SmartDialSearchFragment) {
398            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
399            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
400            if (mFragmentsFrame != null) {
401                mFragmentsFrame.setAlpha(1.0f);
402            }
403        } else if (fragment instanceof SearchFragment) {
404            mRegularSearchFragment = (RegularSearchFragment) fragment;
405            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
406            if (mFragmentsFrame != null) {
407                mFragmentsFrame.setAlpha(1.0f);
408            }
409        } else if (fragment instanceof ListsFragment) {
410            mListsFragment = (ListsFragment) fragment;
411            mListsFragment.addOnPageChangeListener(this);
412        }
413    }
414
415    protected void handleMenuSettings() {
416        openTelephonySetting(this);
417    }
418
419    public static void openTelephonySetting(Activity activity) {
420        final Intent settingsIntent = getCallSettingsIntent();
421        activity.startActivity(settingsIntent);
422    }
423
424    @Override
425    public void onClick(View view) {
426        switch (view.getId()) {
427            case R.id.floating_action_button:
428                if (!mIsDialpadShown) {
429                    mInCallDialpadUp = false;
430                    showDialpadFragment(true);
431                } else {
432                    // Dial button was pressed; tell the Dialpad fragment
433                    mDialpadFragment.dialButtonPressed();
434                }
435                break;
436            case R.id.search_close_button:
437                // Clear the search field
438                if (!TextUtils.isEmpty(mSearchView.getText())) {
439                    mDialpadFragment.clearDialpad();
440                    mSearchView.setText(null);
441                }
442                break;
443            case R.id.voice_search_button:
444                try {
445                    startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
446                            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
447                } catch (ActivityNotFoundException e) {
448                    Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
449                            Toast.LENGTH_SHORT).show();
450                }
451                break;
452            default: {
453                Log.wtf(TAG, "Unexpected onClick event from " + view);
454                break;
455            }
456        }
457    }
458
459    @Override
460    public boolean onOptionsItemSelected(MenuItem item) {
461        switch (item.getItemId()) {
462            case R.id.menu_history:
463                showCallHistory();
464                break;
465            case R.id.menu_add_contact:
466                try {
467                    startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
468                } catch (ActivityNotFoundException e) {
469                    Toast toast = Toast.makeText(this,
470                            R.string.add_contact_not_available,
471                            Toast.LENGTH_SHORT);
472                    toast.show();
473                }
474                break;
475            case R.id.menu_import_export:
476                // We hard-code the "contactsAreAvailable" argument because doing it properly would
477                // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
478                // now in Dialtacts for (potential) performance reasons. Compare with how it is
479                // done in {@link PeopleActivity}.
480                ImportExportDialogFragment.show(getFragmentManager(), true,
481                        DialtactsActivity.class);
482                return true;
483            case R.id.menu_clear_frequents:
484                // TODO: This should be enabled/disabled based on
485                // SpeedDialFragment.hasFrequents
486                ClearFrequentsDialog.show(getFragmentManager());
487                return true;
488            case R.id.menu_call_settings:
489                handleMenuSettings();
490                return true;
491        }
492        return false;
493    }
494
495    @Override
496    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
497        if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
498            if (resultCode == RESULT_OK) {
499                final ArrayList<String> matches = data.getStringArrayListExtra(
500                        RecognizerIntent.EXTRA_RESULTS);
501                if (matches.size() > 0) {
502                    final String match = matches.get(0);
503                    mSearchView.setText(match);
504                } else {
505                    Log.e(TAG, "Voice search - nothing heard");
506                }
507            } else {
508                Log.e(TAG, "Voice search failed");
509            }
510        }
511        super.onActivityResult(requestCode, resultCode, data);
512    }
513
514    /**
515     * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
516     * updates are handled by a callback which is invoked after the dialpad fragment is shown.
517     * @see #onDialpadShown
518     */
519    private void showDialpadFragment(boolean animate) {
520        if (mIsDialpadShown) {
521            return;
522        }
523        mIsDialpadShown = true;
524        mDialpadFragment.setAnimate(animate);
525
526        final FragmentTransaction ft = getFragmentManager().beginTransaction();
527        ft.show(mDialpadFragment);
528        ft.commit();
529    }
530
531    /**
532     * Callback from child DialpadFragment when the dialpad is shown.
533     */
534    public void onDialpadShown() {
535        updateFloatingActionButton();
536        if (mDialpadFragment.getAnimate()) {
537            Animation slideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in);
538            mDialpadFragment.getView().startAnimation(slideIn);
539        } else {
540            mDialpadFragment.setYFraction(0);
541        }
542
543        if (mListsFragment != null && mListsFragment.isResumed() && mListsFragment.isVisible()) {
544            // If the favorites fragment is showing, fade to blank.
545            mFragmentsFrame.animate().alpha(0.0f);
546            parentLayout.setBackgroundColor(mContactListBackgroundColor);
547        }
548
549        updateSearchFragmentPosition();
550        getActionBar().hide();
551    }
552
553    /**
554     * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in
555     * a callback after the hide animation ends.
556     * @see #commitDialpadFragmentHide
557     */
558    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
559        if (mDialpadFragment == null) {
560            return;
561        }
562        if (clearDialpad) {
563            mDialpadFragment.clearDialpad();
564        }
565        if (!mIsDialpadShown) {
566            return;
567        }
568        mIsDialpadShown = false;
569        mDialpadFragment.setAnimate(animate);
570
571        updateFloatingActionButton();
572        if (animate) {
573            Animation slideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out);
574            slideOut.setAnimationListener(new ActivityAnimationListener() {
575                @Override
576                public void onAnimationEnd(Animation animation) {
577                    commitDialpadFragmentHide();
578                }
579            });
580            mDialpadFragment.getView().startAnimation(slideOut);
581        } else {
582            commitDialpadFragmentHide();
583        }
584
585        if (mListsFragment != null && mListsFragment.isVisible()) {
586            mFragmentsFrame.animate().alpha(1.0f);
587            parentLayout.setBackgroundColor(mDialerBackgroundColor);
588        }
589
590        updateSearchFragmentPosition();
591        getActionBar().show();
592    }
593
594    /**
595     * Finishes hiding the dialpad fragment after any animations are completed.
596     */
597    private void commitDialpadFragmentHide() {
598        final FragmentTransaction ft = getFragmentManager().beginTransaction();
599        ft.hide(mDialpadFragment);
600        ft.commit();
601    }
602
603    private void updateSearchFragmentPosition() {
604        int translationValue = mIsDialpadShown ?  -mActionBarHeight : 0;
605        SearchFragment fragment = null;
606        if (mInDialpadSearch) {
607            fragment = mSmartDialSearchFragment;
608        } else if (mInRegularSearch) {
609            fragment = mRegularSearchFragment;
610        }
611        if (fragment != null && fragment.isVisible()) {
612            fragment.getView().animate().translationY(translationValue)
613                    .setInterpolator(hideActionBarInterpolator).setDuration(ANIMATION_DURATION);
614        }
615    }
616
617    private boolean getInSearchUi() {
618        return mInDialpadSearch || mInRegularSearch;
619    }
620
621    private void setNotInSearchUi() {
622        mInDialpadSearch = false;
623        mInRegularSearch = false;
624    }
625
626    private void hideDialpadAndSearchUi() {
627        mSearchView.setText(null);
628        hideDialpadFragment(false, true);
629    }
630
631    private void hideInputMethod(View view) {
632        final InputMethodManager imm = (InputMethodManager) getSystemService(
633                Context.INPUT_METHOD_SERVICE);
634        if (imm != null && view != null) {
635            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
636        }
637    }
638
639    private void prepareVoiceSearchButton() {
640        mVoiceSearchButton = getActionBar().getCustomView().findViewById(R.id.voice_search_button);
641        final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
642        if (canIntentBeHandled(voiceIntent)) {
643            mVoiceSearchButton.setVisibility(View.VISIBLE);
644            mVoiceSearchButton.setOnClickListener(this);
645        } else {
646            mVoiceSearchButton.setVisibility(View.GONE);
647        }
648    }
649
650    @Override
651    public boolean onCreateOptionsMenu(Menu menu) {
652        if (DEBUG) {
653            Log.d(TAG, "onCreateOptionsMenu");
654        }
655        MenuInflater inflater = getMenuInflater();
656        inflater.inflate(R.menu.dialtacts_options, menu);
657
658        if (mPendingSearchViewQuery != null) {
659            mSearchView.setText(mPendingSearchViewQuery);
660            mPendingSearchViewQuery = null;
661        }
662        return super.onCreateOptionsMenu(menu);
663    }
664
665    /**
666     * Returns true if the intent is due to hitting the green send key (hardware call button:
667     * KEYCODE_CALL) while in a call.
668     *
669     * @param intent the intent that launched this activity
670     * @return true if the intent is due to hitting the green send key while in a call
671     */
672    private boolean isSendKeyWhileInCall(Intent intent) {
673        // If there is a call in progress and the user launched the dialer by hitting the call
674        // button, go straight to the in-call screen.
675        final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
676
677        try {
678            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
679            if (callKey && phone != null && phone.showCallScreen()) {
680                return true;
681            }
682        } catch (RemoteException e) {
683            Log.e(TAG, "Failed to handle send while in call", e);
684        }
685
686        return false;
687    }
688
689    /**
690     * Sets the current tab based on the intent's request type
691     *
692     * @param intent Intent that contains information about which tab should be selected
693     */
694    private void displayFragment(Intent intent) {
695        // If we got here by hitting send and we're in call forward along to the in-call activity
696        if (isSendKeyWhileInCall(intent)) {
697            finish();
698            return;
699        }
700
701        if (mDialpadFragment != null) {
702            final boolean phoneIsInUse = phoneIsInUse();
703            if (phoneIsInUse || isDialIntent(intent)) {
704                mDialpadFragment.setStartedFromNewIntent(true);
705                if (phoneIsInUse && !mDialpadFragment.isVisible()) {
706                    mInCallDialpadUp = true;
707                }
708                showDialpadFragment(false);
709            }
710        }
711    }
712
713    @Override
714    public void onNewIntent(Intent newIntent) {
715        setIntent(newIntent);
716        displayFragment(newIntent);
717
718        invalidateOptionsMenu();
719    }
720
721    /** Returns true if the given intent contains a phone number to populate the dialer with */
722    private boolean isDialIntent(Intent intent) {
723        final String action = intent.getAction();
724        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
725            return true;
726        }
727        if (Intent.ACTION_VIEW.equals(action)) {
728            final Uri data = intent.getData();
729            if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) {
730                return true;
731            }
732        }
733        return false;
734    }
735
736    /**
737     * Returns an appropriate call origin for this Activity. May return null when no call origin
738     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
739     * for remembering the tab in which the user made a phone call, so the external app's DIAL
740     * request should not be counted.)
741     */
742    public String getCallOrigin() {
743        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
744    }
745
746    /**
747     * Shows the search fragment
748     */
749    private void enterSearchUi(boolean smartDialSearch, String query) {
750        if (getFragmentManager().isDestroyed()) {
751            // Weird race condition where fragment is doing work after the activity is destroyed
752            // due to talkback being on (b/10209937). Just return since we can't do any
753            // constructive here.
754            return;
755        }
756
757        if (DEBUG) {
758            Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
759        }
760
761        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
762        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
763            transaction.remove(mSmartDialSearchFragment);
764        } else if (mInRegularSearch && mRegularSearchFragment != null) {
765            transaction.remove(mRegularSearchFragment);
766        }
767
768        final String tag;
769        if (smartDialSearch) {
770            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
771        } else {
772            tag = TAG_REGULAR_SEARCH_FRAGMENT;
773        }
774        mInDialpadSearch = smartDialSearch;
775        mInRegularSearch = !smartDialSearch;
776
777        SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
778        if (fragment == null) {
779            if (smartDialSearch) {
780                fragment = new SmartDialSearchFragment();
781            } else {
782                fragment = new RegularSearchFragment();
783            }
784        }
785        // DialtactsActivity will provide the options menu
786        fragment.setHasOptionsMenu(false);
787        transaction.replace(R.id.dialtacts_frame, fragment, tag);
788        transaction.addToBackStack(null);
789        fragment.setQueryString(query, false);
790        transaction.commit();
791    }
792
793    /**
794     * Hides the search fragment
795     */
796    private void exitSearchUi() {
797        // See related bug in enterSearchUI();
798        if (getFragmentManager().isDestroyed()) {
799            return;
800        }
801        // Go all the way back to the favorites fragment, regardless of how many times we
802        // transitioned between search fragments
803        getFragmentManager().popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
804        setNotInSearchUi();
805
806        if (mIsDialpadShown) {
807            mFragmentsFrame.setAlpha(0);
808        }
809    }
810
811    /** Returns an Intent to launch Call Settings screen */
812    public static Intent getCallSettingsIntent() {
813        final Intent intent = new Intent(Intent.ACTION_MAIN);
814        intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
815        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
816        return intent;
817    }
818
819    @Override
820    public void onBackPressed() {
821        if (mIsDialpadShown) {
822            hideDialpadFragment(true, false);
823        } else if (getInSearchUi()) {
824            mSearchView.setText(null);
825            mDialpadFragment.clearDialpad();
826        } else {
827            super.onBackPressed();
828        }
829    }
830
831    @Override
832    public void onDialpadQueryChanged(String query) {
833        if (mSmartDialSearchFragment != null) {
834            mSmartDialSearchFragment.setAddToContactNumber(query);
835        }
836        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
837                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
838
839        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
840            if (DEBUG) {
841                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
842            }
843            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
844                // This callback can happen if the dialpad fragment is recreated because of
845                // activity destruction. In that case, don't update the search view because
846                // that would bring the user back to the search fragment regardless of the
847                // previous state of the application. Instead, just return here and let the
848                // fragment manager correctly figure out whatever fragment was last displayed.
849                if (!TextUtils.isEmpty(normalizedQuery)) {
850                    mPendingSearchViewQuery = normalizedQuery;
851                }
852                return;
853            }
854            mSearchView.setText(normalizedQuery);
855        }
856    }
857
858    @Override
859    public void onListFragmentScrollStateChange(int scrollState) {
860        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
861            hideDialpadFragment(true, false);
862            hideInputMethod(getCurrentFocus());
863        }
864    }
865
866    @Override
867    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
868            int totalItemCount) {
869        // TODO: No-op for now. This should eventually show/hide the actionBar based on
870        // interactions with the ListsFragments.
871    }
872
873    @Override
874    public void setFloatingActionButtonVisible(boolean visible) {
875        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
876    }
877
878    private boolean phoneIsInUse() {
879        final TelephonyManager tm = (TelephonyManager) getSystemService(
880                Context.TELEPHONY_SERVICE);
881        return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
882    }
883
884    public static Intent getAddNumberToContactIntent(CharSequence text) {
885        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
886        intent.putExtra(Intents.Insert.PHONE, text);
887        intent.setType(Contacts.CONTENT_ITEM_TYPE);
888        return intent;
889    }
890
891    private boolean canIntentBeHandled(Intent intent) {
892        final PackageManager packageManager = getPackageManager();
893        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
894                PackageManager.MATCH_DEFAULT_ONLY);
895        return resolveInfo != null && resolveInfo.size() > 0;
896    }
897
898    @Override
899    public void showCallHistory() {
900        // Use explicit CallLogActivity intent instead of ACTION_VIEW +
901        // CONTENT_TYPE, so that we always open our call log from our dialer
902        final Intent intent = new Intent(this, CallLogActivity.class);
903        startActivity(intent);
904    }
905
906    /**
907     * Called when the user has long-pressed a contact tile to start a drag operation.
908     */
909    @Override
910    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
911        getActionBar().hide();
912        mRemoveViewContainer.setVisibility(View.VISIBLE);
913    }
914
915    @Override
916    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
917    }
918
919    /**
920     * Called when the user has released a contact tile after long-pressing it.
921     */
922    @Override
923    public void onDragFinished(int x, int y) {
924        getActionBar().show();
925        mRemoveViewContainer.setVisibility(View.GONE);
926    }
927
928    @Override
929    public void onDroppedOnRemove() {}
930
931    /**
932     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
933     * once it has been attached to the activity.
934     */
935    @Override
936    public void setDragDropController(DragDropController dragController) {
937        mDragDropController = dragController;
938        ((RemoveView) findViewById(R.id.remove_view))
939                .setDragDropController(dragController);
940    }
941
942    @Override
943    public void onPickPhoneNumberAction(Uri dataUri) {
944        // Specify call-origin so that users will see the previous tab instead of
945        // CallLog screen (search UI will be automatically exited).
946        PhoneNumberInteraction.startInteractionForPhoneCall(
947            DialtactsActivity.this, dataUri, getCallOrigin());
948        mClearSearchOnPause = true;
949    }
950
951    @Override
952    public void onCallNumberDirectly(String phoneNumber) {
953        Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
954        startActivity(intent);
955        mClearSearchOnPause = true;
956    }
957
958    @Override
959    public void onShortcutIntentCreated(Intent intent) {
960        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
961    }
962
963    @Override
964    public void onHomeInActionBarSelected() {
965        exitSearchUi();
966    }
967
968    public int getActionBarHeight() {
969        return mActionBarHeight;
970    }
971
972    @Override
973    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
974
975    }
976
977    @Override
978    public void onPageSelected(int position) {
979        mCurrentTabPosition = position;
980        // If the dialpad is showing, the floating action button should always be middle aligned.
981        if (!mIsDialpadShown) {
982            alignFloatingActionButtonByTab(mCurrentTabPosition);
983        }
984    }
985
986    @Override
987    public void onPageScrollStateChanged(int state) {
988    }
989
990    private void updateFloatingActionButton() {
991        if (mIsDialpadShown) {
992            mFloatingActionButton.setImageResource(R.drawable.fab_ic_call);
993            mFloatingActionButton.setContentDescription(
994                    getResources().getString(R.string.description_dial_button));
995            alignFloatingActionButtonByTab(mCurrentTabPosition);
996        } else {
997            mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial);
998            mFloatingActionButton.setContentDescription(
999                    getResources().getString(R.string.action_menu_dialpad_button));
1000            alignFloatingActionButtonMiddle();
1001        }
1002    }
1003
1004    private void alignFloatingActionButtonByTab(int position) {
1005        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
1006            alignFloatingActionButtonMiddle();
1007        } else {
1008            alignFloatingActionButtonRight();
1009        }
1010    }
1011
1012    private void alignFloatingActionButtonRight() {
1013        final RelativeLayout.LayoutParams params =
1014                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1015        params.removeRule(RelativeLayout.CENTER_HORIZONTAL);
1016        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1017        mFloatingActionButtonContainer.setLayoutParams(params);
1018    }
1019
1020    private void alignFloatingActionButtonMiddle() {
1021        final RelativeLayout.LayoutParams params =
1022                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1023        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1024        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
1025        mFloatingActionButtonContainer.setLayoutParams(params);
1026    }
1027
1028    /**
1029     * Convenience class which implements AnimationListener interface as null-op methods.
1030     */
1031    private class ActivityAnimationListener implements AnimationListener {
1032        @Override
1033        public void onAnimationStart(Animation animation) {
1034        }
1035
1036        @Override
1037        public void onAnimationEnd(Animation animation) {
1038        }
1039
1040        @Override
1041        public void onAnimationRepeat(Animation animation) {
1042        }
1043    }
1044}
1045