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