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