diff options
Diffstat (limited to 'java/com/android/dialer/app/DialtactsActivity.java')
-rw-r--r-- | java/com/android/dialer/app/DialtactsActivity.java | 1526 |
1 files changed, 1526 insertions, 0 deletions
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java new file mode 100644 index 000000000..6e2c6be7d --- /dev/null +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -0,0 +1,1526 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app; + +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.KeyguardManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; +import android.os.Trace; +import android.provider.CallLog.Calls; +import android.speech.RecognizerIntent; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.telecom.PhoneAccount; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.DragEvent; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnDragListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView.OnScrollListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.dialog.ClearFrequentsDialog; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker; +import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener; +import com.android.contacts.common.widget.FloatingActionButtonController; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.animation.AnimationListenerAdapter; +import com.android.dialer.app.calllog.CallLogActivity; +import com.android.dialer.app.calllog.CallLogFragment; +import com.android.dialer.app.calllog.CallLogNotificationsService; +import com.android.dialer.app.dialpad.DialpadFragment; +import com.android.dialer.app.list.DialtactsPagerAdapter; +import com.android.dialer.app.list.DragDropController; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.list.OldSpeedDialFragment; +import com.android.dialer.app.list.OnDragDropListener; +import com.android.dialer.app.list.OnListFragmentScrolledListener; +import com.android.dialer.app.list.PhoneFavoriteSquareTileView; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.list.SearchFragment; +import com.android.dialer.app.list.SmartDialSearchFragment; +import com.android.dialer.app.settings.DialerSettingsActivity; +import com.android.dialer.app.widget.ActionBarController; +import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.callcomposer.CallComposerActivity; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.interactions.PhoneNumberInteraction; +import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.ScreenEvent; +import com.android.dialer.p13n.inference.P13nRanking; +import com.android.dialer.p13n.inference.protocol.P13nRanker; +import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener; +import com.android.dialer.p13n.logging.P13nLogger; +import com.android.dialer.p13n.logging.P13nLogging; +import com.android.dialer.postcall.PostCall; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.SimulatorComponent; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.util.TouchPointManager; +import com.android.dialer.util.TransactionSafeActivity; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** The dialer tab's title is 'phone', a more common name (see strings.xml). */ +@UsedByReflection(value = "AndroidManifest-app.xml") +public class DialtactsActivity extends TransactionSafeActivity + implements View.OnClickListener, + DialpadFragment.OnDialpadQueryChangedListener, + OnListFragmentScrolledListener, + CallLogFragment.HostInterface, + DialpadFragment.HostInterface, + OldSpeedDialFragment.HostInterface, + SearchFragment.HostInterface, + OnDragDropListener, + OnPhoneNumberPickerActionListener, + PopupMenu.OnMenuItemClickListener, + ViewPager.OnPageChangeListener, + ActionBarController.ActivityUi, + PhoneNumberInteraction.InteractionErrorListener, + PhoneNumberInteraction.DisambigDialogDismissedListener, + ActivityCompat.OnRequestPermissionsResultCallback { + + public static final boolean DEBUG = false; + @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad"; + private static final String ACTION_SHOW_TAB = "ACTION_SHOW_TAB"; + @VisibleForTesting public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB"; + public static final String EXTRA_CLEAR_NEW_VOICEMAILS = "EXTRA_CLEAR_NEW_VOICEMAILS"; + private static final String TAG = "DialtactsActivity"; + private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; + private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; + private static final String KEY_SEARCH_QUERY = "search_query"; + private static final String KEY_FIRST_LAUNCH = "first_launch"; + private static final String KEY_WAS_CONFIGURATION_CHANGE = "was_configuration_change"; + private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; + private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; + private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; + private static final String TAG_FAVORITES_FRAGMENT = "favorites"; + /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */ + private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; + + private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; + public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2; + + private static final int FAB_SCALE_IN_DELAY_MS = 300; + + /** + * Minimum time the history tab must have been selected for it to be marked as seen in onStop() + */ + private static final long HISTORY_TAB_SEEN_TIMEOUT = TimeUnit.SECONDS.toMillis(3); + + /** Fragment containing the dialpad that slides into view */ + protected DialpadFragment mDialpadFragment; + + private CoordinatorLayout mParentLayout; + /** Fragment for searching phone numbers using the alphanumeric keyboard. */ + private RegularSearchFragment mRegularSearchFragment; + + /** Fragment for searching phone numbers using the dialpad. */ + private SmartDialSearchFragment mSmartDialSearchFragment; + + /** Animation that slides in. */ + private Animation mSlideIn; + + /** Animation that slides out. */ + private Animation mSlideOut; + /** Fragment containing the speed dial list, call history list, and all contacts list. */ + private ListsFragment mListsFragment; + /** + * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can be + * commited. + */ + private boolean mStateSaved; + + private boolean mIsRestarting; + private boolean mInDialpadSearch; + private boolean mInRegularSearch; + private boolean mClearSearchOnPause; + private boolean mIsDialpadShown; + private boolean mShowDialpadOnResume; + /** Whether or not the device is in landscape orientation. */ + private boolean mIsLandscape; + /** True if the dialpad is only temporarily showing due to being in call */ + private boolean mInCallDialpadUp; + /** True when this activity has been launched for the first time. */ + private boolean mFirstLaunch; + /** + * Search query to be applied to the SearchView in the ActionBar once onCreateOptionsMenu has been + * called. + */ + private String mPendingSearchViewQuery; + + private PopupMenu mOverflowMenu; + private EditText mSearchView; + private View mVoiceSearchButton; + private String mSearchQuery; + private String mDialpadQuery; + private DialerDatabaseHelper mDialerDatabaseHelper; + private DragDropController mDragDropController; + private ActionBarController mActionBarController; + private FloatingActionButtonController mFloatingActionButtonController; + private boolean mWasConfigurationChange; + private long timeTabSelected; + + private P13nLogger mP13nLogger; + private P13nRanker mP13nRanker; + + AnimationListenerAdapter mSlideInListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + maybeEnterSearchUi(); + } + }; + /** Listener for after slide out animation completes on dialer fragment. */ + AnimationListenerAdapter mSlideOutListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + commitDialpadFragmentHide(); + } + }; + /** Listener used to send search queries to the phone search fragment. */ + private final TextWatcher mPhoneSearchQueryTextListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final String newText = s.toString(); + if (newText.equals(mSearchQuery)) { + // If the query hasn't changed (perhaps due to activity being destroyed + // and restored, or user launching the same DIAL intent twice), then there is + // no need to do anything here. + return; + } + if (DEBUG) { + LogUtil.v("DialtactsActivity.onTextChanged", "called with new query: " + newText); + LogUtil.v("DialtactsActivity.onTextChanged", "previous query: " + mSearchQuery); + } + mSearchQuery = newText; + + // Show search fragment only when the query string is changed to non-empty text. + if (!TextUtils.isEmpty(newText)) { + // Call enterSearchUi only if we are switching search modes, or showing a search + // fragment for the first time. + final boolean sameSearchMode = + (mIsDialpadShown && mInDialpadSearch) || (!mIsDialpadShown && mInRegularSearch); + if (!sameSearchMode) { + enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */); + } + } + + if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + mSmartDialSearchFragment.setQueryString(mSearchQuery); + } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + mRegularSearchFragment.setQueryString(mSearchQuery); + } + } + + @Override + public void afterTextChanged(Editable s) {} + }; + /** Open the search UI when the user clicks on the search box. */ + private final View.OnClickListener mSearchViewOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!isInSearchUi()) { + mActionBarController.onSearchBoxTapped(); + enterSearchUi( + false /* smartDialSearch */, mSearchView.getText().toString(), true /* animate */); + } + } + }; + + private int mActionBarHeight; + private int mPreviouslySelectedTabIndex; + /** Handles the user closing the soft keyboard. */ + private final View.OnKeyListener mSearchEditTextLayoutListener = + new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { + if (TextUtils.isEmpty(mSearchView.getText().toString())) { + // If the search term is empty, close the search UI. + maybeExitSearchUi(); + } else { + // If the search term is not empty, show the dialpad fab. + showFabInSearchUi(); + } + } + return false; + } + }; + /** + * The text returned from a voice search query. Set in {@link #onActivityResult} and used in + * {@link #onResume()} to populate the search box. + */ + private String mVoiceSearchQuery; + + /** + * @param tab the TAB_INDEX_* constant in {@link ListsFragment} + * @return A intent that will open the DialtactsActivity into the specified tab. The intent for + * each tab will be unique. + */ + public static Intent getShowTabIntent(Context context, int tab) { + Intent intent = new Intent(context, DialtactsActivity.class); + intent.setAction(ACTION_SHOW_TAB); + intent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, tab); + intent.setData( + new Uri.Builder() + .scheme("intent") + .authority(context.getPackageName()) + .appendPath(TAG) + .appendQueryParameter(DialtactsActivity.EXTRA_SHOW_TAB, String.valueOf(tab)) + .build()); + + return intent; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedInstanceState); + + mFirstLaunch = true; + + final Resources resources = getResources(); + mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); + + Trace.beginSection(TAG + " setContentView"); + setContentView(R.layout.dialtacts_activity); + Trace.endSection(); + getWindow().setBackgroundDrawable(null); + + Trace.beginSection(TAG + " setup Views"); + final ActionBar actionBar = getActionBarSafely(); + actionBar.setCustomView(R.layout.search_edittext); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setBackgroundDrawable(null); + + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); + searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); + + mActionBarController = new ActionBarController(this, searchEditTextLayout); + + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); + mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); + mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); + searchEditTextLayout + .findViewById(R.id.search_box_collapsed) + .setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout.setCallback( + new SearchEditTextLayout.Callback() { + @Override + public void onBackButtonClicked() { + onBackPressed(); + } + + @Override + public void onSearchViewClicked() { + // Hide FAB, as the keyboard is shown. + mFloatingActionButtonController.scaleOut(); + } + }); + + mIsLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + mPreviouslySelectedTabIndex = DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL; + FloatingActionButton floatingActionButton = + (FloatingActionButton) findViewById(R.id.floating_action_button); + floatingActionButton.setOnClickListener(this); + mFloatingActionButtonController = + new FloatingActionButtonController(this, floatingActionButton); + + ImageButton optionsMenuButton = + (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); + optionsMenuButton.setOnClickListener(this); + mOverflowMenu = buildOptionsMenu(optionsMenuButton); + optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); + + // Add the favorites fragment but only if savedInstanceState is null. Otherwise the + // fragment manager is responsible for recreating it. + if (savedInstanceState == null) { + getFragmentManager() + .beginTransaction() + .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) + .commit(); + } else { + mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); + mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); + mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); + mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); + mWasConfigurationChange = savedInstanceState.getBoolean(KEY_WAS_CONFIGURATION_CHANGE); + mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); + mActionBarController.restoreInstanceState(savedInstanceState); + } + + final boolean isLayoutRtl = ViewUtil.isRtl(); + if (mIsLandscape) { + mSlideIn = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); + mSlideOut = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); + } else { + mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); + mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); + } + + mSlideIn.setInterpolator(AnimUtils.EASE_IN); + mSlideOut.setInterpolator(AnimUtils.EASE_OUT); + + mSlideIn.setAnimationListener(mSlideInListener); + mSlideOut.setAnimationListener(mSlideOutListener); + + mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout); + mParentLayout.setOnDragListener(new LayoutOnDragListener()); + ViewUtil.doOnGlobalLayout( + floatingActionButton, + view -> { + int screenWidth = mParentLayout.getWidth(); + mFloatingActionButtonController.setScreenWidth(screenWidth); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + }); + + Trace.endSection(); + + Trace.beginSection(TAG + " initialize smart dialing"); + mDialerDatabaseHelper = Database.get(this).getDatabaseHelper(this); + SmartDialPrefix.initializeNanpSettings(this); + Trace.endSection(); + + mP13nLogger = P13nLogging.get(getApplicationContext()); + mP13nRanker = P13nRanking.get(getApplicationContext()); + Trace.endSection(); + } + + @NonNull + private ActionBar getActionBarSafely() { + return Assert.isNotNull(getSupportActionBar()); + } + + @Override + protected void onResume() { + LogUtil.d("DialtactsActivity.onResume", ""); + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + mStateSaved = false; + if (mFirstLaunch) { + displayFragment(getIntent()); + } else if (!phoneIsInUse() && mInCallDialpadUp) { + hideDialpadFragment(false, true); + mInCallDialpadUp = false; + } else if (mShowDialpadOnResume) { + showDialpadFragment(false); + mShowDialpadOnResume = false; + } else { + PostCall.promptUserForMessageIfNecessary(this, mParentLayout); + } + + // If there was a voice query result returned in the {@link #onActivityResult} callback, it + // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be + // shown until onResume has completed. Active the search UI and set the search term now. + if (!TextUtils.isEmpty(mVoiceSearchQuery)) { + mActionBarController.onSearchBoxTapped(); + mSearchView.setText(mVoiceSearchQuery); + mVoiceSearchQuery = null; + } + + if (mIsRestarting) { + // This is only called when the activity goes from resumed -> paused -> resumed, so it + // will not cause an extra view to be sent out on rotation + if (mIsDialpadShown) { + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + } + mIsRestarting = false; + } + + prepareVoiceSearchButton(); + if (!mWasConfigurationChange) { + mDialerDatabaseHelper.startSmartDialUpdateThread(); + } + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + + if (mFirstLaunch) { + // Only process the Intent the first time onResume() is called after receiving it + if (Calls.CONTENT_TYPE.equals(getIntent().getType())) { + // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only + // used internally. + final Bundle extras = getIntent().getExtras(); + if (extras != null && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) { + mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); + Logger.get(this).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CLICKED); + } else { + mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_HISTORY); + } + } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) { + int index = + getIntent().getIntExtra(EXTRA_SHOW_TAB, DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL); + if (index < mListsFragment.getTabCount()) { + // Hide dialpad since this is an explicit intent to show a specific tab, which is coming + // from missed call or voicemail notification. + hideDialpadFragment(false, false); + exitSearchUi(); + mListsFragment.showTab(index); + } + } + + if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) { + CallLogNotificationsService.markNewVoicemailsAsOld(this, null); + } + } + + mFirstLaunch = false; + + setSearchBoxHint(); + timeTabSelected = SystemClock.elapsedRealtime(); + + mP13nLogger.reset(); + mP13nRanker.refresh( + new P13nRefreshCompleteListener() { + @Override + public void onP13nRefreshComplete() { + // TODO: make zero-query search results visible + } + }); + Trace.endSection(); + } + + @Override + protected void onRestart() { + super.onRestart(); + mIsRestarting = true; + } + + @Override + protected void onPause() { + if (mClearSearchOnPause) { + hideDialpadAndSearchUi(); + mClearSearchOnPause = false; + } + if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) { + commitDialpadFragmentHide(); + } + super.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + boolean timeoutElapsed = + SystemClock.elapsedRealtime() - timeTabSelected >= HISTORY_TAB_SEEN_TIMEOUT; + boolean isOnHistoryTab = + mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_HISTORY; + if (isOnHistoryTab + && timeoutElapsed + && !isChangingConfigurations() + && !getSystemService(KeyguardManager.class).isKeyguardLocked()) { + mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_SEARCH_QUERY, mSearchQuery); + outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); + outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); + outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); + outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); + outState.putBoolean(KEY_WAS_CONFIGURATION_CHANGE, isChangingConfigurations()); + mActionBarController.saveInstanceState(outState); + mStateSaved = true; + } + + @Override + public void onAttachFragment(final Fragment fragment) { + LogUtil.d("DialtactsActivity.onAttachFragment", "fragment: %s", fragment); + if (fragment instanceof DialpadFragment) { + mDialpadFragment = (DialpadFragment) fragment; + if (!mIsDialpadShown && !mShowDialpadOnResume) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.hide(mDialpadFragment); + transaction.commit(); + } + } else if (fragment instanceof SmartDialSearchFragment) { + mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; + mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); + if (!TextUtils.isEmpty(mDialpadQuery)) { + mSmartDialSearchFragment.setAddToContactNumber(mDialpadQuery); + } + } else if (fragment instanceof SearchFragment) { + mRegularSearchFragment = (RegularSearchFragment) fragment; + mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); + } else if (fragment instanceof ListsFragment) { + mListsFragment = (ListsFragment) fragment; + mListsFragment.addOnPageChangeListener(this); + } + if (fragment instanceof SearchFragment) { + final SearchFragment searchFragment = (SearchFragment) fragment; + searchFragment.setReranker( + new CursorReranker() { + @Override + @MainThread + public Cursor rerankCursor(Cursor data) { + Assert.isMainThread(); + String queryString = searchFragment.getQueryString(); + return mP13nRanker.rankCursor(data, queryString == null ? 0 : queryString.length()); + } + }); + searchFragment.addOnLoadFinishedListener( + new OnLoadFinishedListener() { + @Override + public void onLoadFinished() { + mP13nLogger.onSearchQuery( + searchFragment.getQueryString(), + (PhoneNumberListAdapter) searchFragment.getAdapter()); + } + }); + } + } + + protected void handleMenuSettings() { + final Intent intent = new Intent(this, DialerSettingsActivity.class); + startActivity(intent); + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.floating_action_button) { + if (mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + DialerUtils.startActivityWithErrorToast( + this, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); + Logger.get(this).logImpression(DialerImpression.Type.NEW_CONTACT_FAB); + } else if (!mIsDialpadShown) { + mInCallDialpadUp = false; + showDialpadFragment(true); + PostCall.closePrompt(); + } + } else if (resId == R.id.voice_search_button) { + try { + startActivityForResult( + new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), + ACTIVITY_REQUEST_CODE_VOICE_SEARCH); + } catch (ActivityNotFoundException e) { + Toast.makeText( + DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT) + .show(); + } + } else if (resId == R.id.dialtacts_options_menu_button) { + mOverflowMenu.show(); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (!isSafeToCommitTransactions()) { + return true; + } + + int resId = item.getItemId(); + if (resId == R.id.menu_history) { + final Intent intent = new Intent(this, CallLogActivity.class); + startActivity(intent); + } else if (resId == R.id.menu_clear_frequents) { + ClearFrequentsDialog.show(getFragmentManager()); + Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this); + return true; + } else if (resId == R.id.menu_call_settings) { + handleMenuSettings(); + Logger.get(this).logScreenView(ScreenEvent.Type.SETTINGS, this); + return true; + } + return false; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + LogUtil.i( + "DialtactsActivity.onActivityResult", + "requestCode:%d, resultCode:%d", + requestCode, + resultCode); + if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { + if (resultCode == RESULT_OK) { + final ArrayList<String> matches = + data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); + if (matches.size() > 0) { + mVoiceSearchQuery = matches.get(0); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard"); + } + } else { + LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed"); + } + } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) { + if (resultCode == RESULT_FIRST_USER) { + LogUtil.i( + "DialtactsActivity.onActivityResult", "returned from call composer, error occurred"); + String message = + getString( + R.string.call_composer_connection_failed, + data.getStringExtra(CallComposerActivity.KEY_CONTACT_NAME)); + Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show(); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error"); + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Update the number of unread voicemails (potentially other tabs) displayed next to the tab icon. + */ + public void updateTabUnreadCounts() { + mListsFragment.updateTabUnreadCounts(); + } + + /** + * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual + * updates are handled by a callback which is invoked after the dialpad fragment is shown. + * + * @see #onDialpadShown + */ + private void showDialpadFragment(boolean animate) { + LogUtil.d("DialtactActivity.showDialpadFragment", "animate: %b", animate); + if (mIsDialpadShown || mStateSaved) { + return; + } + mIsDialpadShown = true; + + mListsFragment.setUserVisibleHint(false); + + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + if (mDialpadFragment == null) { + mDialpadFragment = new DialpadFragment(); + ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); + } else { + ft.show(mDialpadFragment); + } + + mDialpadFragment.setAnimate(animate); + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + ft.commit(); + + if (animate) { + mFloatingActionButtonController.scaleOut(); + } else { + mFloatingActionButtonController.setVisible(false); + maybeEnterSearchUi(); + } + mActionBarController.onDialpadUp(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + + //adjust the title, so the user will know where we're at when the activity start/resumes. + setTitle(R.string.launcherDialpadActivityLabel); + } + + /** Callback from child DialpadFragment when the dialpad is shown. */ + public void onDialpadShown() { + LogUtil.d("DialtactsActivity.onDialpadShown", ""); + Assert.isNotNull(mDialpadFragment); + if (mDialpadFragment.getAnimate()) { + Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn); + } else { + mDialpadFragment.setYFraction(0); + } + + updateSearchFragmentPosition(); + } + + /** + * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in a + * callback after the hide animation ends. + * + * @see #commitDialpadFragmentHide + */ + public void hideDialpadFragment(boolean animate, boolean clearDialpad) { + if (mDialpadFragment == null || mDialpadFragment.getView() == null) { + return; + } + if (clearDialpad) { + // Temporarily disable accessibility when we clear the dialpad, since it should be + // invisible and should not announce anything. + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mDialpadFragment.clearDialpad(); + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + if (!mIsDialpadShown) { + return; + } + mIsDialpadShown = false; + mDialpadFragment.setAnimate(animate); + mListsFragment.setUserVisibleHint(true); + mListsFragment.sendScreenViewForCurrentPosition(); + + updateSearchFragmentPosition(); + + mFloatingActionButtonController.align(getFabAlignment(), animate); + if (animate) { + mDialpadFragment.getView().startAnimation(mSlideOut); + } else { + commitDialpadFragmentHide(); + } + + mActionBarController.onDialpadDown(); + + if (isInSearchUi()) { + if (TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + } + } + //reset the title to normal. + setTitle(R.string.launcherActivityLabel); + } + + /** Finishes hiding the dialpad fragment after any animations are completed. */ + private void commitDialpadFragmentHide() { + if (!mStateSaved + && mDialpadFragment != null + && !mDialpadFragment.isHidden() + && !isDestroyed()) { + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.hide(mDialpadFragment); + ft.commit(); + } + mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); + } + + private void updateSearchFragmentPosition() { + SearchFragment fragment = null; + if (mSmartDialSearchFragment != null) { + fragment = mSmartDialSearchFragment; + } else if (mRegularSearchFragment != null) { + fragment = mRegularSearchFragment; + } + LogUtil.d( + "DialtactsActivity.updateSearchFragmentPosition", + "fragment: %s, isVisible: %b", + fragment, + fragment != null && fragment.isVisible()); + if (fragment != null) { + // We need to force animation here even when fragment is not visible since it might not be + // visible immediately after screen orientation change and dialpad height would not be + // available immediately which is required to update position. By forcing an animation, + // position will be updated after a delay by when the dialpad height would be available. + fragment.updatePosition(true /* animate */); + } + } + + @Override + public boolean isInSearchUi() { + return mInDialpadSearch || mInRegularSearch; + } + + @Override + public boolean hasSearchQuery() { + return !TextUtils.isEmpty(mSearchQuery); + } + + private void setNotInSearchUi() { + mInDialpadSearch = false; + mInRegularSearch = false; + } + + private void hideDialpadAndSearchUi() { + if (mIsDialpadShown) { + hideDialpadFragment(false, true); + } else { + exitSearchUi(); + } + } + + private void prepareVoiceSearchButton() { + final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + if (canIntentBeHandled(voiceIntent)) { + mVoiceSearchButton.setVisibility(View.VISIBLE); + mVoiceSearchButton.setOnClickListener(this); + } else { + mVoiceSearchButton.setVisibility(View.GONE); + } + } + + public boolean isNearbyPlacesSearchEnabled() { + return false; + } + + protected int getSearchBoxHint() { + return R.string.dialer_hint_find_contact; + } + + /** Sets the hint text for the contacts search box */ + private void setSearchBoxHint() { + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) + getActionBarSafely().getCustomView().findViewById(R.id.search_view_container); + ((TextView) searchEditTextLayout.findViewById(R.id.search_box_start_search)) + .setHint(getSearchBoxHint()); + } + + protected OptionsPopupMenu buildOptionsMenu(View invoker) { + final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); + popupMenu.inflate(R.menu.dialtacts_options); + popupMenu.setOnMenuItemClickListener(this); + return popupMenu; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mPendingSearchViewQuery != null) { + mSearchView.setText(mPendingSearchViewQuery); + mPendingSearchViewQuery = null; + } + if (mActionBarController != null) { + mActionBarController.restoreActionBarOffset(); + } + return false; + } + + /** + * Returns true if the intent is due to hitting the green send key (hardware call button: + * KEYCODE_CALL) while in a call. + * + * @param intent the intent that launched this activity + * @return true if the intent is due to hitting the green send key while in a call + */ + private boolean isSendKeyWhileInCall(Intent intent) { + // If there is a call in progress and the user launched the dialer by hitting the call + // button, go straight to the in-call screen. + final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); + + // When KEYCODE_CALL event is handled it dispatches an intent with the ACTION_CALL_BUTTON. + // Besides of checking the intent action, we must check if the phone is really during a + // call in order to decide whether to ignore the event or continue to display the activity. + if (callKey && phoneIsInUse()) { + TelecomUtil.showInCallScreen(this, false); + return true; + } + + return false; + } + + /** + * Sets the current tab based on the intent's request type + * + * @param intent Intent that contains information about which tab should be selected + */ + private void displayFragment(Intent intent) { + // If we got here by hitting send and we're in call forward along to the in-call activity + if (isSendKeyWhileInCall(intent)) { + finish(); + return; + } + + final boolean showDialpadChooser = + !ACTION_SHOW_TAB.equals(intent.getAction()) + && phoneIsInUse() + && !DialpadFragment.isAddCallMode(intent); + if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) { + showDialpadFragment(false); + mDialpadFragment.setStartedFromNewIntent(true); + if (showDialpadChooser && !mDialpadFragment.isVisible()) { + mInCallDialpadUp = true; + } + } + } + + @Override + public void onNewIntent(Intent newIntent) { + setIntent(newIntent); + mFirstLaunch = true; + + mStateSaved = false; + displayFragment(newIntent); + + invalidateOptionsMenu(); + } + + /** Returns true if the given intent contains a phone number to populate the dialer with */ + private boolean isDialIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { + return true; + } + if (Intent.ACTION_VIEW.equals(action)) { + final Uri data = intent.getData(); + if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { + return true; + } + } + return false; + } + + /** Shows the search fragment */ + private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) { + if (mStateSaved || getFragmentManager().isDestroyed()) { + // Weird race condition where fragment is doing work after the activity is destroyed + // due to talkback being on (b/10209937). Just return since we can't do any + // constructive here. + return; + } + + if (DEBUG) { + LogUtil.v("DialtactsActivity.enterSearchUi", "smart dial " + smartDialSearch); + } + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mInDialpadSearch && mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } else if (mInRegularSearch && mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + + final String tag; + if (smartDialSearch) { + tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; + } else { + tag = TAG_REGULAR_SEARCH_FRAGMENT; + } + mInDialpadSearch = smartDialSearch; + mInRegularSearch = !smartDialSearch; + + mFloatingActionButtonController.scaleOut(); + + SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); + if (animate) { + transaction.setCustomAnimations(android.R.animator.fade_in, 0); + } else { + transaction.setTransition(FragmentTransaction.TRANSIT_NONE); + } + if (fragment == null) { + if (smartDialSearch) { + fragment = new SmartDialSearchFragment(); + } else { + fragment = Bindings.getLegacy(this).newRegularSearchFragment(); + fragment.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // Show the FAB when the user touches the lists fragment and the soft + // keyboard is hidden. + hideDialpadFragment(true, false); + showFabInSearchUi(); + v.performClick(); + return false; + } + }); + } + transaction.add(R.id.dialtacts_frame, fragment, tag); + } else { + transaction.show(fragment); + } + // DialtactsActivity will provide the options menu + fragment.setHasOptionsMenu(false); + // Will show empty list if P13nRanker is not enabled. Else, re-ranked list by the ranker. + fragment.setShowEmptyListForNullQuery(mP13nRanker.shouldShowEmptyListForNullQuery()); + if (!smartDialSearch) { + fragment.setQueryString(query); + } + transaction.commit(); + + if (animate) { + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + } + mListsFragment.setUserVisibleHint(false); + + if (smartDialSearch) { + Logger.get(this).logScreenView(ScreenEvent.Type.SMART_DIAL_SEARCH, this); + } else { + Logger.get(this).logScreenView(ScreenEvent.Type.REGULAR_SEARCH, this); + } + } + + /** Hides the search fragment */ + private void exitSearchUi() { + // See related bug in enterSearchUI(); + if (getFragmentManager().isDestroyed() || mStateSaved) { + return; + } + + mSearchView.setText(null); + + if (mDialpadFragment != null) { + mDialpadFragment.clearDialpad(); + } + + setNotInSearchUi(); + + // Restore the FAB for the lists fragment. + if (getFabAlignment() != FloatingActionButtonController.ALIGN_END) { + mFloatingActionButtonController.setVisible(false); + } + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + onPageScrolled(mListsFragment.getCurrentTabIndex(), 0 /* offset */, 0 /* pixelOffset */); + onPageSelected(mListsFragment.getCurrentTabIndex()); + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } + if (mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + transaction.commit(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(1).withLayer(); + + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // If the dialpad fragment wasn't previously visible, then send a screen view because + // we are exiting regular search. Otherwise, the screen view will be sent by + // {@link #hideDialpadFragment}. + mListsFragment.sendScreenViewForCurrentPosition(); + mListsFragment.setUserVisibleHint(true); + } + + mActionBarController.onSearchUiExited(); + } + + @Override + public void onBackPressed() { + if (mStateSaved) { + return; + } + if (mIsDialpadShown) { + if (TextUtils.isEmpty(mSearchQuery) + || (mSmartDialSearchFragment != null + && mSmartDialSearchFragment.isVisible() + && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { + exitSearchUi(); + } + hideDialpadFragment(true, false); + } else if (isInSearchUi()) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + } else { + super.onBackPressed(); + } + } + + private void maybeEnterSearchUi() { + if (!isInSearchUi()) { + enterSearchUi(true /* isSmartDial */, mSearchQuery, false); + } + } + + /** @return True if the search UI was exited, false otherwise */ + private boolean maybeExitSearchUi() { + if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + return true; + } + return false; + } + + private void showFabInSearchUi() { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.quantum_ic_dialpad_white_24, null), + getResources().getString(R.string.action_menu_dialpad_button)); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + } + + @Override + public void onDialpadQueryChanged(String query) { + mDialpadQuery = query; + if (mSmartDialSearchFragment != null) { + mSmartDialSearchFragment.setAddToContactNumber(query); + } + final String normalizedQuery = + SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); + + if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { + if (DEBUG) { + LogUtil.v("DialtactsActivity.onDialpadQueryChanged", "new query: " + query); + } + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // This callback can happen if the dialpad fragment is recreated because of + // activity destruction. In that case, don't update the search view because + // that would bring the user back to the search fragment regardless of the + // previous state of the application. Instead, just return here and let the + // fragment manager correctly figure out whatever fragment was last displayed. + if (!TextUtils.isEmpty(normalizedQuery)) { + mPendingSearchViewQuery = normalizedQuery; + } + return; + } + mSearchView.setText(normalizedQuery); + } + + try { + if (mDialpadFragment != null && mDialpadFragment.isVisible()) { + mDialpadFragment.process_quote_emergency_unquote(normalizedQuery); + } + } catch (Exception ignored) { + // Skip any exceptions for this piece of code + } + } + + @Override + public boolean onDialpadSpacerTouchWithEmptyQuery() { + if (mInDialpadSearch + && mSmartDialSearchFragment != null + && !mSmartDialSearchFragment.isShowingPermissionRequest()) { + hideDialpadFragment(true /* animate */, true /* clearDialpad */); + return true; + } + return false; + } + + @Override + public void onListFragmentScrollStateChange(int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + hideDialpadFragment(true, false); + DialerUtils.hideInputMethod(mParentLayout); + } + } + + @Override + public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) { + // TODO: No-op for now. This should eventually show/hide the actionBar based on + // interactions with the ListsFragments. + } + + private boolean phoneIsInUse() { + return TelecomUtil.isInCall(this); + } + + private boolean canIntentBeHandled(Intent intent) { + final PackageManager packageManager = getPackageManager(); + final List<ResolveInfo> resolveInfo = + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return resolveInfo != null && resolveInfo.size() > 0; + } + + /** Called when the user has long-pressed a contact tile to start a drag operation. */ + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { + mListsFragment.showRemoveView(true); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {} + + /** Called when the user has released a contact tile after long-pressing it. */ + @Override + public void onDragFinished(int x, int y) { + mListsFragment.showRemoveView(false); + } + + @Override + public void onDroppedOnRemove() {} + + /** + * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer once it has + * been attached to the activity. + */ + @Override + public void setDragDropController(DragDropController dragController) { + mDragDropController = dragController; + mListsFragment.getRemoveView().setDragDropController(dragController); + } + + /** Implemented to satisfy {@link OldSpeedDialFragment.HostInterface} */ + @Override + public void showAllContactsTab() { + if (mListsFragment != null) { + mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS); + } + } + + /** Implemented to satisfy {@link CallLogFragment.HostInterface} */ + @Override + public void showDialpad() { + showDialpadFragment(true); + } + + @Override + public void enableFloatingButton(boolean enabled) { + LogUtil.d("DialtactsActivity.enableFloatingButton", "enable: %b", enabled); + // Floating button shouldn't be enabled when dialpad is shown. + if (!isDialpadShown() || !enabled) { + mFloatingActionButtonController.setVisible(enabled); + } + } + + @Override + public void onPickDataUri( + Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + mClearSearchOnPause = true; + PhoneNumberInteraction.startInteractionForPhoneCall( + DialtactsActivity.this, dataUri, isVideoCall, callSpecificAppData); + } + + @Override + public void onPickPhoneNumber( + String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + if (phoneNumber == null) { + // Invalid phone number, but let the call go through so that InCallUI can show + // an error message. + phoneNumber = ""; + } + + Intent intent = + new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build(); + + DialerUtils.startActivityWithErrorToast(this, intent); + mClearSearchOnPause = true; + } + + @Override + public void onHomeInActionBarSelected() { + exitSearchUi(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabIndex = mListsFragment.getCurrentTabIndex(); + + // Scroll the button from center to end when moving from the Speed Dial to Call History tab. + // In RTL, scroll when the current tab is Call History instead, since the order of the tabs + // is reversed and the ViewPager returns the left tab position during scroll. + boolean isRtl = ViewUtil.isRtl(); + if (!isRtl && tabIndex == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(positionOffset); + } else if (isRtl && tabIndex == DialtactsPagerAdapter.TAB_INDEX_HISTORY && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(1 - positionOffset); + } else if (tabIndex != DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { + mFloatingActionButtonController.onPageScrolled(1); + } + } + + @Override + public void onPageSelected(int position) { + updateMissedCalls(); + int tabIndex = mListsFragment.getCurrentTabIndex(); + mPreviouslySelectedTabIndex = tabIndex; + mFloatingActionButtonController.setVisible(true); + if (tabIndex == DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.quantum_ic_person_add_white_24, null), + getResources().getString(R.string.search_shortcut_create_new_contact)); + } else { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.quantum_ic_dialpad_white_24, null), + getResources().getString(R.string.action_menu_dialpad_button)); + } + + timeTabSelected = SystemClock.elapsedRealtime(); + } + + @Override + public void onPageScrollStateChanged(int state) {} + + @Override + public boolean isActionBarShowing() { + return mActionBarController.isActionBarShowing(); + } + + @Override + public boolean isDialpadShown() { + return mIsDialpadShown; + } + + @Override + public int getDialpadHeight() { + if (mDialpadFragment != null) { + return mDialpadFragment.getDialpadHeight(); + } + return 0; + } + + @Override + public void setActionBarHideOffset(int offset) { + getActionBarSafely().setHideOffset(offset); + } + + @Override + public int getActionBarHeight() { + return mActionBarHeight; + } + + private int getFabAlignment() { + if (!mIsLandscape + && !isInSearchUi() + && mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { + return FloatingActionButtonController.ALIGN_MIDDLE; + } + return FloatingActionButtonController.ALIGN_END; + } + + private void updateMissedCalls() { + if (mPreviouslySelectedTabIndex == DialtactsPagerAdapter.TAB_INDEX_HISTORY) { + mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); + } + } + + @Override + public void onDisambigDialogDismissed() { + // Don't do anything; the app will remain open with favorites tiles displayed. + } + + @Override + public void interactionError(@InteractionErrorCode int interactionErrorCode) { + switch (interactionErrorCode) { + case InteractionErrorCode.USER_LEAVING_ACTIVITY: + // This is expected to happen if the user exits the activity before the interaction occurs. + return; + case InteractionErrorCode.CONTACT_NOT_FOUND: + case InteractionErrorCode.CONTACT_HAS_NO_NUMBER: + case InteractionErrorCode.OTHER_ERROR: + default: + // All other error codes are unexpected. For example, it should be impossible to start an + // interaction with an invalid contact from the Dialtacts activity. + Assert.fail("PhoneNumberInteraction error: " + interactionErrorCode); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + // This should never happen; it should be impossible to start an interaction without the + // contacts permission from the Dialtacts activity. + Assert.fail( + String.format( + Locale.US, + "Permissions requested unexpectedly: %d/%s/%s", + requestCode, + Arrays.toString(permissions), + Arrays.toString(grantResults))); + } + + protected class OptionsPopupMenu extends PopupMenu { + + public OptionsPopupMenu(Context context, View anchor) { + super(context, anchor, Gravity.END); + } + + @Override + public void show() { + Menu menu = getMenu(); + MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); + clearFrequents.setVisible( + PermissionsUtil.hasContactsReadPermissions(DialtactsActivity.this) + && mListsFragment != null + && mListsFragment.hasFrequents()); + + menu.findItem(R.id.menu_history) + .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this)); + + Context context = DialtactsActivity.this.getApplicationContext(); + MenuItem simulatorMenuItem = menu.findItem(R.id.menu_simulator_submenu); + Simulator simulator = SimulatorComponent.get(context).getSimulator(); + if (simulator.shouldShow()) { + simulatorMenuItem.setVisible(true); + simulatorMenuItem.setActionProvider(simulator.getActionProvider(context)); + } else { + simulatorMenuItem.setVisible(false); + } + + super.show(); + } + } + + /** + * Listener that listens to drag events and sends their x and y coordinates to a {@link + * DragDropController}. + */ + private class LayoutOnDragListener implements OnDragListener { + + @Override + public boolean onDrag(View v, DragEvent event) { + if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { + mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); + } + return true; + } + } +} |