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