• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.app.Activity;
23 import android.app.Fragment;
24 import android.app.FragmentManager;
25 import android.app.FragmentTransaction;
26 import android.content.ActivityNotFoundException;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.RemoteException;
34 import android.os.ServiceManager;
35 import android.provider.CallLog.Calls;
36 import android.provider.ContactsContract.Contacts;
37 import android.provider.ContactsContract.Intents;
38 import android.provider.ContactsContract.Intents.UI;
39 import android.speech.RecognizerIntent;
40 import android.telephony.TelephonyManager;
41 import android.text.Editable;
42 import android.text.TextUtils;
43 import android.text.TextWatcher;
44 import android.util.Log;
45 import android.view.Menu;
46 import android.view.MenuItem;
47 import android.view.View;
48 import android.view.ViewGroup.LayoutParams;
49 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
50 import android.view.inputmethod.InputMethodManager;
51 import android.widget.AbsListView.OnScrollListener;
52 import android.widget.EditText;
53 import android.widget.LinearLayout;
54 import android.widget.PopupMenu;
55 import android.widget.Toast;
56 
57 import com.android.contacts.common.CallUtil;
58 import com.android.contacts.common.activity.TransactionSafeActivity;
59 import com.android.contacts.common.dialog.ClearFrequentsDialog;
60 import com.android.contacts.common.interactions.ImportExportDialogFragment;
61 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
62 import com.android.dialer.calllog.CallLogActivity;
63 import com.android.dialer.database.DialerDatabaseHelper;
64 import com.android.dialer.dialpad.DialpadFragment;
65 import com.android.dialer.dialpad.SmartDialNameMatcher;
66 import com.android.dialer.dialpad.SmartDialPrefix;
67 import com.android.dialer.interactions.PhoneNumberInteraction;
68 import com.android.dialer.list.AllContactsActivity;
69 import com.android.dialer.list.DragDropController;
70 import com.android.dialer.list.OnDragDropListener;
71 import com.android.dialer.list.OnListFragmentScrolledListener;
72 import com.android.dialer.list.PhoneFavoriteFragment;
73 import com.android.dialer.list.PhoneFavoriteTileView;
74 import com.android.dialer.list.RegularSearchFragment;
75 import com.android.dialer.list.RemoveView;
76 import com.android.dialer.list.SearchFragment;
77 import com.android.dialer.list.SmartDialSearchFragment;
78 import com.android.dialerbind.DatabaseHelperManager;
79 import com.android.internal.telephony.ITelephony;
80 
81 import java.util.ArrayList;
82 import java.util.List;
83 
84 /**
85  * The dialer tab's title is 'phone', a more common name (see strings.xml).
86  */
87 public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
88         DialpadFragment.OnDialpadQueryChangedListener, PopupMenu.OnMenuItemClickListener,
89         OnListFragmentScrolledListener,
90         DialpadFragment.HostInterface,
91         PhoneFavoriteFragment.OnShowAllContactsListener,
92         PhoneFavoriteFragment.HostInterface,
93         OnDragDropListener, View.OnLongClickListener {
94     private static final String TAG = "DialtactsActivity";
95 
96     public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
97 
98     public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
99 
100     /** Used to open Call Setting */
101     private static final String PHONE_PACKAGE = "com.android.phone";
102     private static final String CALL_SETTINGS_CLASS_NAME =
103             "com.android.phone.CallFeaturesSetting";
104     /** @see #getCallOrigin() */
105     private static final String CALL_ORIGIN_DIALTACTS =
106             "com.android.dialer.DialtactsActivity";
107 
108     private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
109     private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
110     private static final String KEY_SEARCH_QUERY = "search_query";
111     private static final String KEY_FIRST_LAUNCH = "first_launch";
112 
113     private static final String TAG_DIALPAD_FRAGMENT = "dialpad";
114     private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
115     private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
116     private static final String TAG_FAVORITES_FRAGMENT = "favorites";
117 
118     /**
119      * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
120      */
121     private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
122 
123     private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
124 
125     private static final int ANIMATION_DURATION = 200;
126 
127     private String mFilterText;
128 
129     /**
130      * The main fragment displaying the user's favorites and frequent contacts
131      */
132     private PhoneFavoriteFragment mPhoneFavoriteFragment;
133 
134     /**
135      * Fragment containing the dialpad that slides into view
136      */
137     private DialpadFragment mDialpadFragment;
138 
139     /**
140      * Fragment for searching phone numbers using the alphanumeric keyboard.
141      */
142     private RegularSearchFragment mRegularSearchFragment;
143 
144     /**
145      * Fragment for searching phone numbers using the dialpad.
146      */
147     private SmartDialSearchFragment mSmartDialSearchFragment;
148 
149     private View mMenuButton;
150     private View mFakeActionBar;
151     private View mCallHistoryButton;
152     private View mDialpadButton;
153     private View mDialButton;
154     private PopupMenu mOverflowMenu;
155     private PopupMenu mDialpadOverflowMenu;
156 
157     // Padding view used to shift the fragment frame up when the dialpad is shown so that
158     // the contents of the fragment frame continue to exist in a layout of the same height
159     private View mFragmentsSpacer;
160     private View mFragmentsFrame;
161 
162     private boolean mInDialpadSearch;
163     private boolean mInRegularSearch;
164     private boolean mClearSearchOnPause;
165 
166     /**
167      * True if the dialpad is only temporarily showing due to being in call
168      */
169     private boolean mInCallDialpadUp;
170 
171     /**
172      * True when this activity has been launched for the first time.
173      */
174     private boolean mFirstLaunch;
175     private View mSearchViewContainer;
176     private RemoveView mRemoveViewContainer;
177     // This view points to the Framelayout that houses both the search view and remove view
178     // containers.
179     private View mSearchAndRemoveViewContainer;
180     private View mSearchViewCloseButton;
181     private View mVoiceSearchButton;
182     private EditText mSearchView;
183 
184     private String mSearchQuery;
185 
186     private DialerDatabaseHelper mDialerDatabaseHelper;
187 
188     private class OverflowPopupMenu extends PopupMenu {
OverflowPopupMenu(Context context, View anchor)189         public OverflowPopupMenu(Context context, View anchor) {
190             super(context, anchor);
191         }
192 
193         @Override
show()194         public void show() {
195             final Menu menu = getMenu();
196             final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
197             clearFrequents.setVisible(mPhoneFavoriteFragment.hasFrequents());
198             super.show();
199         }
200     }
201 
202     /**
203      * Listener used when one of phone numbers in search UI is selected. This will initiate a
204      * phone call using the phone number.
205      */
206     private final OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener =
207             new OnPhoneNumberPickerActionListener() {
208                 @Override
209                 public void onPickPhoneNumberAction(Uri dataUri) {
210                     // Specify call-origin so that users will see the previous tab instead of
211                     // CallLog screen (search UI will be automatically exited).
212                     PhoneNumberInteraction.startInteractionForPhoneCall(
213                         DialtactsActivity.this, dataUri, getCallOrigin());
214                     mClearSearchOnPause = true;
215                 }
216 
217                 @Override
218                 public void onCallNumberDirectly(String phoneNumber) {
219                     Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
220                     startActivity(intent);
221                     mClearSearchOnPause = true;
222                 }
223 
224                 @Override
225                 public void onShortcutIntentCreated(Intent intent) {
226                     Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
227                 }
228 
229                 @Override
230                 public void onHomeInActionBarSelected() {
231                     exitSearchUi();
232                 }
233     };
234 
235     /**
236      * Listener used to send search queries to the phone search fragment.
237      */
238     private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
239             @Override
240             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
241             }
242 
243             @Override
244             public void onTextChanged(CharSequence s, int start, int before, int count) {
245                 final String newText = s.toString();
246                 if (newText.equals(mSearchQuery)) {
247                     // If the query hasn't changed (perhaps due to activity being destroyed
248                     // and restored, or user launching the same DIAL intent twice), then there is
249                     // no need to do anything here.
250                     return;
251                 }
252                 mSearchQuery = newText;
253                 if (DEBUG) {
254                     Log.d(TAG, "onTextChange for mSearchView called with new query: " + s);
255                 }
256                 final boolean dialpadSearch = isDialpadShowing();
257 
258                 // Show search result with non-empty text. Show a bare list otherwise.
259                 if (TextUtils.isEmpty(newText) && getInSearchUi()) {
260                     exitSearchUi();
261                     mSearchViewCloseButton.setVisibility(View.GONE);
262                     mVoiceSearchButton.setVisibility(View.VISIBLE);
263                     return;
264                 } else if (!TextUtils.isEmpty(newText)) {
265                     final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) ||
266                             (!dialpadSearch && mInRegularSearch);
267                     if (!sameSearchMode) {
268                         // call enterSearchUi only if we are switching search modes, or entering
269                         // search ui for the first time
270                         enterSearchUi(dialpadSearch, newText);
271                     }
272 
273                     if (dialpadSearch && mSmartDialSearchFragment != null) {
274                             mSmartDialSearchFragment.setQueryString(newText, false);
275                     } else if (mRegularSearchFragment != null) {
276                         mRegularSearchFragment.setQueryString(newText, false);
277                     }
278                     mSearchViewCloseButton.setVisibility(View.VISIBLE);
279                     mVoiceSearchButton.setVisibility(View.GONE);
280                     return;
281                 }
282             }
283 
284             @Override
285             public void afterTextChanged(Editable s) {
286             }
287     };
288 
isDialpadShowing()289     private boolean isDialpadShowing() {
290         return mDialpadFragment != null && mDialpadFragment.isVisible();
291     }
292 
293     @Override
onCreate(Bundle savedInstanceState)294     protected void onCreate(Bundle savedInstanceState) {
295         super.onCreate(savedInstanceState);
296         mFirstLaunch = true;
297 
298         final Intent intent = getIntent();
299         fixIntent(intent);
300 
301         setContentView(R.layout.dialtacts_activity);
302 
303         getActionBar().hide();
304 
305         // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState
306         // is null. Otherwise the fragment manager takes care of recreating these fragments.
307         if (savedInstanceState == null) {
308             getFragmentManager().beginTransaction()
309                     .add(R.id.dialtacts_frame, new PhoneFavoriteFragment(), TAG_FAVORITES_FRAGMENT)
310                     .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT)
311                     .commit();
312         } else {
313             mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
314             mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
315             mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
316             mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
317         }
318 
319         mFragmentsFrame = findViewById(R.id.dialtacts_frame);
320         mFragmentsSpacer = findViewById(R.id.contact_tile_frame_spacer);
321 
322         mRemoveViewContainer = (RemoveView) findViewById(R.id.remove_view_container);
323         mSearchAndRemoveViewContainer = findViewById(R.id.search_and_remove_view_container);
324 
325         // When the first global layout pass is completed (and mSearchAndRemoveViewContainer has
326         // been assigned a valid height), assign that height to mFragmentsSpacer as well.
327         mSearchAndRemoveViewContainer.getViewTreeObserver().addOnGlobalLayoutListener(
328                 new OnGlobalLayoutListener() {
329                     @Override
330                     public void onGlobalLayout() {
331                         mSearchAndRemoveViewContainer.getViewTreeObserver()
332                                 .removeOnGlobalLayoutListener(this);
333                         mFragmentsSpacer.setLayoutParams(
334                                 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
335                                         mSearchAndRemoveViewContainer.getHeight()));
336                     }
337                 });
338 
339         setupFakeActionBarItems();
340         prepareSearchView();
341 
342         if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction())
343                 && savedInstanceState == null) {
344             setupFilterText(intent);
345         }
346 
347         hideDialpadFragment(false, false);
348 
349         mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
350         SmartDialPrefix.initializeNanpSettings(this);
351     }
352 
353     @Override
onResume()354     protected void onResume() {
355         super.onResume();
356         if (mFirstLaunch) {
357             displayFragment(getIntent());
358         } else if (!phoneIsInUse() && mInCallDialpadUp) {
359             hideDialpadFragment(false, true);
360             mInCallDialpadUp = false;
361         }
362         prepareVoiceSearchButton();
363         mFirstLaunch = false;
364         mDialerDatabaseHelper.startSmartDialUpdateThread();
365     }
366 
367     @Override
onPause()368     protected void onPause() {
369         if (mClearSearchOnPause) {
370             hideDialpadAndSearchUi();
371             mClearSearchOnPause = false;
372         }
373         super.onPause();
374     }
375 
376     @Override
onSaveInstanceState(Bundle outState)377     protected void onSaveInstanceState(Bundle outState) {
378         super.onSaveInstanceState(outState);
379         outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
380         outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
381         outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
382         outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
383     }
384 
385     @Override
onAttachFragment(Fragment fragment)386     public void onAttachFragment(Fragment fragment) {
387         if (fragment instanceof DialpadFragment) {
388             mDialpadFragment = (DialpadFragment) fragment;
389             final FragmentTransaction transaction = getFragmentManager().beginTransaction();
390             transaction.hide(mDialpadFragment);
391             transaction.commit();
392         } else if (fragment instanceof SmartDialSearchFragment) {
393             mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
394             mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(
395                     mPhoneNumberPickerActionListener);
396             if (mFragmentsFrame != null) {
397                 mFragmentsFrame.setAlpha(1.0f);
398             }
399         } else if (fragment instanceof SearchFragment) {
400             mRegularSearchFragment = (RegularSearchFragment) fragment;
401             mRegularSearchFragment.setOnPhoneNumberPickerActionListener(
402                     mPhoneNumberPickerActionListener);
403             if (mFragmentsFrame != null) {
404                 mFragmentsFrame.setAlpha(1.0f);
405             }
406         } else if (fragment instanceof PhoneFavoriteFragment) {
407             mPhoneFavoriteFragment = (PhoneFavoriteFragment) fragment;
408             mPhoneFavoriteFragment.setListener(mPhoneFavoriteListener);
409         }
410     }
411 
412     @Override
onMenuItemClick(MenuItem item)413     public boolean onMenuItemClick(MenuItem item) {
414         switch (item.getItemId()) {
415             case R.id.menu_import_export:
416                 // We hard-code the "contactsAreAvailable" argument because doing it properly would
417                 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
418                 // now in Dialtacts for (potential) performance reasons. Compare with how it is
419                 // done in {@link PeopleActivity}.
420                 ImportExportDialogFragment.show(getFragmentManager(), true,
421                         DialtactsActivity.class);
422                 return true;
423             case R.id.menu_clear_frequents:
424                 ClearFrequentsDialog.show(getFragmentManager());
425                 return true;
426             case R.id.menu_add_contact:
427                 try {
428                     startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
429                 } catch (ActivityNotFoundException e) {
430                     Toast toast = Toast.makeText(this,
431                             R.string.add_contact_not_available,
432                             Toast.LENGTH_SHORT);
433                     toast.show();
434                 }
435                 return true;
436             case R.id.menu_call_settings:
437                 handleMenuSettings();
438                 return true;
439             case R.id.menu_all_contacts:
440                 onShowAllContacts();
441                 return true;
442         }
443         return false;
444     }
445 
handleMenuSettings()446     protected void handleMenuSettings() {
447         openTelephonySetting(this);
448     }
449 
openTelephonySetting(Activity activity)450     public static void openTelephonySetting(Activity activity) {
451         final Intent settingsIntent = getCallSettingsIntent();
452         activity.startActivity(settingsIntent);
453     }
454 
455     @Override
onClick(View view)456     public void onClick(View view) {
457         switch (view.getId()) {
458             case R.id.overflow_menu: {
459                 if (isDialpadShowing()) {
460                     mDialpadOverflowMenu.show();
461                 } else {
462                     mOverflowMenu.show();
463                 }
464                 break;
465             }
466             case R.id.dialpad_button:
467                 // Reset the boolean flag that tracks whether the dialpad was up because
468                 // we were in call. Regardless of whether it was true before, we want to
469                 // show the dialpad because the user has explicitly clicked the dialpad
470                 // button.
471                 mInCallDialpadUp = false;
472                 showDialpadFragment(true);
473                 break;
474             case R.id.dial_button:
475                 // Dial button was pressed; tell the Dialpad fragment
476                 mDialpadFragment.dialButtonPressed();
477                 break;
478             case R.id.call_history_button:
479                 // Use explicit CallLogActivity intent instead of ACTION_VIEW +
480                 // CONTENT_TYPE, so that we always open our call log from our dialer
481                 final Intent intent = new Intent(this, CallLogActivity.class);
482                 startActivity(intent);
483                 break;
484             case R.id.search_close_button:
485                 // Clear the search field
486                 if (!TextUtils.isEmpty(mSearchView.getText())) {
487                     mDialpadFragment.clearDialpad();
488                     mSearchView.setText("");
489                 }
490                 break;
491             case R.id.voice_search_button:
492                 try {
493                     startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
494                             ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
495                 } catch (ActivityNotFoundException e) {
496                     Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
497                             Toast.LENGTH_SHORT).show();
498                 }
499                 break;
500             default: {
501                 Log.wtf(TAG, "Unexpected onClick event from " + view);
502                 break;
503             }
504         }
505     }
506 
507     @Override
onLongClick(View view)508     public boolean onLongClick(View view) {
509         switch (view.getId()) {
510             case R.id.dial_button: {
511                 // Dial button was pressed; tell the Dialpad fragment
512                 mDialpadFragment.dialButtonPressed();
513                 return true;  // Consume the event
514             }
515             default: {
516                 Log.wtf(TAG, "Unexpected onClick event from " + view);
517                 break;
518             }
519         }
520         return false;
521     }
522 
523     @Override
onActivityResult(int requestCode, int resultCode, Intent data)524     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
525         if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
526             if (resultCode == RESULT_OK) {
527                 final ArrayList<String> matches = data.getStringArrayListExtra(
528                         RecognizerIntent.EXTRA_RESULTS);
529                 if (matches.size() > 0) {
530                     final String match = matches.get(0);
531                     mSearchView.setText(match);
532                 } else {
533                     Log.e(TAG, "Voice search - nothing heard");
534                 }
535             } else {
536                 Log.e(TAG, "Voice search failed");
537             }
538         }
539         super.onActivityResult(requestCode, resultCode, data);
540     }
541 
showDialpadFragment(boolean animate)542     private void showDialpadFragment(boolean animate) {
543         mDialpadFragment.setAdjustTranslationForAnimation(animate);
544         final FragmentTransaction ft = getFragmentManager().beginTransaction();
545         if (animate) {
546             ft.setCustomAnimations(R.anim.slide_in, 0);
547         } else {
548             mDialpadFragment.setYFraction(0);
549         }
550         ft.show(mDialpadFragment);
551         ft.commit();
552         mDialButton.setVisibility(shouldShowOnscreenDialButton() ? View.VISIBLE : View.GONE);
553         mDialpadButton.setVisibility(View.GONE);
554 
555         if (mDialpadOverflowMenu == null) {
556             mDialpadOverflowMenu = mDialpadFragment.buildOptionsMenu(mMenuButton);
557         }
558 
559         mMenuButton.setOnTouchListener(mDialpadOverflowMenu.getDragToOpenListener());
560     }
561 
hideDialpadFragment(boolean animate, boolean clearDialpad)562     public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
563         if (mDialpadFragment == null) return;
564         if (clearDialpad) {
565             mDialpadFragment.clearDialpad();
566         }
567         if (!mDialpadFragment.isVisible()) return;
568         mDialpadFragment.setAdjustTranslationForAnimation(animate);
569         final FragmentTransaction ft = getFragmentManager().beginTransaction();
570         if (animate) {
571             ft.setCustomAnimations(0, R.anim.slide_out);
572         }
573         ft.hide(mDialpadFragment);
574         ft.commit();
575         mDialButton.setVisibility(View.GONE);
576         mDialpadButton.setVisibility(View.VISIBLE);
577         mMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener());
578     }
579 
prepareSearchView()580     private void prepareSearchView() {
581         mSearchViewContainer = findViewById(R.id.search_view_container);
582         mSearchViewCloseButton = findViewById(R.id.search_close_button);
583         mSearchViewCloseButton.setOnClickListener(this);
584 
585         mSearchView = (EditText) findViewById(R.id.search_view);
586         mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
587         mSearchView.setHint(getString(R.string.dialer_hint_find_contact));
588 
589         prepareVoiceSearchButton();
590     }
591 
prepareVoiceSearchButton()592     private void prepareVoiceSearchButton() {
593         mVoiceSearchButton = findViewById(R.id.voice_search_button);
594         final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
595         if (canIntentBeHandled(voiceIntent)) {
596             mVoiceSearchButton.setVisibility(View.VISIBLE);
597             mVoiceSearchButton.setOnClickListener(this);
598         } else {
599             mVoiceSearchButton.setVisibility(View.GONE);
600         }
601     }
602 
603     final AnimatorListener mHideListener = new AnimatorListenerAdapter() {
604         @Override
605         public void onAnimationEnd(Animator animation) {
606             mSearchAndRemoveViewContainer.setVisibility(View.GONE);
607         }
608     };
609 
getInSearchUi()610     private boolean getInSearchUi() {
611         return mInDialpadSearch || mInRegularSearch;
612     }
613 
setNotInSearchUi()614     private void setNotInSearchUi() {
615         mInDialpadSearch = false;
616         mInRegularSearch = false;
617     }
618 
hideDialpadAndSearchUi()619     private void hideDialpadAndSearchUi() {
620         mSearchView.setText(null);
621         hideDialpadFragment(false, true);
622     }
623 
hideSearchBar()624     public void hideSearchBar() {
625         final int height = mSearchAndRemoveViewContainer.getHeight();
626 
627         mSearchAndRemoveViewContainer.animate().cancel();
628         mSearchAndRemoveViewContainer.setAlpha(1);
629         mSearchAndRemoveViewContainer.setTranslationY(0);
630         mSearchAndRemoveViewContainer.animate().withLayer().alpha(0)
631                 .translationY(-height).setDuration(ANIMATION_DURATION)
632                 .setListener(mHideListener);
633 
634         mFragmentsFrame.animate().withLayer()
635                 .translationY(-height).setDuration(ANIMATION_DURATION).setListener(
636                 new AnimatorListenerAdapter() {
637                     @Override
638                     public void onAnimationEnd(Animator animation) {
639                         mFragmentsFrame.setTranslationY(0);
640                         // Display the fragments spacer (which has the same height as the
641                         // search box) now that the search box is hidden, so that
642                         // mFragmentsFrame always retains the same height
643                         mFragmentsSpacer.setVisibility(View.VISIBLE);
644                     }
645                 });
646 
647         if (!mInDialpadSearch && !mInRegularSearch) {
648             // If the favorites fragment is showing, fade to blank.
649             mFragmentsFrame.animate().alpha(0.0f);
650         }
651     }
652 
showSearchBar()653     public void showSearchBar() {
654         final int height = mSearchAndRemoveViewContainer.getHeight();
655         mSearchAndRemoveViewContainer.animate().cancel();
656         mSearchAndRemoveViewContainer.setAlpha(0);
657         mSearchAndRemoveViewContainer.setTranslationY(-height);
658         mSearchAndRemoveViewContainer.animate().withLayer().alpha(1).translationY(0)
659                 .setDuration(ANIMATION_DURATION).setListener(new AnimatorListenerAdapter() {
660                         @Override
661                         public void onAnimationStart(Animator animation) {
662                             mSearchAndRemoveViewContainer.setVisibility(View.VISIBLE);
663                         }
664                     });
665 
666         mFragmentsFrame.setTranslationY(-height);
667         mFragmentsFrame.animate().withLayer().translationY(0).setDuration(ANIMATION_DURATION)
668                 .alpha(1.0f)
669                 .setListener(
670                         new AnimatorListenerAdapter() {
671                             @Override
672                             public void onAnimationStart(Animator animation) {
673                                 // Hide the fragment spacer now that the search box will
674                                 // be displayed again
675                                 mFragmentsSpacer.setVisibility(View.GONE);
676                             }
677                         });
678     }
679 
setupFakeActionBarItems()680     private void setupFakeActionBarItems() {
681         mMenuButton = findViewById(R.id.overflow_menu);
682         if (mMenuButton != null) {
683             mMenuButton.setOnClickListener(this);
684             if (mOverflowMenu == null) {
685                 mOverflowMenu = buildOptionsMenu(mMenuButton);
686             }
687             mMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener());
688         }
689 
690         mFakeActionBar = findViewById(R.id.fake_action_bar);
691 
692         mCallHistoryButton = findViewById(R.id.call_history_button);
693         mCallHistoryButton.setOnClickListener(this);
694 
695         mDialButton = findViewById(R.id.dial_button);
696         mDialButton.setOnClickListener(this);
697         mDialButton.setOnLongClickListener(this);
698 
699         mDialpadButton = findViewById(R.id.dialpad_button);
700         mDialpadButton.setOnClickListener(this);
701     }
702 
buildOptionsMenu(View invoker)703     private PopupMenu buildOptionsMenu(View invoker) {
704         PopupMenu menu = new OverflowPopupMenu(this, invoker);
705         menu.inflate(R.menu.dialtacts_options);
706         menu.setOnMenuItemClickListener(this);
707         return menu;
708     }
709 
fixIntent(Intent intent)710     private void fixIntent(Intent intent) {
711         // This should be cleaned up: the call key used to send an Intent
712         // that just said to go to the recent calls list.  It now sends this
713         // abstract action, but this class hasn't been rewritten to deal with it.
714         if (Intent.ACTION_CALL_BUTTON.equals(intent.getAction())) {
715             intent.setDataAndType(Calls.CONTENT_URI, Calls.CONTENT_TYPE);
716             intent.putExtra("call_key", true);
717             setIntent(intent);
718         }
719     }
720 
721     /**
722      * Returns true if the intent is due to hitting the green send key (hardware call button:
723      * KEYCODE_CALL) while in a call.
724      *
725      * @param intent the intent that launched this activity
726      * @param recentCallsRequest true if the intent is requesting to view recent calls
727      * @return true if the intent is due to hitting the green send key while in a call
728      */
isSendKeyWhileInCall(Intent intent, boolean recentCallsRequest)729     private boolean isSendKeyWhileInCall(Intent intent, boolean recentCallsRequest) {
730         // If there is a call in progress go to the call screen
731         if (recentCallsRequest) {
732             final boolean callKey = intent.getBooleanExtra("call_key", false);
733 
734             try {
735                 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
736                 if (callKey && phone != null && phone.showCallScreen()) {
737                     return true;
738                 }
739             } catch (RemoteException e) {
740                 Log.e(TAG, "Failed to handle send while in call", e);
741             }
742         }
743 
744         return false;
745     }
746 
747     /**
748      * Sets the current tab based on the intent's request type
749      *
750      * @param intent Intent that contains information about which tab should be selected
751      */
displayFragment(Intent intent)752     private void displayFragment(Intent intent) {
753         // If we got here by hitting send and we're in call forward along to the in-call activity
754         boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.resolveType(
755             getContentResolver()));
756         if (isSendKeyWhileInCall(intent, recentCallsRequest)) {
757             finish();
758             return;
759         }
760 
761         if (mDialpadFragment != null) {
762             final boolean phoneIsInUse = phoneIsInUse();
763             if (phoneIsInUse || isDialIntent(intent)) {
764                 mDialpadFragment.setStartedFromNewIntent(true);
765                 if (phoneIsInUse && !mDialpadFragment.isVisible()) {
766                     mInCallDialpadUp = true;
767                 }
768                 showDialpadFragment(false);
769             }
770         }
771     }
772 
773     @Override
onNewIntent(Intent newIntent)774     public void onNewIntent(Intent newIntent) {
775         setIntent(newIntent);
776         fixIntent(newIntent);
777         displayFragment(newIntent);
778         final String action = newIntent.getAction();
779 
780         invalidateOptionsMenu();
781     }
782 
783     /** Returns true if the given intent contains a phone number to populate the dialer with */
isDialIntent(Intent intent)784     private boolean isDialIntent(Intent intent) {
785         final String action = intent.getAction();
786         if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
787             return true;
788         }
789         if (Intent.ACTION_VIEW.equals(action)) {
790             final Uri data = intent.getData();
791             if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) {
792                 return true;
793             }
794         }
795         return false;
796     }
797 
798     /**
799      * Returns an appropriate call origin for this Activity. May return null when no call origin
800      * should be used (e.g. when some 3rd party application launched the screen. Call origin is
801      * for remembering the tab in which the user made a phone call, so the external app's DIAL
802      * request should not be counted.)
803      */
getCallOrigin()804     public String getCallOrigin() {
805         return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
806     }
807 
808     /**
809      * Retrieves the filter text stored in {@link #setupFilterText(Intent)}.
810      * This text originally came from a FILTER_CONTACTS_ACTION intent received
811      * by this activity. The stored text will then be cleared after after this
812      * method returns.
813      *
814      * @return The stored filter text
815      */
getAndClearFilterText()816     public String getAndClearFilterText() {
817         String filterText = mFilterText;
818         mFilterText = null;
819         return filterText;
820     }
821 
822     /**
823      * Stores the filter text associated with a FILTER_CONTACTS_ACTION intent.
824      * This is so child activities can check if they are supposed to display a filter.
825      *
826      * @param intent The intent received in {@link #onNewIntent(Intent)}
827      */
setupFilterText(Intent intent)828     private void setupFilterText(Intent intent) {
829         // If the intent was relaunched from history, don't apply the filter text.
830         if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
831             return;
832         }
833         String filter = intent.getStringExtra(UI.FILTER_TEXT_EXTRA_KEY);
834         if (filter != null && filter.length() > 0) {
835             mFilterText = filter;
836         }
837     }
838 
839     private final PhoneFavoriteFragment.Listener mPhoneFavoriteListener =
840             new PhoneFavoriteFragment.Listener() {
841         @Override
842         public void onContactSelected(Uri contactUri) {
843             PhoneNumberInteraction.startInteractionForPhoneCall(
844                         DialtactsActivity.this, contactUri, getCallOrigin());
845         }
846 
847         @Override
848         public void onCallNumberDirectly(String phoneNumber) {
849             Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
850             startActivity(intent);
851         }
852     };
853 
854     /* TODO krelease: This is only relevant for phones that have a hard button search key (i.e.
855      * Nexus S). Supporting it is a little more tricky because of the dialpad fragment might
856      * be showing when the search key is pressed so there is more state management involved.
857 
858     @Override
859     public void startSearch(String initialQuery, boolean selectInitialQuery,
860             Bundle appSearchData, boolean globalSearch) {
861         if (mRegularSearchFragment != null && mRegularSearchFragment.isAdded() && !globalSearch) {
862             if (mInSearchUi) {
863                 if (mSearchView.hasFocus()) {
864                     showInputMethod(mSearchView.findFocus());
865                 } else {
866                     mSearchView.requestFocus();
867                 }
868             } else {
869                 enterSearchUi();
870             }
871         } else {
872             super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
873         }
874     }*/
875 
showInputMethod(View view)876     private void showInputMethod(View view) {
877         final InputMethodManager imm = (InputMethodManager) getSystemService(
878                 Context.INPUT_METHOD_SERVICE);
879         if (imm != null) {
880             imm.showSoftInput(view, 0);
881         }
882     }
883 
hideInputMethod(View view)884     private void hideInputMethod(View view) {
885         final InputMethodManager imm = (InputMethodManager) getSystemService(
886                 Context.INPUT_METHOD_SERVICE);
887         if (imm != null && view != null) {
888             imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
889         }
890     }
891 
892     /**
893      * Shows the search fragment
894      */
enterSearchUi(boolean smartDialSearch, String query)895     private void enterSearchUi(boolean smartDialSearch, String query) {
896         if (getFragmentManager().isDestroyed()) {
897             // Weird race condition where fragment is doing work after the activity is destroyed
898             // due to talkback being on (b/10209937). Just return since we can't do any
899             // constructive here.
900             return;
901         }
902 
903         if (DEBUG) {
904             Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
905         }
906 
907         final FragmentTransaction transaction = getFragmentManager().beginTransaction();
908 
909         SearchFragment fragment;
910         if (mInDialpadSearch) {
911             transaction.remove(mSmartDialSearchFragment);
912         } else if (mInRegularSearch) {
913             transaction.remove(mRegularSearchFragment);
914         } else {
915             transaction.remove(mPhoneFavoriteFragment);
916         }
917 
918         final String tag;
919         if (smartDialSearch) {
920             tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
921         } else {
922             tag = TAG_REGULAR_SEARCH_FRAGMENT;
923         }
924         mInDialpadSearch = smartDialSearch;
925         mInRegularSearch = !smartDialSearch;
926 
927         fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
928         if (fragment == null) {
929             if (smartDialSearch) {
930                 fragment = new SmartDialSearchFragment();
931             } else {
932                 fragment = new RegularSearchFragment();
933             }
934         }
935         transaction.replace(R.id.dialtacts_frame, fragment, tag);
936         transaction.addToBackStack(null);
937         fragment.setQueryString(query, false);
938         transaction.commit();
939     }
940 
941     /**
942      * Hides the search fragment
943      */
exitSearchUi()944     private void exitSearchUi() {
945         // See related bug in enterSearchUI();
946         if (getFragmentManager().isDestroyed()) {
947             return;
948         }
949         // Go all the way back to the favorites fragment, regardless of how many times we
950         // transitioned between search fragments
951         getFragmentManager().popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
952         setNotInSearchUi();
953 
954         if (isDialpadShowing()) {
955             mFragmentsFrame.setAlpha(0);
956         }
957     }
958 
959     /** Returns an Intent to launch Call Settings screen */
getCallSettingsIntent()960     public static Intent getCallSettingsIntent() {
961         final Intent intent = new Intent(Intent.ACTION_MAIN);
962         intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
963         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
964         return intent;
965     }
966 
967     @Override
onBackPressed()968     public void onBackPressed() {
969         if (mDialpadFragment != null && mDialpadFragment.isVisible()) {
970             hideDialpadFragment(true, false);
971         } else if (getInSearchUi()) {
972             mSearchView.setText(null);
973             mDialpadFragment.clearDialpad();
974         } else {
975             super.onBackPressed();
976         }
977     }
978 
979     @Override
onDialpadQueryChanged(String query)980     public void onDialpadQueryChanged(String query) {
981         final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
982                 SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
983         if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
984             if (DEBUG) {
985                 Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
986             }
987             if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
988                 // This callback can happen if the dialpad fragment is recreated because of
989                 // activity destruction. In that case, don't update the search view because
990                 // that would bring the user back to the search fragment regardless of the
991                 // previous state of the application. Instead, just return here and let the
992                 // fragment manager correctly figure out whatever fragment was last displayed.
993                 return;
994             }
995             mSearchView.setText(normalizedQuery);
996         }
997     }
998 
999     @Override
onListFragmentScrollStateChange(int scrollState)1000     public void onListFragmentScrollStateChange(int scrollState) {
1001         if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
1002             hideDialpadFragment(true, false);
1003             hideInputMethod(getCurrentFocus());
1004         }
1005     }
1006 
1007     @Override
setDialButtonEnabled(boolean enabled)1008     public void setDialButtonEnabled(boolean enabled) {
1009         if (mDialButton != null) {
1010             mDialButton.setEnabled(enabled);
1011         }
1012     }
1013 
1014     @Override
setDialButtonContainerVisible(boolean visible)1015     public void setDialButtonContainerVisible(boolean visible) {
1016         mFakeActionBar.setVisibility(visible ? View.VISIBLE : View.GONE);
1017     }
1018 
phoneIsInUse()1019     private boolean phoneIsInUse() {
1020         final TelephonyManager tm = (TelephonyManager) getSystemService(
1021                 Context.TELEPHONY_SERVICE);
1022         return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
1023     }
1024 
1025     @Override
onShowAllContacts()1026     public void onShowAllContacts() {
1027         final Intent intent = new Intent(this, AllContactsActivity.class);
1028         startActivity(intent);
1029     }
1030 
getAddNumberToContactIntent(CharSequence text)1031     public static Intent getAddNumberToContactIntent(CharSequence text) {
1032         final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
1033         intent.putExtra(Intents.Insert.PHONE, text);
1034         intent.setType(Contacts.CONTENT_ITEM_TYPE);
1035         return intent;
1036     }
1037 
canIntentBeHandled(Intent intent)1038     private boolean canIntentBeHandled(Intent intent) {
1039         final PackageManager packageManager = getPackageManager();
1040         final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
1041                 PackageManager.MATCH_DEFAULT_ONLY);
1042         return resolveInfo != null && resolveInfo.size() > 0;
1043     }
1044 
1045     @Override
onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view)1046     public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) {
1047         crossfadeViews(mRemoveViewContainer, mSearchViewContainer, ANIMATION_DURATION);
1048     }
1049 
1050     @Override
onDragHovered(int itemIndex, int x, int y)1051     public void onDragHovered(int itemIndex, int x, int y) {}
1052 
1053     @Override
onDragFinished(int x, int y)1054     public void onDragFinished(int x, int y) {
1055         crossfadeViews(mSearchViewContainer, mRemoveViewContainer, ANIMATION_DURATION);
1056     }
1057 
1058     @Override
onDroppedOnRemove()1059     public void onDroppedOnRemove() {}
1060 
1061     /**
1062      * Allows the PhoneFavoriteFragment to attach the drag controller to mRemoveViewContainer
1063      * once it has been attached to the activity.
1064      */
1065     @Override
setDragDropController(DragDropController dragController)1066     public void setDragDropController(DragDropController dragController) {
1067         mRemoveViewContainer.setDragDropController(dragController);
1068     }
1069 
1070     /**
1071      * Crossfades two views so that the first one appears while the other one is fading
1072      * out of view.
1073      */
crossfadeViews(final View fadeIn, final View fadeOut, int duration)1074     private void crossfadeViews(final View fadeIn, final View fadeOut, int duration) {
1075         fadeOut.animate().alpha(0).setDuration(duration)
1076         .setListener(new AnimatorListenerAdapter() {
1077             @Override
1078             public void onAnimationEnd(Animator animation) {
1079                 fadeOut.setVisibility(View.GONE);
1080             }
1081         });
1082 
1083         fadeIn.setVisibility(View.VISIBLE);
1084         fadeIn.setAlpha(0);
1085         fadeIn.animate().alpha(1).setDuration(ANIMATION_DURATION)
1086                 .setListener(null);
1087     }
1088 
shouldShowOnscreenDialButton()1089     private boolean shouldShowOnscreenDialButton() {
1090         return getResources().getBoolean(R.bool.config_show_onscreen_dial_button);
1091     }
1092 }
1093