DialtactsActivity.java revision 4e28c1d175a1a9e565b974c85a54709f41ee925b
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.DecelerateInterpolator;
53import android.view.animation.Interpolator;
54import android.view.inputmethod.InputMethodManager;
55import android.widget.AbsListView.OnScrollListener;
56import android.widget.EditText;
57import android.widget.PopupMenu;
58import android.widget.RelativeLayout;
59import android.widget.Toast;
60
61import com.android.contacts.common.CallUtil;
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.list.OnPhoneNumberPickerActionListener;
66import com.android.contacts.common.util.ViewUtil;
67import com.android.dialer.calllog.CallLogActivity;
68import com.android.dialer.database.DialerDatabaseHelper;
69import com.android.dialer.dialpad.DialpadFragment;
70import com.android.dialer.dialpad.SmartDialNameMatcher;
71import com.android.dialer.dialpad.SmartDialPrefix;
72import com.android.dialer.interactions.PhoneNumberInteraction;
73import com.android.dialer.list.DragDropController;
74import com.android.dialer.list.ListsFragment;
75import com.android.dialer.list.OnDragDropListener;
76import com.android.dialer.list.OnListFragmentScrolledListener;
77import com.android.dialer.list.SpeedDialFragment;
78import com.android.dialer.list.PhoneFavoriteSquareTileView;
79import com.android.dialer.list.RegularSearchFragment;
80import com.android.dialer.list.RemoveView;
81import com.android.dialer.list.SearchFragment;
82import com.android.dialer.list.SmartDialSearchFragment;
83import com.android.dialerbind.DatabaseHelperManager;
84import com.android.internal.telephony.ITelephony;
85
86import java.util.ArrayList;
87import java.util.List;
88
89/**
90 * The dialer tab's title is 'phone', a more common name (see strings.xml).
91 */
92public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
93        DialpadFragment.OnDialpadQueryChangedListener,
94        OnListFragmentScrolledListener,
95        DialpadFragment.HostInterface,
96        ListsFragment.HostInterface,
97        SpeedDialFragment.HostInterface,
98        OnDragDropListener, View.OnLongClickListener,
99        OnPhoneNumberPickerActionListener,
100        ViewPager.OnPageChangeListener {
101    private static final String TAG = "DialtactsActivity";
102
103    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
104
105    public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
106
107    /** Used to open Call Setting */
108    private static final String PHONE_PACKAGE = "com.android.phone";
109    private static final String CALL_SETTINGS_CLASS_NAME =
110            "com.android.phone.CallFeaturesSetting";
111    /** @see #getCallOrigin() */
112    private static final String CALL_ORIGIN_DIALTACTS =
113            "com.android.dialer.DialtactsActivity";
114
115    private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
116    private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
117    private static final String KEY_SEARCH_QUERY = "search_query";
118    private static final String KEY_FIRST_LAUNCH = "first_launch";
119
120    private static final String TAG_DIALPAD_FRAGMENT = "dialpad";
121    private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
122    private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
123    private static final String TAG_FAVORITES_FRAGMENT = "favorites";
124
125    /**
126     * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
127     */
128    private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
129
130    private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
131
132    private static final int ANIMATION_DURATION = 250;
133
134    private RelativeLayout parentLayout;
135
136    /**
137     * Fragment containing the dialpad that slides into view
138     */
139    private DialpadFragment mDialpadFragment;
140
141    /**
142     * Fragment for searching phone numbers using the alphanumeric keyboard.
143     */
144    private RegularSearchFragment mRegularSearchFragment;
145
146    /**
147     * Fragment for searching phone numbers using the dialpad.
148     */
149    private SmartDialSearchFragment mSmartDialSearchFragment;
150
151    /**
152     * Fragment containing the speed dial list, recents list, and all contacts list.
153     */
154    private ListsFragment mListsFragment;
155
156    private View mFloatingActionButton;
157    private View mDialpadButton;
158    private View mDialButton;
159
160    private View mFragmentsFrame;
161
162    private int mActionBarHeight;
163    private boolean mInDialpadSearch;
164    private boolean mInRegularSearch;
165    private boolean mClearSearchOnPause;
166
167    /**
168     * The position of the currently selected tab in the attached {@link ListsFragment}.
169     */
170    private int mCurrentTabPosition = 0;
171
172    /**
173     * True if the dialpad is only temporarily showing due to being in call
174     */
175    private boolean mInCallDialpadUp;
176
177    /**
178     * True when this activity has been launched for the first time.
179     */
180    private boolean mFirstLaunch;
181
182    /**
183     * Search query to be applied to the SearchView in the ActionBar once
184     * onCreateOptionsMenu has been called.
185     */
186    private String mPendingSearchViewQuery;
187
188    private EditText mSearchView;
189    private View mSearchViewCloseButton;
190    private View mVoiceSearchButton;
191    /**
192     * View that contains the "Remove" dialog that shows up when the user long presses a contact.
193     * If the user releases a contact when hovering on top of this, the contact is unfavorited and
194     * removed from the speed dial list.
195     */
196    private View mRemoveViewContainer;
197
198    final Interpolator hideActionBarInterpolator = new AccelerateInterpolator(1.5f);
199    final Interpolator showActionBarInterpolator = new DecelerateInterpolator(1.5f);
200    private String mSearchQuery;
201
202    private DialerDatabaseHelper mDialerDatabaseHelper;
203    private DragDropController mDragDropController;
204
205    private int mDialerBackgroundColor;
206    private int mContactListBackgroundColor;
207
208    private class OverflowPopupMenu extends PopupMenu {
209        public OverflowPopupMenu(Context context, View anchor) {
210            super(context, anchor);
211        }
212
213        @Override
214        public void show() {
215            final Menu menu = getMenu();
216            final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
217            // TODO: Check mSpeedDialFragment.hasFrequents()
218            clearFrequents.setVisible(true);
219            super.show();
220        }
221    }
222
223    /**
224     * Listener that listens to drag events and sends their x and y coordinates to a
225     * {@link DragDropController}.
226     */
227    private class LayoutOnDragListener implements OnDragListener {
228        @Override
229        public boolean onDrag(View v, DragEvent event) {
230            if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
231                mDragDropController.handleDragHovered(v, (int) event.getX(),
232                        (int) event.getY());
233            }
234            return true;
235        }
236    }
237
238    /**
239     * Listener used to send search queries to the phone search fragment.
240     */
241    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
242            @Override
243            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
244            }
245
246            @Override
247            public void onTextChanged(CharSequence s, int start, int before, int count) {
248                final String newText = s.toString();
249                if (newText.equals(mSearchQuery)) {
250                    // If the query hasn't changed (perhaps due to activity being destroyed
251                    // and restored, or user launching the same DIAL intent twice), then there is
252                    // no need to do anything here.
253                    return;
254                }
255                mSearchQuery = newText;
256                if (DEBUG) {
257                    Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText);
258                }
259                final boolean dialpadSearch = isDialpadShowing();
260
261                // Show search result with non-empty text. Show a bare list otherwise.
262                if (TextUtils.isEmpty(newText) && getInSearchUi()) {
263                    exitSearchUi();
264                    mSearchViewCloseButton.setVisibility(View.GONE);
265                    mVoiceSearchButton.setVisibility(View.VISIBLE);
266                    return;
267                } else if (!TextUtils.isEmpty(newText)) {
268                    final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) ||
269                            (!dialpadSearch && mInRegularSearch);
270                    if (!sameSearchMode) {
271                        // call enterSearchUi only if we are switching search modes, or entering
272                        // search ui for the first time
273                        enterSearchUi(dialpadSearch, newText);
274                    }
275
276                    if (dialpadSearch && mSmartDialSearchFragment != null) {
277                            mSmartDialSearchFragment.setQueryString(newText, false);
278                    } else if (mRegularSearchFragment != null) {
279                        mRegularSearchFragment.setQueryString(newText, false);
280                    }
281                    mSearchViewCloseButton.setVisibility(View.VISIBLE);
282                    mVoiceSearchButton.setVisibility(View.GONE);
283                    return;
284                }
285                return;
286            }
287
288            @Override
289            public void afterTextChanged(Editable s) {
290            }
291    };
292
293    private boolean isDialpadShowing() {
294        return mDialpadFragment != null && mDialpadFragment.isVisible();
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        mFloatingActionButton = findViewById(R.id.floating_action_button);
347        ViewUtil.setupFloatingActionButton(mFloatingActionButton, getResources());
348
349        mDialButton = findViewById(R.id.dial_button);
350        mDialButton.setOnClickListener(this);
351        mDialpadButton = findViewById(R.id.dialpad_button);
352        mDialpadButton.setOnClickListener(this);
353
354        mRemoveViewContainer = findViewById(R.id.remove_view_container);
355
356        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
357        SmartDialPrefix.initializeNanpSettings(this);
358    }
359
360    @Override
361    protected void onResume() {
362        super.onResume();
363        if (mFirstLaunch) {
364            displayFragment(getIntent());
365        } else if (!phoneIsInUse() && mInCallDialpadUp) {
366            hideDialpadFragment(false, true);
367            mInCallDialpadUp = false;
368        }
369        mFirstLaunch = false;
370        prepareVoiceSearchButton();
371        mDialerDatabaseHelper.startSmartDialUpdateThread();
372    }
373
374    @Override
375    protected void onPause() {
376        if (mClearSearchOnPause) {
377            hideDialpadAndSearchUi();
378            mClearSearchOnPause = false;
379        }
380        super.onPause();
381    }
382
383    @Override
384    protected void onSaveInstanceState(Bundle outState) {
385        super.onSaveInstanceState(outState);
386        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
387        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
388        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
389        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
390    }
391
392    @Override
393    public void onAttachFragment(Fragment fragment) {
394        if (fragment instanceof DialpadFragment) {
395            mDialpadFragment = (DialpadFragment) fragment;
396            final FragmentTransaction transaction = getFragmentManager().beginTransaction();
397            transaction.hide(mDialpadFragment);
398            transaction.commit();
399        } else if (fragment instanceof SmartDialSearchFragment) {
400            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
401            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
402            if (mFragmentsFrame != null) {
403                mFragmentsFrame.setAlpha(1.0f);
404            }
405        } else if (fragment instanceof SearchFragment) {
406            mRegularSearchFragment = (RegularSearchFragment) fragment;
407            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
408            if (mFragmentsFrame != null) {
409                mFragmentsFrame.setAlpha(1.0f);
410            }
411        } else if (fragment instanceof ListsFragment) {
412            mListsFragment = (ListsFragment) fragment;
413            mListsFragment.addOnPageChangeListener(this);
414        }
415    }
416
417    protected void handleMenuSettings() {
418        openTelephonySetting(this);
419    }
420
421    public static void openTelephonySetting(Activity activity) {
422        final Intent settingsIntent = getCallSettingsIntent();
423        activity.startActivity(settingsIntent);
424    }
425
426    @Override
427    public void onClick(View view) {
428        switch (view.getId()) {
429            case R.id.dialpad_button:
430                // Reset the boolean flag that tracks whether the dialpad was up because
431                // we were in call. Regardless of whether it was true before, we want to
432                // show the dialpad because the user has explicitly clicked the dialpad
433                // button.
434                mInCallDialpadUp = false;
435                showDialpadFragment(true);
436                break;
437            case R.id.dial_button:
438                // Dial button was pressed; tell the Dialpad fragment
439                mDialpadFragment.dialButtonPressed();
440                break;
441            case R.id.search_close_button:
442                // Clear the search field
443                if (!TextUtils.isEmpty(mSearchView.getText())) {
444                    mDialpadFragment.clearDialpad();
445                    mSearchView.setText(null);
446                }
447                break;
448            case R.id.voice_search_button:
449                try {
450                    startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
451                            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
452                } catch (ActivityNotFoundException e) {
453                    Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
454                            Toast.LENGTH_SHORT).show();
455                }
456                break;
457            default: {
458                Log.wtf(TAG, "Unexpected onClick event from " + view);
459                break;
460            }
461        }
462    }
463
464    @Override
465    public boolean onOptionsItemSelected(MenuItem item) {
466        switch (item.getItemId()) {
467            case R.id.menu_history:
468                showCallHistory();
469                break;
470            case R.id.menu_add_contact:
471                try {
472                    startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
473                } catch (ActivityNotFoundException e) {
474                    Toast toast = Toast.makeText(this,
475                            R.string.add_contact_not_available,
476                            Toast.LENGTH_SHORT);
477                    toast.show();
478                }
479                break;
480            case R.id.menu_import_export:
481                // We hard-code the "contactsAreAvailable" argument because doing it properly would
482                // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
483                // now in Dialtacts for (potential) performance reasons. Compare with how it is
484                // done in {@link PeopleActivity}.
485                ImportExportDialogFragment.show(getFragmentManager(), true,
486                        DialtactsActivity.class);
487                return true;
488            case R.id.menu_clear_frequents:
489                // TODO: This should be enabled/disabled based on
490                // SpeedDialFragment.hasFrequents
491                ClearFrequentsDialog.show(getFragmentManager());
492                return true;
493            case R.id.menu_call_settings:
494                handleMenuSettings();
495                return true;
496        }
497        return false;
498    }
499
500    @Override
501    public boolean onLongClick(View view) {
502        switch (view.getId()) {
503            case R.id.dial_button: {
504                // Dial button was pressed; tell the Dialpad fragment
505                mDialpadFragment.dialButtonPressed();
506                return true;  // Consume the event
507            }
508            default: {
509                Log.wtf(TAG, "Unexpected onClick event from " + view);
510                break;
511            }
512        }
513        return false;
514    }
515
516    @Override
517    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
518        if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
519            if (resultCode == RESULT_OK) {
520                final ArrayList<String> matches = data.getStringArrayListExtra(
521                        RecognizerIntent.EXTRA_RESULTS);
522                if (matches.size() > 0) {
523                    final String match = matches.get(0);
524                    mSearchView.setText(match);
525                } else {
526                    Log.e(TAG, "Voice search - nothing heard");
527                }
528            } else {
529                Log.e(TAG, "Voice search failed");
530            }
531        }
532        super.onActivityResult(requestCode, resultCode, data);
533    }
534
535    private void showDialpadFragment(boolean animate) {
536        mDialpadFragment.setAnimate(animate);
537
538        final FragmentTransaction ft = getFragmentManager().beginTransaction();
539        if (animate) {
540            ft.setCustomAnimations(R.anim.slide_in, 0);
541        } else {
542            mDialpadFragment.setYFraction(0);
543        }
544        ft.show(mDialpadFragment);
545        ft.commit();
546    }
547
548    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
549        if (mDialpadFragment == null) return;
550        if (clearDialpad) {
551            mDialpadFragment.clearDialpad();
552        }
553        if (!mDialpadFragment.isVisible()) return;
554        mDialpadFragment.setAnimate(animate);
555        final FragmentTransaction ft = getFragmentManager().beginTransaction();
556        if (animate) {
557            ft.setCustomAnimations(0, R.anim.slide_out);
558        }
559        ft.hide(mDialpadFragment);
560        ft.commit();
561    }
562
563    private boolean getInSearchUi() {
564        return mInDialpadSearch || mInRegularSearch;
565    }
566
567    private void setNotInSearchUi() {
568        mInDialpadSearch = false;
569        mInRegularSearch = false;
570    }
571
572    private void hideDialpadAndSearchUi() {
573        mSearchView.setText(null);
574        hideDialpadFragment(false, true);
575    }
576
577    /**
578     * Callback from child DialpadFragment when the dialpad is shown.
579     */
580    public void onDialpadShown() {
581        mDialButton.setVisibility(View.VISIBLE);
582        mDialpadButton.setVisibility(View.GONE);
583
584        SearchFragment fragment = null;
585        if (mInDialpadSearch) {
586            fragment = mSmartDialSearchFragment;
587        } else if (mInRegularSearch) {
588            fragment = mRegularSearchFragment;
589        }
590        if (fragment != null && fragment.isVisible()) {
591            fragment.getView().animate().translationY(-mActionBarHeight)
592                    .setInterpolator(hideActionBarInterpolator).setDuration(ANIMATION_DURATION);
593        }
594
595        if (mListsFragment != null && mListsFragment.isResumed() && mListsFragment.isVisible()) {
596            // If the favorites fragment is showing, fade to blank.
597            mFragmentsFrame.animate().alpha(0.0f);
598            parentLayout.setBackgroundColor(mContactListBackgroundColor);
599        }
600        getActionBar().hide();
601        alignFloatingActionButtonMiddle();
602    }
603
604    /**
605     * Callback from child DialpadFragment when the dialpad is hidden.
606     */
607    public void onDialpadHidden() {
608        mDialButton.setVisibility(View.GONE);
609        mDialpadButton.setVisibility(View.VISIBLE);
610
611        SearchFragment fragment = null;
612        if (mInDialpadSearch) {
613            fragment = mSmartDialSearchFragment;
614        } else if (mInRegularSearch) {
615            fragment = mRegularSearchFragment;
616        }
617        if (fragment != null && fragment.isVisible()) {
618            fragment.getView().animate().translationY(0)
619                    .setInterpolator(showActionBarInterpolator).setDuration(ANIMATION_DURATION);
620        }
621
622        if (mListsFragment != null && mListsFragment.isVisible()) {
623            mFragmentsFrame.animate().alpha(1.0f);
624            parentLayout.setBackgroundColor(mDialerBackgroundColor);
625
626        }
627        getActionBar().show();
628        alignFloatingActionButtonByTab(mCurrentTabPosition);
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
763        SearchFragment fragment;
764        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
765            transaction.remove(mSmartDialSearchFragment);
766        } else if (mInRegularSearch && mRegularSearchFragment != null) {
767            transaction.remove(mRegularSearchFragment);
768        }
769
770        final String tag;
771        if (smartDialSearch) {
772            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
773        } else {
774            tag = TAG_REGULAR_SEARCH_FRAGMENT;
775        }
776        mInDialpadSearch = smartDialSearch;
777        mInRegularSearch = !smartDialSearch;
778
779        fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
780        if (fragment == null) {
781            if (smartDialSearch) {
782                fragment = new SmartDialSearchFragment();
783            } else {
784                fragment = new RegularSearchFragment();
785            }
786        }
787        // DialtactsActivity will provide the options menu
788        fragment.setHasOptionsMenu(false);
789        transaction.replace(R.id.dialtacts_frame, fragment, tag);
790        transaction.addToBackStack(null);
791        fragment.setQueryString(query, false);
792        transaction.commit();
793    }
794
795    /**
796     * Hides the search fragment
797     */
798    private void exitSearchUi() {
799        // See related bug in enterSearchUI();
800        if (getFragmentManager().isDestroyed()) {
801            return;
802        }
803        // Go all the way back to the favorites fragment, regardless of how many times we
804        // transitioned between search fragments
805        getFragmentManager().popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
806        setNotInSearchUi();
807
808        if (isDialpadShowing()) {
809            mFragmentsFrame.setAlpha(0);
810        }
811    }
812
813    /** Returns an Intent to launch Call Settings screen */
814    public static Intent getCallSettingsIntent() {
815        final Intent intent = new Intent(Intent.ACTION_MAIN);
816        intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
817        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
818        return intent;
819    }
820
821    @Override
822    public void onBackPressed() {
823        if (mDialpadFragment != null && mDialpadFragment.isVisible()) {
824            hideDialpadFragment(true, false);
825        } else if (getInSearchUi()) {
826            mSearchView.setText(null);
827            mDialpadFragment.clearDialpad();
828        } else {
829            super.onBackPressed();
830        }
831    }
832
833    @Override
834    public void onDialpadQueryChanged(String query) {
835        if (mSmartDialSearchFragment != null) {
836            mSmartDialSearchFragment.setAddToContactNumber(query);
837        }
838        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
839                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
840
841        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
842            if (DEBUG) {
843                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
844            }
845            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
846                // This callback can happen if the dialpad fragment is recreated because of
847                // activity destruction. In that case, don't update the search view because
848                // that would bring the user back to the search fragment regardless of the
849                // previous state of the application. Instead, just return here and let the
850                // fragment manager correctly figure out whatever fragment was last displayed.
851                if (!TextUtils.isEmpty(normalizedQuery)) {
852                    mPendingSearchViewQuery = normalizedQuery;
853                }
854                return;
855            }
856            mSearchView.setText(normalizedQuery);
857        }
858    }
859
860    @Override
861    public void onListFragmentScrollStateChange(int scrollState) {
862        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
863            hideDialpadFragment(true, false);
864            hideInputMethod(getCurrentFocus());
865        }
866    }
867
868    @Override
869    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
870            int totalItemCount) {
871        // TODO: No-op for now. This should eventually show/hide the actionBar based on
872        // interactions with the ListsFragments.
873    }
874
875    @Override
876    public void setDialButtonEnabled(boolean enabled) {
877        if (mDialButton != null) {
878            mDialButton.setEnabled(enabled);
879        }
880    }
881
882    @Override
883    public void setDialButtonContainerVisible(boolean visible) {
884        mFloatingActionButton.setVisibility(visible ? View.VISIBLE : View.GONE);
885    }
886
887    private boolean phoneIsInUse() {
888        final TelephonyManager tm = (TelephonyManager) getSystemService(
889                Context.TELEPHONY_SERVICE);
890        return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
891    }
892
893    public static Intent getAddNumberToContactIntent(CharSequence text) {
894        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
895        intent.putExtra(Intents.Insert.PHONE, text);
896        intent.setType(Contacts.CONTENT_ITEM_TYPE);
897        return intent;
898    }
899
900    private boolean canIntentBeHandled(Intent intent) {
901        final PackageManager packageManager = getPackageManager();
902        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
903                PackageManager.MATCH_DEFAULT_ONLY);
904        return resolveInfo != null && resolveInfo.size() > 0;
905    }
906
907    @Override
908    public void showCallHistory() {
909        // Use explicit CallLogActivity intent instead of ACTION_VIEW +
910        // CONTENT_TYPE, so that we always open our call log from our dialer
911        final Intent intent = new Intent(this, CallLogActivity.class);
912        startActivity(intent);
913    }
914
915    /**
916     * Called when the user has long-pressed a contact tile to start a drag operation.
917     */
918    @Override
919    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
920        getActionBar().hide();
921        mRemoveViewContainer.setVisibility(View.VISIBLE);
922    }
923
924    @Override
925    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
926    }
927
928    /**
929     * Called when the user has released a contact tile after long-pressing it.
930     */
931    @Override
932    public void onDragFinished(int x, int y) {
933        getActionBar().show();
934        mRemoveViewContainer.setVisibility(View.GONE);
935    }
936
937    @Override
938    public void onDroppedOnRemove() {}
939
940    /**
941     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
942     * once it has been attached to the activity.
943     */
944    @Override
945    public void setDragDropController(DragDropController dragController) {
946        mDragDropController = dragController;
947        ((RemoveView) findViewById(R.id.remove_view))
948                .setDragDropController(dragController);
949    }
950
951    @Override
952    public void onPickPhoneNumberAction(Uri dataUri) {
953        // Specify call-origin so that users will see the previous tab instead of
954        // CallLog screen (search UI will be automatically exited).
955        PhoneNumberInteraction.startInteractionForPhoneCall(
956            DialtactsActivity.this, dataUri, getCallOrigin());
957        mClearSearchOnPause = true;
958    }
959
960    @Override
961    public void onCallNumberDirectly(String phoneNumber) {
962        Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
963        startActivity(intent);
964        mClearSearchOnPause = true;
965    }
966
967    @Override
968    public void onShortcutIntentCreated(Intent intent) {
969        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
970    }
971
972    @Override
973    public void onHomeInActionBarSelected() {
974        exitSearchUi();
975    }
976
977    public int getActionBarHeight() {
978        return mActionBarHeight;
979    }
980
981    @Override
982    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
983
984    }
985
986    @Override
987    public void onPageSelected(int position) {
988        mCurrentTabPosition = position;
989        // If the dialpad is showing, the floating action button should always be middle aligned.
990        if (!isDialpadShowing()) {
991            alignFloatingActionButtonByTab(mCurrentTabPosition);
992        }
993    }
994
995    @Override
996    public void onPageScrollStateChanged(int state) {
997    }
998
999    private void alignFloatingActionButtonByTab(int position) {
1000        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
1001            alignFloatingActionButtonMiddle();
1002        } else {
1003            alignFloatingActionButtonRight();
1004        }
1005    }
1006
1007    private void alignFloatingActionButtonRight() {
1008        final RelativeLayout.LayoutParams params =
1009                (RelativeLayout.LayoutParams) mFloatingActionButton.getLayoutParams();
1010        params.removeRule(RelativeLayout.CENTER_HORIZONTAL);
1011        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1012        mFloatingActionButton.setLayoutParams(params);
1013    }
1014
1015    private void alignFloatingActionButtonMiddle() {
1016        final RelativeLayout.LayoutParams params =
1017                (RelativeLayout.LayoutParams) mFloatingActionButton.getLayoutParams();
1018        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1019        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
1020        mFloatingActionButton.setLayoutParams(params);
1021    }
1022}
1023