diff options
Diffstat (limited to 'src')
25 files changed, 2273 insertions, 1381 deletions
diff --git a/src/com/android/dialer/NeededForReflection.java b/src/com/android/dialer/NeededForReflection.java new file mode 100644 index 000000000..e836908b1 --- /dev/null +++ b/src/com/android/dialer/NeededForReflection.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes that the class, constructor, method or field is used for reflection and therefore cannot + * be removed by tools like ProGuard. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) +public @interface NeededForReflection{} diff --git a/src/com/android/dialer/NewDialtactsActivity.java b/src/com/android/dialer/NewDialtactsActivity.java index 584c951b3..66cbe8530 100644 --- a/src/com/android/dialer/NewDialtactsActivity.java +++ b/src/com/android/dialer/NewDialtactsActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * 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. @@ -16,10 +16,9 @@ package com.android.dialer; -import android.app.ActionBar; -import android.app.ActionBar.LayoutParams; -import android.app.ActionBar.Tab; -import android.app.ActionBar.TabListener; +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.backup.BackupManager; import android.app.Fragment; @@ -33,26 +32,20 @@ import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; -import android.preference.PreferenceManager; import android.provider.CallLog.Calls; +import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents.UI; -import android.support.v13.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; -import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.provider.Settings; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; -import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.ViewConfiguration; -import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.OnScrollListener; import android.widget.PopupMenu; import android.widget.SearchView; import android.widget.SearchView.OnCloseListener; @@ -61,27 +54,30 @@ import android.widget.Toast; import com.android.contacts.common.CallUtil; import com.android.contacts.common.activity.TransactionSafeActivity; -import com.android.contacts.common.list.ContactListFilterController; -import com.android.contacts.common.list.ContactListFilterController.ContactListFilterListener; +import com.android.contacts.common.dialog.ClearFrequentsDialog; +import com.android.contacts.common.interactions.ImportExportDialogFragment; import com.android.contacts.common.list.ContactListItemView; import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; import com.android.contacts.common.list.PhoneNumberPickerFragment; -import com.android.contacts.common.util.AccountFilterUtil; -import com.android.dialer.calllog.CallLogFragment; -import com.android.dialer.dialpad.DialpadFragment; +import com.android.dialer.calllog.NewCallLogActivity; +import com.android.dialer.dialpad.NewDialpadFragment; +import com.android.dialer.dialpad.SmartDialNameMatcher; import com.android.dialer.interactions.PhoneNumberInteraction; -import com.android.dialer.list.PhoneFavoriteFragment; -import com.android.dialer.util.OrientationUtil; +import com.android.dialer.list.NewPhoneFavoriteFragment; +import com.android.dialer.list.OnListFragmentScrolledListener; +import com.android.dialer.list.SmartDialSearchFragment; import com.android.internal.telephony.ITelephony; /** - * The dialer activity that has one tab with the virtual 12key - * dialer, a tab with recent calls in it, a tab with the contacts and - * a tab with the favorite. This is the container and the tabs are - * embedded using intents. * The dialer tab's title is 'phone', a more common name (see strings.xml). + * + * TODO krelease: All classes currently prefixed with New will replace the original classes or + * be renamed more appropriately before shipping. */ -public class NewDialtactsActivity extends TransactionSafeActivity implements View.OnClickListener { +public class NewDialtactsActivity extends TransactionSafeActivity implements View.OnClickListener, + NewDialpadFragment.OnDialpadQueryChangedListener, PopupMenu.OnMenuItemClickListener, + OnListFragmentScrolledListener, + NewPhoneFavoriteFragment.OnPhoneFavoriteFragmentStartedListener { private static final String TAG = "DialtactsActivity"; public static final boolean DEBUG = false; @@ -95,18 +91,16 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie private static final String CALL_ORIGIN_DIALTACTS = "com.android.dialer.DialtactsActivity"; + private static final String TAG_DIALPAD_FRAGMENT = "dialpad"; + 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"; - /** Used both by {@link ActionBar} and {@link ViewPagerAdapter} */ - private static final int TAB_INDEX_DIALER = 0; - private static final int TAB_INDEX_CALL_LOG = 1; - private static final int TAB_INDEX_FAVORITES = 2; - - private static final int TAB_INDEX_COUNT = 3; - private SharedPreferences mPrefs; public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences"; @@ -114,291 +108,51 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie /** Last manually selected tab index */ private static final String PREF_LAST_MANUALLY_SELECTED_TAB = "DialtactsActivity_last_manually_selected_tab"; - private static final int PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT = TAB_INDEX_DIALER; private static final int SUBACTIVITY_ACCOUNT_FILTER = 1; - public class ViewPagerAdapter extends FragmentPagerAdapter { - public ViewPagerAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public Fragment getItem(int position) { - switch (position) { - case TAB_INDEX_DIALER: - return new DialpadFragment(); - case TAB_INDEX_CALL_LOG: - return new CallLogFragment(); - case TAB_INDEX_FAVORITES: - return new PhoneFavoriteFragment(); - } - throw new IllegalStateException("No fragment at position " + position); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - // The parent's setPrimaryItem() also calls setMenuVisibility(), so we want to know - // when it happens. - if (DEBUG) { - Log.d(TAG, "FragmentPagerAdapter#setPrimaryItem(), position: " + position); - } - super.setPrimaryItem(container, position, object); - } - - @Override - public int getCount() { - return TAB_INDEX_COUNT; - } - } + private String mFilterText; /** - * True when the app detects user's drag event. This variable should not become true when - * mUserTabClick is true. - * - * During user's drag or tab click, we shouldn't show fake buttons but just show real - * ActionBar at the bottom of the screen, for transition animation. + * The main fragment displaying the user's favorites and frequent contacts */ - boolean mDuringSwipe = false; + private NewPhoneFavoriteFragment mPhoneFavoriteFragment; + /** - * True when the app detects user's tab click (at the top of the screen). This variable should - * not become true when mDuringSwipe is true. - * - * During user's drag or tab click, we shouldn't show fake buttons but just show real - * ActionBar at the bottom of the screen, for transition animation. + * Fragment containing the dialpad that slides into view */ - boolean mUserTabClick = false; - - private class PageChangeListener implements OnPageChangeListener { - private int mCurrentPosition = -1; - /** - * Used during page migration, to remember the next position {@link #onPageSelected(int)} - * specified. - */ - private int mNextPosition = -1; - - @Override - public void onPageScrolled( - int position, float positionOffset, int positionOffsetPixels) { - } - - @Override - public void onPageSelected(int position) { - if (DEBUG) Log.d(TAG, "onPageSelected: position: " + position); - final ActionBar actionBar = getActionBar(); - if (mDialpadFragment != null) { - if (mDuringSwipe && position == TAB_INDEX_DIALER) { - // TODO: Figure out if we want this or not. Right now - // - with this call, both fake buttons and real action bar overlap - // - without this call, there's tiny flicker happening to search/menu buttons. - // If we can reduce the flicker without this call, it would be much better. - // updateFakeMenuButtonsVisibility(true); - } - } - - if (mCurrentPosition == position) { - Log.w(TAG, "Previous position and next position became same (" + position + ")"); - } - - actionBar.selectTab(actionBar.getTabAt(position)); - mNextPosition = position; - } - - public void setCurrentPosition(int position) { - mCurrentPosition = position; - } - - public int getCurrentPosition() { - return mCurrentPosition; - } - - @Override - public void onPageScrollStateChanged(int state) { - switch (state) { - case ViewPager.SCROLL_STATE_IDLE: { - if (mNextPosition == -1) { - // This happens when the user drags the screen just after launching the - // application, and settle down the same screen without actually swiping it. - // At that moment mNextPosition is apparently -1 yet, and we expect it - // being updated by onPageSelected(), which is *not* called if the user - // settle down the exact same tab after the dragging. - if (DEBUG) { - Log.d(TAG, "Next position is not specified correctly. Use current tab (" - + mViewPager.getCurrentItem() + ")"); - } - mNextPosition = mViewPager.getCurrentItem(); - } - if (DEBUG) { - Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_IDLE. " - + "mCurrentPosition: " + mCurrentPosition - + ", mNextPosition: " + mNextPosition); - } - // Interpret IDLE as the end of migration (both swipe and tab click) - mDuringSwipe = false; - mUserTabClick = false; - - updateFakeMenuButtonsVisibility(mNextPosition == TAB_INDEX_DIALER); - sendFragmentVisibilityChange(mCurrentPosition, false); - sendFragmentVisibilityChange(mNextPosition, true); - - invalidateOptionsMenu(); + private NewDialpadFragment mDialpadFragment; - mCurrentPosition = mNextPosition; - break; - } - case ViewPager.SCROLL_STATE_DRAGGING: { - if (DEBUG) Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_DRAGGING"); - mDuringSwipe = true; - mUserTabClick = false; - break; - } - case ViewPager.SCROLL_STATE_SETTLING: { - if (DEBUG) Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_SETTLING"); - mDuringSwipe = true; - mUserTabClick = false; - break; - } - default: - break; - } - } - } - - private String mFilterText; + /** + * Fragment for searching phone numbers using the alphanumeric keyboard. + */ + private NewSearchFragment mRegularSearchFragment; - /** Enables horizontal swipe between Fragments. */ - private ViewPager mViewPager; - private final PageChangeListener mPageChangeListener = new PageChangeListener(); - private DialpadFragment mDialpadFragment; - private CallLogFragment mCallLogFragment; - private PhoneFavoriteFragment mPhoneFavoriteFragment; + /** + * Fragment for searching phone numbers using the dialpad. + */ + private SmartDialSearchFragment mSmartDialSearchFragment; - private View mSearchButton; private View mMenuButton; + private View mCallHistoryButton; + private View mDialpadButton; - private final ContactListFilterListener mContactListFilterListener = - new ContactListFilterListener() { - @Override - public void onContactListFilterChanged() { - boolean doInvalidateOptionsMenu = false; - - if (mPhoneFavoriteFragment != null && mPhoneFavoriteFragment.isAdded()) { - mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter()); - doInvalidateOptionsMenu = true; - } - - if (mSearchFragment != null && mSearchFragment.isAdded()) { - mSearchFragment.setFilter(mContactListFilterController.getFilter()); - doInvalidateOptionsMenu = true; - } else { - Log.w(TAG, "Search Fragment isn't available when ContactListFilter is changed"); - } - - if (doInvalidateOptionsMenu) { - invalidateOptionsMenu(); - } - } - }; - - private final TabListener mTabListener = new TabListener() { - @Override - public void onTabUnselected(Tab tab, FragmentTransaction ft) { - if (DEBUG) Log.d(TAG, "onTabUnselected(). tab: " + tab); - } - - @Override - public void onTabSelected(Tab tab, FragmentTransaction ft) { - if (DEBUG) { - Log.d(TAG, "onTabSelected(). tab: " + tab + ", mDuringSwipe: " + mDuringSwipe); - } - // When the user swipes the screen horizontally, this method will be called after - // ViewPager.SCROLL_STATE_DRAGGING and ViewPager.SCROLL_STATE_SETTLING events, while - // when the user clicks a tab at the ActionBar at the top, this will be called before - // them. This logic interprets the order difference as a difference of the user action. - if (!mDuringSwipe) { - if (DEBUG) { - Log.d(TAG, "Tab select. from: " + mPageChangeListener.getCurrentPosition() - + ", to: " + tab.getPosition()); - } - if (mDialpadFragment != null) { - updateFakeMenuButtonsVisibility(tab.getPosition() == TAB_INDEX_DIALER); - } - mUserTabClick = true; - } - - if (mViewPager.getCurrentItem() != tab.getPosition()) { - mViewPager.setCurrentItem(tab.getPosition(), true); - } - - // During the call, we don't remember the tab position. - if (mDialpadFragment == null || !mDialpadFragment.phoneIsInUse()) { - // Remember this tab index. This function is also called, if the tab is set - // automatically in which case the setter (setCurrentTab) has to set this to its old - // value afterwards - mLastManuallySelectedFragment = tab.getPosition(); - } - } - - @Override - public void onTabReselected(Tab tab, FragmentTransaction ft) { - if (DEBUG) Log.d(TAG, "onTabReselected"); - } - }; + // Padding view used to shift the fragments up when the dialpad is shown. + private View mBottomPaddingView; /** - * Fragment for searching phone numbers. Unlike the other Fragments, this doesn't correspond - * to tab but is shown by a search action. - */ - private PhoneNumberPickerFragment mSearchFragment; - /** * True when this Activity is in its search UI (with a {@link SearchView} and * {@link PhoneNumberPickerFragment}). */ private boolean mInSearchUi; private SearchView mSearchView; - private final OnClickListener mFilterOptionClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - final PopupMenu popupMenu = new PopupMenu(NewDialtactsActivity.this, view); - final Menu menu = popupMenu.getMenu(); - popupMenu.inflate(R.menu.dialtacts_search_options); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - filterOptionMenuItem.setOnMenuItemClickListener(mFilterOptionsMenuItemClickListener); - final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact); - addContactOptionMenuItem.setIntent( - new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); - popupMenu.show(); - } - }; - /** * The index of the Fragment (or, the tab) that has last been manually selected. * This value does not keep track of programmatically set Tabs (e.g. Call Log after a Call) */ private int mLastManuallySelectedFragment; - private ContactListFilterController mContactListFilterController; - private OnMenuItemClickListener mFilterOptionsMenuItemClickListener = - new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - AccountFilterUtil.startAccountFilterActivityForResult( - NewDialtactsActivity.this, SUBACTIVITY_ACCOUNT_FILTER, - mContactListFilterController.getFilter()); - return true; - } - }; - - private OnMenuItemClickListener mSearchMenuItemClickListener = - new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - enterSearchUi(); - return true; - } - }; - /** * Listener used when one of phone numbers in search UI is selected. This will initiate a * phone call using the phone number. @@ -410,7 +164,7 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie // Specify call-origin so that users will see the previous tab instead of // CallLog screen (search UI will be automatically exited). PhoneNumberInteraction.startInteractionForPhoneCall( - NewDialtactsActivity.this, dataUri, getCallOrigin()); + NewDialtactsActivity.this, dataUri, getCallOrigin()); } @Override @@ -441,14 +195,29 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie @Override public boolean onQueryTextChange(String newText) { + final boolean smartDialSearch = isDialpadShowing(); + // Show search result with non-empty text. Show a bare list otherwise. - if (mSearchFragment != null) { - mSearchFragment.setQueryString(newText, true); + if (TextUtils.isEmpty(newText) && mInSearchUi) { + exitSearchUi(); + return true; + } else if (!TextUtils.isEmpty(newText) && !mInSearchUi) { + enterSearchUi(smartDialSearch); + } + + if (isDialpadShowing()) { + mSmartDialSearchFragment.setQueryString(newText, false); + } else { + mRegularSearchFragment.setQueryString(newText, false); } return true; } }; + private boolean isDialpadShowing() { + return mDialpadFragment.isVisible(); + } + /** * Listener used to handle the "close" button on the right side of {@link SearchView}. * If some text is in the search view, this will clean it up. Otherwise this will exit @@ -467,135 +236,132 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } }; - private final View.OnLayoutChangeListener mFirstLayoutListener - = new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - v.removeOnLayoutChangeListener(this); // Unregister self. - addSearchFragment(); - } - }; - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); final Intent intent = getIntent(); fixIntent(intent); - setContentView(R.layout.dialtacts_activity); - - mContactListFilterController = ContactListFilterController.getInstance(this); - mContactListFilterController.addListener(mContactListFilterListener); + setContentView(R.layout.new_dialtacts_activity); - findViewById(R.id.dialtacts_frame).addOnLayoutChangeListener(mFirstLayoutListener); + getActionBar().hide(); - mViewPager = (ViewPager) findViewById(R.id.pager); - mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager())); - mViewPager.setOnPageChangeListener(mPageChangeListener); - mViewPager.setOffscreenPageLimit(2); + mPhoneFavoriteFragment = new NewPhoneFavoriteFragment(); + mPhoneFavoriteFragment.setListener(mPhoneFavoriteListener); - // Do same width calculation as ActionBar does - DisplayMetrics dm = getResources().getDisplayMetrics(); - int minCellSize = getResources().getDimensionPixelSize(R.dimen.fake_menu_button_min_width); - int cellCount = dm.widthPixels / minCellSize; - int fakeMenuItemWidth = dm.widthPixels / cellCount; - if (DEBUG) Log.d(TAG, "The size of fake menu buttons (in pixel): " + fakeMenuItemWidth); + mRegularSearchFragment = new NewSearchFragment(); + mSmartDialSearchFragment = new SmartDialSearchFragment(); + mDialpadFragment = new NewDialpadFragment(); - // Soft menu button should appear only when there's no hardware menu button. - mMenuButton = findViewById(R.id.overflow_menu); - if (mMenuButton != null) { - mMenuButton.setMinimumWidth(fakeMenuItemWidth); - if (ViewConfiguration.get(this).hasPermanentMenuKey()) { - // This is required for dialpad button's layout, so must not use GONE here. - mMenuButton.setVisibility(View.INVISIBLE); - } else { - mMenuButton.setOnClickListener(this); - } - } - mSearchButton = findViewById(R.id.searchButton); - if (mSearchButton != null) { - mSearchButton.setMinimumWidth(fakeMenuItemWidth); - mSearchButton.setOnClickListener(this); - } - - // Setup the ActionBar tabs (the order matches the tab-index contants TAB_INDEX_*) - setupDialer(); - setupCallLog(); - setupFavorites(); - getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); - getActionBar().setDisplayShowTitleEnabled(false); - getActionBar().setDisplayShowHomeEnabled(false); + // TODO krelease: load fragments on demand instead of creating all of them at run time + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.add(R.id.dialtacts_frame, mPhoneFavoriteFragment, TAG_FAVORITES_FRAGMENT); + ft.add(R.id.dialtacts_frame, mRegularSearchFragment, TAG_REGULAR_SEARCH_FRAGMENT); + ft.add(R.id.dialtacts_frame, mSmartDialSearchFragment, TAG_SMARTDIAL_SEARCH_FRAGMENT); + ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); + ft.hide(mRegularSearchFragment); + ft.hide(mDialpadFragment); + ft.hide(mSmartDialSearchFragment); + ft.commit(); + + mBottomPaddingView = findViewById(R.id.dialtacts_bottom_padding); + prepareSearchView(); // Load the last manually loaded tab mPrefs = this.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - mLastManuallySelectedFragment = mPrefs.getInt(PREF_LAST_MANUALLY_SELECTED_TAB, - PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT); - if (mLastManuallySelectedFragment >= TAB_INDEX_COUNT) { - // Stored value may have exceeded the number of current tabs. Reset it. - mLastManuallySelectedFragment = PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT; - } - setCurrentTab(intent); + /* + * TODO krelease : Remember which fragment was last displayed, and then redisplay it as + * necessary. mLastManuallySelectedFragment = mPrefs.getInt(PREF_LAST_MANUALLY_SELECTED_TAB, + * PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT); if (mLastManuallySelectedFragment >= + * TAB_INDEX_COUNT) { // Stored value may have exceeded the number of current tabs. Reset + * it. mLastManuallySelectedFragment = PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT; } + */ if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction()) - && icicle == null) { + && savedInstanceState == null) { setupFilterText(intent); } } @Override - public void onStart() { - super.onStart(); - if (mPhoneFavoriteFragment != null) { - mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter()); - } - if (mSearchFragment != null) { - mSearchFragment.setFilter(mContactListFilterController.getFilter()); - } - - if (mDuringSwipe || mUserTabClick) { - if (DEBUG) Log.d(TAG, "reset buggy flag state.."); - mDuringSwipe = false; - mUserTabClick = false; + protected void onResume() { + super.onResume(); + final FragmentManager fm = getFragmentManager(); + mPhoneFavoriteFragment = (NewPhoneFavoriteFragment) fm.findFragmentByTag( + TAG_FAVORITES_FRAGMENT); + mDialpadFragment = (NewDialpadFragment) fm.findFragmentByTag(TAG_DIALPAD_FRAGMENT); + + mRegularSearchFragment = (NewSearchFragment) fm.findFragmentByTag( + TAG_REGULAR_SEARCH_FRAGMENT); + mRegularSearchFragment.setOnPhoneNumberPickerActionListener( + mPhoneNumberPickerActionListener); + if (!mRegularSearchFragment.isHidden()) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.hide(mRegularSearchFragment); + transaction.commit(); } - final int currentPosition = mPageChangeListener.getCurrentPosition(); - if (DEBUG) { - Log.d(TAG, "onStart(). current position: " + mPageChangeListener.getCurrentPosition() - + ". Reset all menu visibility state."); - } - updateFakeMenuButtonsVisibility(currentPosition == TAB_INDEX_DIALER && !mInSearchUi); - for (int i = 0; i < TAB_INDEX_COUNT; i++) { - sendFragmentVisibilityChange(i, i == currentPosition); + mSmartDialSearchFragment = (SmartDialSearchFragment) fm.findFragmentByTag( + TAG_SMARTDIAL_SEARCH_FRAGMENT); + mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener( + mPhoneNumberPickerActionListener); + if (!mSmartDialSearchFragment.isHidden()) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.hide(mSmartDialSearchFragment); + transaction.commit(); } } @Override - public void onDestroy() { - super.onDestroy(); - mContactListFilterController.removeListener(mContactListFilterListener); + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_import_export: + // We hard-code the "contactsAreAvailable" argument because doing it properly would + // involve querying a {@link ProviderStatusLoader}, which we don't want to do right + // now in Dialtacts for (potential) performance reasons. Compare with how it is + // done in {@link PeopleActivity}. + ImportExportDialogFragment.show(getFragmentManager(), true, + DialtactsActivity.class); + return true; + case R.id.menu_clear_frequents: + ClearFrequentsDialog.show(getFragmentManager()); + return true; + case R.id.add_contact: + try { + startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); + } catch (ActivityNotFoundException e) { + Toast toast = Toast.makeText(this, R.string.add_contact_not_available, + Toast.LENGTH_SHORT); + toast.show(); + } + return true; + case R.id.menu_call_settings: + final Intent settingsIntent = DialtactsActivity.getCallSettingsIntent(); + startActivity(settingsIntent); + } + return false; } @Override public void onClick(View view) { switch (view.getId()) { - case R.id.searchButton: { - enterSearchUi(); - break; - } case R.id.overflow_menu: { - if (mDialpadFragment != null) { - PopupMenu popup = mDialpadFragment.constructPopupMenu(view); - if (popup != null) { - popup.show(); - } - } else { - Log.w(TAG, "DialpadFragment is null during onClick() event for " + view); - } + final PopupMenu popupMenu = new PopupMenu(NewDialtactsActivity.this, view); + final Menu menu = popupMenu.getMenu(); + popupMenu.inflate(R.menu.dialtacts_options_new); + popupMenu.setOnMenuItemClickListener(this); + popupMenu.show(); break; } + case R.id.dialpad_button: + showDialpadFragment(); + break; + case R.id.call_history_button: + final Intent intent = new Intent(this, NewCallLogActivity.class); + startActivity(intent); + break; default: { Log.wtf(TAG, "Unexpected onClick event from " + view); break; @@ -603,33 +369,22 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } } - /** - * Add search fragment. Note this is called during onLayout, so there's some restrictions, - * such as executePendingTransaction can't be used in it. - */ - private void addSearchFragment() { - // In order to take full advantage of "fragment deferred start", we need to create the - // search fragment after all other fragments are created. - // The other fragments are created by the ViewPager on the first onMeasure(). - // We use the first onLayout call, which is after onMeasure(). - - // Just return if the fragment is already created, which happens after configuration - // changes. - if (mSearchFragment != null) return; - + private void showDialpadFragment() { final FragmentTransaction ft = getFragmentManager().beginTransaction(); - final Fragment searchFragment = new PhoneNumberPickerFragment(); + ft.setCustomAnimations(R.anim.slide_in, 0); + ft.show(mDialpadFragment); + ft.commit(); + } - searchFragment.setUserVisibleHint(false); - ft.add(R.id.dialtacts_frame, searchFragment); - ft.hide(searchFragment); - ft.commitAllowingStateLoss(); + private void hideDialpadFragment() { + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.setCustomAnimations(0, R.anim.slide_out); + ft.hide(mDialpadFragment); + ft.commit(); } private void prepareSearchView() { - final View searchViewLayout = - getLayoutInflater().inflate(R.layout.dialtacts_custom_action_bar, null); - mSearchView = (SearchView) searchViewLayout.findViewById(R.id.search_view); + mSearchView = (SearchView) findViewById(R.id.search_view); mSearchView.setOnQueryTextListener(mPhoneSearchQueryTextListener); mSearchView.setOnCloseListener(mPhoneSearchCloseListener); // Since we're using a custom layout for showing SearchView instead of letting the @@ -639,8 +394,9 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie // - it should not be iconified at this time // See also comments for onActionViewExpanded()/onActionViewCollapsed() mSearchView.setIconifiedByDefault(true); - mSearchView.setQueryHint(getString(R.string.hint_findContacts)); + mSearchView.setQueryHint(getString(R.string.dialer_hint_find_contact)); mSearchView.setIconified(false); + mSearchView.clearFocus(); mSearchView.setOnQueryTextFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean hasFocus) { @@ -649,69 +405,87 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } } }); + } + + private void hideDialpadFragmentIfNecessary() { + if (mDialpadFragment.isVisible()) { + hideDialpadFragment(); + } + } - if (!ViewConfiguration.get(this).hasPermanentMenuKey()) { - // Filter option menu should be shown on the right side of SearchView. - final View filterOptionView = searchViewLayout.findViewById(R.id.search_option); - filterOptionView.setVisibility(View.VISIBLE); - filterOptionView.setOnClickListener(mFilterOptionClickListener); + final AnimatorListener mHideListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mSearchView.setVisibility(View.GONE); } + }; + + public void hideSearchBar() { + mSearchView.animate().cancel(); + mSearchView.setAlpha(1); + mSearchView.setTranslationY(0); + mSearchView.animate().withLayer().alpha(0).translationY(-mSearchView.getHeight()). + setDuration(200).setListener(mHideListener); + + mPhoneFavoriteFragment.getView().animate().withLayer() + .translationY(-mSearchView.getHeight()).setDuration(200).setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBottomPaddingView.setVisibility(View.VISIBLE); + mPhoneFavoriteFragment.getView().setTranslationY(0); + } + }); + } + + public void showSearchBar() { + mSearchView.animate().cancel(); + mSearchView.setAlpha(0); + mSearchView.setTranslationY(-mSearchView.getHeight()); + mSearchView.animate().withLayer().alpha(1).translationY(0).setDuration(200) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mSearchView.setVisibility(View.VISIBLE); + } + }); - getActionBar().setCustomView(searchViewLayout, - new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + mPhoneFavoriteFragment.getView().setTranslationY(-mSearchView.getHeight()); + mPhoneFavoriteFragment.getView().animate().withLayer().translationY(0).setDuration(200) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mBottomPaddingView.setVisibility(View.GONE); + } + }); } - @Override - public void onAttachFragment(Fragment fragment) { - // This method can be called before onCreate(), at which point we cannot rely on ViewPager. - // In that case, we will setup the "current position" soon after the ViewPager is ready. - final int currentPosition = mViewPager != null ? mViewPager.getCurrentItem() : -1; - - if (fragment instanceof DialpadFragment) { - mDialpadFragment = (DialpadFragment) fragment; - } else if (fragment instanceof CallLogFragment) { - mCallLogFragment = (CallLogFragment) fragment; - } else if (fragment instanceof PhoneFavoriteFragment) { - mPhoneFavoriteFragment = (PhoneFavoriteFragment) fragment; - mPhoneFavoriteFragment.setListener(mPhoneFavoriteListener); - if (mContactListFilterController != null - && mContactListFilterController.getFilter() != null) { - mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter()); - } - } else if (fragment instanceof PhoneNumberPickerFragment) { - mSearchFragment = (PhoneNumberPickerFragment) fragment; - mSearchFragment.setOnPhoneNumberPickerActionListener(mPhoneNumberPickerActionListener); - mSearchFragment.setQuickContactEnabled(true); - mSearchFragment.setDarkTheme(true); - mSearchFragment.setPhotoPosition(ContactListItemView.getDefaultPhotoPosition( - true /* opposite */)); - mSearchFragment.setUseCallableUri(true); - if (mContactListFilterController != null - && mContactListFilterController.getFilter() != null) { - mSearchFragment.setFilter(mContactListFilterController.getFilter()); - } - // Here we assume that we're not on the search mode, so let's hide the fragment. - // - // We get here either when the fragment is created (normal case), or after configuration - // changes. In the former case, we're not in search mode because we can only - // enter search mode if the fragment is created. (see enterSearchUi()) - // In the latter case we're not in search mode either because we don't retain - // mInSearchUi -- ideally we should but at this point it's not supported. - mSearchFragment.setUserVisibleHint(false); - // After configuration changes fragments will forget their "hidden" state, so make - // sure to hide it. - if (!mSearchFragment.isHidden()) { - final FragmentTransaction transaction = getFragmentManager().beginTransaction(); - transaction.hide(mSearchFragment); - transaction.commitAllowingStateLoss(); + + public void setupFakeActionBarItems() { + mMenuButton = findViewById(R.id.overflow_menu); + if (mMenuButton != null) { + // mMenuButton.setMinimumWidth(fakeMenuItemWidth); + if (ViewConfiguration.get(this).hasPermanentMenuKey()) { + // This is required for dialpad button's layout, so must not use GONE here. + mMenuButton.setVisibility(View.INVISIBLE); + } else { + mMenuButton.setOnClickListener(this); } } + + mCallHistoryButton = findViewById(R.id.call_history_button); + // mCallHistoryButton.setMinimumWidth(fakeMenuItemWidth); + mCallHistoryButton.setOnClickListener(this); + + mDialpadButton = findViewById(R.id.dialpad_button); + // DialpadButton.setMinimumWidth(fakeMenuItemWidth); + mDialpadButton.setOnClickListener(this); } @Override protected void onPause() { super.onPause(); - mPrefs.edit().putInt(PREF_LAST_MANUALLY_SELECTED_TAB, mLastManuallySelectedFragment) .apply(); requestBackup(); @@ -733,30 +507,6 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } } - private void setupDialer() { - final Tab tab = getActionBar().newTab(); - tab.setContentDescription(R.string.dialerIconLabel); - tab.setTabListener(mTabListener); - tab.setIcon(R.drawable.ic_tab_dialer); - getActionBar().addTab(tab); - } - - private void setupCallLog() { - final Tab tab = getActionBar().newTab(); - tab.setContentDescription(R.string.recentCallsIconLabel); - tab.setIcon(R.drawable.ic_tab_recent); - tab.setTabListener(mTabListener); - getActionBar().addTab(tab); - } - - private void setupFavorites() { - final Tab tab = getActionBar().newTab(); - tab.setContentDescription(R.string.contactsFavoritesLabel); - tab.setIcon(R.drawable.ic_tab_all); - tab.setTabListener(mTabListener); - getActionBar().addTab(tab); - } - /** * Returns true if the intent is due to hitting the green send key (hardware call button: * KEYCODE_CALL) while in a call. @@ -765,8 +515,7 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie * @param recentCallsRequest true if the intent is requesting to view recent calls * @return true if the intent is due to hitting the green send key while in a call */ - private boolean isSendKeyWhileInCall(final Intent intent, - final boolean recentCallsRequest) { + private boolean isSendKeyWhileInCall(Intent intent, boolean recentCallsRequest) { // If there is a call in progress go to the call screen if (recentCallsRequest) { final boolean callKey = intent.getBooleanExtra("call_key", false); @@ -789,7 +538,10 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie * * @param intent Intent that contains information about which tab should be selected */ - private void setCurrentTab(Intent intent) { + private void displayFragment(Intent intent) { + // TODO krelease: Make navigation via intent work by displaying the correct fragment + // as appropriate. + // If we got here by hitting send and we're in call forward along to the in-call activity boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.resolveType( getContentResolver())); @@ -797,61 +549,30 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie finish(); return; } - - // Remember the old manually selected tab index so that it can be restored if it is - // overwritten by one of the programmatic tab selections - final int savedTabIndex = mLastManuallySelectedFragment; - - final int tabIndex; - if ((mDialpadFragment != null && mDialpadFragment.phoneIsInUse()) - || isDialIntent(intent)) { - tabIndex = TAB_INDEX_DIALER; - } else if (recentCallsRequest) { - tabIndex = TAB_INDEX_CALL_LOG; - } else { - tabIndex = mLastManuallySelectedFragment; - } - - final int previousItemIndex = mViewPager.getCurrentItem(); - mViewPager.setCurrentItem(tabIndex, false /* smoothScroll */); - if (previousItemIndex != tabIndex) { - sendFragmentVisibilityChange(previousItemIndex, false /* not visible */ ); - } - mPageChangeListener.setCurrentPosition(tabIndex); - sendFragmentVisibilityChange(tabIndex, true /* visible */ ); - - // Restore to the previous manual selection - mLastManuallySelectedFragment = savedTabIndex; - mDuringSwipe = false; - mUserTabClick = false; } @Override public void onNewIntent(Intent newIntent) { setIntent(newIntent); fixIntent(newIntent); - setCurrentTab(newIntent); + displayFragment(newIntent); final String action = newIntent.getAction(); if (UI.FILTER_CONTACTS_ACTION.equals(action)) { setupFilterText(newIntent); } - if (mInSearchUi || (mSearchFragment != null && mSearchFragment.isVisible())) { + if (mInSearchUi || (mRegularSearchFragment != null && mRegularSearchFragment.isVisible())) { exitSearchUi(); } - if (mViewPager.getCurrentItem() == TAB_INDEX_DIALER) { - if (mDialpadFragment != null) { - mDialpadFragment.setStartedFromNewIntent(true); - } else { - Log.e(TAG, "DialpadFragment isn't ready yet when the tab is already selected."); - } - } else if (mViewPager.getCurrentItem() == TAB_INDEX_CALL_LOG) { - if (mCallLogFragment != null) { - mCallLogFragment.configureScreenFromIntent(newIntent); - } else { - Log.e(TAG, "CallLogFragment isn't ready yet when the tab is already selected."); - } - } + // TODO krelease: Handle onNewIntent for all other fragments + /* + *if (mViewPager.getCurrentItem() == TAB_INDEX_DIALER) { if (mDialpadFragment != null) { + * mDialpadFragment.setStartedFromNewIntent(true); } else { Log.e(TAG, + * "DialpadFragment isn't ready yet when the tab is already selected."); } } else if + * (mViewPager.getCurrentItem() == TAB_INDEX_CALL_LOG) { if (mCallLogFragment != null) { + * mCallLogFragment.configureScreenFromIntent(newIntent); } else { Log.e(TAG, + * "CallLogFragment isn't ready yet when the tab is already selected."); } } + */ invalidateOptionsMenu(); } @@ -911,28 +632,12 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } } - @Override - public void onBackPressed() { - if (mInSearchUi) { - // We should let the user go back to usual screens with tabs. - exitSearchUi(); - } else if (isTaskRoot()) { - // Instead of stopping, simply push this to the back of the stack. - // This is only done when running at the top of the stack; - // otherwise, we have been launched by someone else so need to - // allow the user to go back to the caller. - moveTaskToBack(false); - } else { - super.onBackPressed(); - } - } - - private final PhoneFavoriteFragment.Listener mPhoneFavoriteListener = - new PhoneFavoriteFragment.Listener() { + private final NewPhoneFavoriteFragment.Listener mPhoneFavoriteListener = + new NewPhoneFavoriteFragment.Listener() { @Override public void onContactSelected(Uri contactUri) { PhoneNumberInteraction.startInteractionForPhoneCall( - NewDialtactsActivity.this, contactUri, getCallOrigin()); + NewDialtactsActivity.this, contactUri, getCallOrigin()); } @Override @@ -942,151 +647,14 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } }; - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.dialtacts_options, menu); - - // set up intents and onClick listeners - final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings); - final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - - callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent()); - searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener); - filterOptionMenuItem.setOnMenuItemClickListener(mFilterOptionsMenuItemClickListener); - - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - if (mInSearchUi) { - prepareOptionsMenuInSearchMode(menu); - } else { - // get reference to the currently selected tab - final Tab tab = getActionBar().getSelectedTab(); - if (tab != null) { - switch(tab.getPosition()) { - case TAB_INDEX_DIALER: - prepareOptionsMenuForDialerTab(menu); - break; - case TAB_INDEX_CALL_LOG: - prepareOptionsMenuForCallLogTab(menu); - break; - case TAB_INDEX_FAVORITES: - prepareOptionsMenuForFavoritesTab(menu); - break; - } - } - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.add_contact: - try { - startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); - } catch (ActivityNotFoundException e) { - Toast toast = Toast.makeText(this, R.string.add_contact_not_available, - Toast.LENGTH_SHORT); - toast.show(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - private void prepareOptionsMenuInSearchMode(Menu menu) { - // get references to menu items - final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact); - final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings); - final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item); - - // prepare the menu items - searchMenuItem.setVisible(false); - filterOptionMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey()); - addContactOptionMenuItem.setVisible(false); - callSettingsMenuItem.setVisible(false); - emptyRightMenuItem.setVisible(false); - } - - private void prepareOptionsMenuForDialerTab(Menu menu) { - if (DEBUG) { - Log.d(TAG, "onPrepareOptionsMenu(dialer). swipe: " + mDuringSwipe - + ", user tab click: " + mUserTabClick); - } - - // get references to menu items - final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact); - final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings); - final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item); - - // prepare the menu items - filterOptionMenuItem.setVisible(false); - addContactOptionMenuItem.setVisible(false); - if (mDuringSwipe || mUserTabClick) { - // During horizontal movement, the real ActionBar menu items are shown - searchMenuItem.setVisible(true); - callSettingsMenuItem.setVisible(true); - // When there is a permanent menu key, there is no overflow icon on the right of - // the action bar which would force the search menu item (if it is visible) to the - // left. This is the purpose of showing the emptyRightMenuItem. - emptyRightMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey()); - } else { - // This is when the user is looking at the dialer pad. In this case, the real - // ActionBar is hidden and fake menu items are shown. - // Except in landscape, in which case the real search menu item is shown. - searchMenuItem.setVisible(OrientationUtil.isLandscape(this)); - // If a permanent menu key is available, then we need to show the call settings item - // so that the call settings item can be invoked by the permanent menu key. - callSettingsMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey()); - emptyRightMenuItem.setVisible(false); - } - } - - private void prepareOptionsMenuForCallLogTab(Menu menu) { - // get references to menu items - final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact); - final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings); - final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item); - - // prepare the menu items - searchMenuItem.setVisible(true); - filterOptionMenuItem.setVisible(false); - addContactOptionMenuItem.setVisible(false); - callSettingsMenuItem.setVisible(true); - emptyRightMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey()); - } - - private void prepareOptionsMenuForFavoritesTab(Menu menu) { - // get references to menu items - final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar); - final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option); - final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact); - final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings); - final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item); - - // prepare the menu items - searchMenuItem.setVisible(true); - filterOptionMenuItem.setVisible(true); - addContactOptionMenuItem.setVisible(true); - callSettingsMenuItem.setVisible(true); - emptyRightMenuItem.setVisible(false); - } + /* TODO krelease: This is only relevant for phones that have a hard button search key (i.e. + * Nexus S). Supporting it is a little more tricky because of the dialpad fragment might + * be showing when the search key is pressed so there is more state management involved. @Override public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { - if (mSearchFragment != null && mSearchFragment.isAdded() && !globalSearch) { + if (mRegularSearchFragment != null && mRegularSearchFragment.isAdded() && !globalSearch) { if (mInSearchUi) { if (mSearchView.hasFocus()) { showInputMethod(mSearchView.findFocus()); @@ -1099,173 +667,52 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } else { super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); } - } - - /** - * Hides every tab and shows search UI for phone lookup. - */ - private void enterSearchUi() { - if (mSearchFragment == null) { - // We add the search fragment dynamically in the first onLayoutChange() and - // mSearchFragment is set sometime later when the fragment transaction is actually - // executed, which means there's a window when users are able to hit the (physical) - // search key but mSearchFragment is still null. - // It's quite hard to handle this case right, so let's just ignore the search key - // in this case. Users can just hit it again and it will work this time. - return; - } - if (mSearchView == null) { - prepareSearchView(); - } - - final ActionBar actionBar = getActionBar(); - - final Tab tab = actionBar.getSelectedTab(); - - // User can search during the call, but we don't want to remember the status. - if (tab != null && (mDialpadFragment == null || - !mDialpadFragment.phoneIsInUse())) { - mLastManuallySelectedFragment = tab.getPosition(); - } - - mSearchView.setQuery(null, true); - - actionBar.setDisplayShowCustomEnabled(true); - actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); - actionBar.setDisplayShowHomeEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - - updateFakeMenuButtonsVisibility(false); - - for (int i = 0; i < TAB_INDEX_COUNT; i++) { - sendFragmentVisibilityChange(i, false /* not visible */ ); - } - - // Show the search fragment and hide everything else. - mSearchFragment.setUserVisibleHint(true); - final FragmentTransaction transaction = getFragmentManager().beginTransaction(); - transaction.show(mSearchFragment); - transaction.commitAllowingStateLoss(); - mViewPager.setVisibility(View.GONE); - - // We need to call this and onActionViewCollapsed() manually, since we are using a custom - // layout instead of asking the search menu item to take care of SearchView. - mSearchView.onActionViewExpanded(); - mInSearchUi = true; - } + }*/ private void showInputMethod(View view) { - InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager) getSystemService( + Context.INPUT_METHOD_SERVICE); if (imm != null) { - if (!imm.showSoftInput(view, 0)) { - Log.w(TAG, "Failed to show soft input method."); - } + imm.showSoftInput(view, 0); } } private void hideInputMethod(View view) { - InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager) getSystemService( + Context.INPUT_METHOD_SERVICE); if (imm != null && view != null) { imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } } /** - * Goes back to usual Phone UI with tags. Previously selected Tag and associated Fragment - * should be automatically focused again. + * Shows the search fragment */ - private void exitSearchUi() { - final ActionBar actionBar = getActionBar(); - - // Hide the search fragment, if exists. - if (mSearchFragment != null) { - mSearchFragment.setUserVisibleHint(false); - - final FragmentTransaction transaction = getFragmentManager().beginTransaction(); - transaction.hide(mSearchFragment); - transaction.commitAllowingStateLoss(); - } - - // We want to hide SearchView and show Tabs. Also focus on previously selected one. - actionBar.setDisplayShowCustomEnabled(false); - actionBar.setDisplayShowHomeEnabled(false); - actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); - - for (int i = 0; i < TAB_INDEX_COUNT; i++) { - sendFragmentVisibilityChange(i, i == mViewPager.getCurrentItem()); - } - - // Before exiting the search screen, reset swipe state. - mDuringSwipe = false; - mUserTabClick = false; - - mViewPager.setVisibility(View.VISIBLE); - - hideInputMethod(getCurrentFocus()); - - // Request to update option menu. - invalidateOptionsMenu(); - - // See comments in onActionViewExpanded() - mSearchView.onActionViewCollapsed(); - mInSearchUi = false; - } - - private Fragment getFragmentAt(int position) { - switch (position) { - case TAB_INDEX_DIALER: - return mDialpadFragment; - case TAB_INDEX_CALL_LOG: - return mCallLogFragment; - case TAB_INDEX_FAVORITES: - return mPhoneFavoriteFragment; - default: - throw new IllegalStateException("Unknown fragment index: " + position); + private void enterSearchUi(boolean smartDialSearch) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + transaction.hide(mPhoneFavoriteFragment); + if (smartDialSearch) { + transaction.show(mSmartDialSearchFragment); + } else { + transaction.show(mRegularSearchFragment); } - } + transaction.commit(); - private void sendFragmentVisibilityChange(int position, boolean visibility) { - if (DEBUG) { - Log.d(TAG, "sendFragmentVisibiltyChange(). position: " + position - + ", visibility: " + visibility); - } - // Position can be -1 initially. See PageChangeListener. - if (position >= 0) { - final Fragment fragment = getFragmentAt(position); - if (fragment != null) { - fragment.setMenuVisibility(visibility); - fragment.setUserVisibleHint(visibility); - } - } + mInSearchUi = true; } /** - * Update visibility of the search button and menu button at the bottom. - * They should be invisible when bottom ActionBar's real items are available, and be visible - * otherwise. - * - * @param visible True when visible. + * Hides the search fragment */ - private void updateFakeMenuButtonsVisibility(boolean visible) { - // Note: Landscape mode does not have the fake menu and search buttons. - if (DEBUG) { - Log.d(TAG, "updateFakeMenuButtonVisibility(" + visible + ")"); - } - - if (mSearchButton != null) { - if (visible) { - mSearchButton.setVisibility(View.VISIBLE); - } else { - mSearchButton.setVisibility(View.INVISIBLE); - } - } - if (mMenuButton != null) { - if (visible && !ViewConfiguration.get(this).hasPermanentMenuKey()) { - mMenuButton.setVisibility(View.VISIBLE); - } else { - mMenuButton.setVisibility(View.INVISIBLE); - } - } + private void exitSearchUi() { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + transaction.hide(mRegularSearchFragment); + transaction.hide(mSmartDialSearchFragment); + transaction.show(mPhoneFavoriteFragment); + transaction.commit(); + mInSearchUi = false; } /** Returns an Intent to launch Call Settings screen */ @@ -1277,16 +724,41 @@ public class NewDialtactsActivity extends TransactionSafeActivity implements Vie } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; + public void onBackPressed() { + if (mDialpadFragment.isVisible()) { + hideDialpadFragment(); + } else if (mInSearchUi) { + mSearchView.setQuery(null, false); + } else if (isTaskRoot()) { + // Instead of stopping, simply push this to the back of the stack. + // This is only done when running at the top of the stack; + // otherwise, we have been launched by someone else so need to + // allow the user to go back to the caller. + moveTaskToBack(false); + } else { + super.onBackPressed(); } - switch (requestCode) { - case SUBACTIVITY_ACCOUNT_FILTER: { - AccountFilterUtil.handleAccountFilterResult( - mContactListFilterController, resultCode, data); - } - break; + } + + @Override + public void onDialpadQueryChanged(String query) { + final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, + SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); + if (!TextUtils.equals(mSearchView.getQuery(), normalizedQuery)) { + mSearchView.setQuery(normalizedQuery, false); + } + } + + @Override + public void onListFragmentScrollStateChange(int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + hideDialpadFragmentIfNecessary(); + hideInputMethod(getCurrentFocus()); } } + + @Override + public void onPhoneFavoriteFragmentStarted() { + setupFakeActionBarItems(); + } } diff --git a/src/com/android/dialer/NewSearchFragment.java b/src/com/android/dialer/NewSearchFragment.java new file mode 100644 index 000000000..c428db239 --- /dev/null +++ b/src/com/android/dialer/NewSearchFragment.java @@ -0,0 +1,75 @@ +/* + * 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; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.ListView; + +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.list.PhoneNumberPickerFragment; +import com.android.dialer.list.OnListFragmentScrolledListener; + +public class NewSearchFragment extends PhoneNumberPickerFragment { + + private OnListFragmentScrolledListener mActivityScrollListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + setQuickContactEnabled(true); + setDarkTheme(false); + setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(true /* opposite */)); + setUseCallableUri(true); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement OnListFragmentScrolledListener"); + } + } + + @Override + public void onStart() { + super.onStart(); + getListView().setOnScrollListener(new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + } + }); + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + // This hides the "All contacts with phone numbers" header in the search fragment + getAdapter().setHasHeader(0, false); + } +} diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index 46f010a15..46bb4fd28 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -517,7 +517,7 @@ import java.util.LinkedList; views.primaryActionView.setTag( IntentProvider.getCallDetailIntentProvider( - this, c.getPosition(), c.getLong(CallLogQuery.ID), count)); + getCursor(), c.getPosition(), c.getLong(CallLogQuery.ID), count)); // Store away the voicemail information so we can play it directly. if (callType == Calls.VOICEMAIL_TYPE) { String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java index fed381427..91a2e5d2b 100644 --- a/src/com/android/dialer/calllog/CallLogQueryHandler.java +++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java @@ -45,7 +45,7 @@ import java.util.List; import javax.annotation.concurrent.GuardedBy; /** Handles asynchronous queries to the call log. */ -/*package*/ class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { +public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final String TAG = "CallLogQueryHandler"; @@ -62,6 +62,8 @@ import javax.annotation.concurrent.GuardedBy; /** The token for the query to fetch voicemail status messages. */ private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58; + private final int mLogLimit; + /** * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular * type. @@ -121,10 +123,16 @@ import javax.annotation.concurrent.GuardedBy; } public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) { + this(contentResolver, listener, -1); + } + + public CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit) { super(contentResolver); mListener = new WeakReference<Listener>(listener); + mLogLimit = limit; } + /** * Fetches the list of calls from the call log for a given type. * <p> @@ -154,8 +162,9 @@ import javax.annotation.concurrent.GuardedBy; selection = String.format("(%s = ?)", Calls.TYPE); selectionArgs.add(Integer.toString(callType)); } + final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit; Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon() - .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(NUM_LOGS_TO_DISPLAY)) + .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit)) .build(); startQuery(token, requestId, uri, CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY), diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java index 1b79d6ebc..01ebf2f3e 100644 --- a/src/com/android/dialer/calllog/IntentProvider.java +++ b/src/com/android/dialer/calllog/IntentProvider.java @@ -62,11 +62,10 @@ public abstract class IntentProvider { } public static IntentProvider getCallDetailIntentProvider( - final CallLogAdapter adapter, final int position, final long id, final int groupSize) { + final Cursor cursor, final int position, final long id, final int groupSize) { return new IntentProvider() { @Override public Intent getIntent(Context context) { - Cursor cursor = adapter.getCursor(); cursor.moveToPosition(position); Intent intent = new Intent(context, CallDetailActivity.class); diff --git a/src/com/android/dialer/calllog/NewCallLogActivity.java b/src/com/android/dialer/calllog/NewCallLogActivity.java new file mode 100644 index 000000000..a3704cc98 --- /dev/null +++ b/src/com/android/dialer/calllog/NewCallLogActivity.java @@ -0,0 +1,189 @@ +/* + * 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.calllog; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.ActionBar.Tab; +import android.app.ActionBar.TabListener; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.os.Bundle; +import android.provider.CallLog.Calls; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.PagerTabStrip; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import com.android.dialer.NewDialtactsActivity; +import com.android.dialer.R; +import com.android.dialer.calllog.NewCallLogFragment; + +public class NewCallLogActivity extends Activity { + + private ViewPager mViewPager; + private ViewPagerAdapter mViewPagerAdapter; + private NewCallLogFragment mAllCallsFragment; + private NewCallLogFragment mMissedCallsFragment; + private NewCallLogFragment mVoicemailsFragment; + + private static final int TAB_INDEX_ALL = 0; + private static final int TAB_INDEX_MISSED = 1; + private static final int TAB_INDEX_VOICEMAIL = 2; + + private static final int TAB_INDEX_COUNT = 3; + + public class ViewPagerAdapter extends FragmentPagerAdapter { + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case TAB_INDEX_ALL: + mAllCallsFragment = new NewCallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL); + return mAllCallsFragment; + case TAB_INDEX_MISSED: + mMissedCallsFragment = new NewCallLogFragment(Calls.MISSED_TYPE); + return mMissedCallsFragment; + case TAB_INDEX_VOICEMAIL: + mVoicemailsFragment = new NewCallLogFragment(Calls.VOICEMAIL_TYPE); + return mVoicemailsFragment; + } + throw new IllegalStateException("No fragment at position " + position); + } + + @Override + public int getCount() { + return TAB_INDEX_COUNT; + } + } + + private final TabListener mTabListener = new TabListener() { + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + if (mViewPager != null && mViewPager.getCurrentItem() != tab.getPosition()) { + mViewPager.setCurrentItem(tab.getPosition(), true); + } + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } + }; + + private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { + + @Override + public void onPageScrolled( + int position, float positionOffset, int positionOffsetPixels) {} + + @Override + public void onPageSelected(int position) { + final ActionBar actionBar = getActionBar(); + actionBar.selectTab(actionBar.getTabAt(position)); + } + + @Override + public void onPageScrollStateChanged(int arg0) { + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.call_log_activity_new); + + final ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + + final Tab allTab = actionBar.newTab(); + final String allTitle = getString(R.string.call_log_all_title); + allTab.setContentDescription(allTitle); + allTab.setText(allTitle); + allTab.setTabListener(mTabListener); + actionBar.addTab(allTab); + + final Tab missedTab = actionBar.newTab(); + final String missedTitle = getString(R.string.call_log_missed_title); + missedTab.setContentDescription(missedTitle); + missedTab.setText(missedTitle); + missedTab.setTabListener(mTabListener); + actionBar.addTab(missedTab); + + final Tab voicemailTab = actionBar.newTab(); + final String voicemailTitle = getString(R.string.call_log_voicemail_title); + voicemailTab.setContentDescription(voicemailTitle); + voicemailTab.setText(voicemailTitle); + voicemailTab.setTabListener(mTabListener); + actionBar.addTab(voicemailTab); + + mViewPager = (ViewPager) findViewById(R.id.call_log_pager); + mViewPagerAdapter = new ViewPagerAdapter(getFragmentManager()); + mViewPager.setAdapter(mViewPagerAdapter); + mViewPager.setOnPageChangeListener(mOnPageChangeListener); + mViewPager.setOffscreenPageLimit(2); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.call_log_options_new, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); + + final NewCallLogAdapter adapter = mAllCallsFragment.getAdapter(); + // Check if all the menu items are inflated correctly. As a shortcut, we assume all + // menu items are ready if the first item is non-null. + if (itemDeleteAll != null) { + itemDeleteAll.setEnabled(adapter != null && !adapter.isEmpty()); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + final Intent intent = new Intent(this, NewDialtactsActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + case R.id.delete_all: + ClearCallLogDialog.show(getFragmentManager()); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/src/com/android/dialer/calllog/NewCallLogAdapter.java b/src/com/android/dialer/calllog/NewCallLogAdapter.java index 079919f59..c2f7c71e6 100644 --- a/src/com/android/dialer/calllog/NewCallLogAdapter.java +++ b/src/com/android/dialer/calllog/NewCallLogAdapter.java @@ -46,7 +46,7 @@ import java.util.LinkedList; /** * Adapter class to fill in data for the Call Log. */ -/*package*/ class NewCallLogAdapter extends GroupingListAdapter +public class NewCallLogAdapter extends GroupingListAdapter implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { /** Interface used to initiate a refresh of the content. */ public interface CallFetcher { @@ -166,7 +166,7 @@ import java.util.LinkedList; private QueryThread mCallerIdThread; /** Instance of helper class for managing views. */ - private final CallLogListItemHelper mCallLogViewsHelper; + private final NewCallLogListItemHelper mCallLogViewsHelper; /** Helper to set up contact photos. */ private final ContactPhotoManager mContactPhotoManager; @@ -227,7 +227,7 @@ import java.util.LinkedList; } }; - NewCallLogAdapter(Context context, CallFetcher callFetcher, + public NewCallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper) { super(context); @@ -246,7 +246,7 @@ import java.util.LinkedList; PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( resources, callTypeHelper, mPhoneNumberHelper); mCallLogViewsHelper = - new CallLogListItemHelper( + new NewCallLogListItemHelper( phoneCallDetailsHelper, mPhoneNumberHelper, resources); mCallLogGroupBuilder = new CallLogGroupBuilder(this); } @@ -259,7 +259,7 @@ import java.util.LinkedList; mCallFetcher.fetchCalls(); } - void setLoading(boolean loading) { + public void setLoading(boolean loading) { mLoading = loading; } @@ -444,7 +444,7 @@ import java.util.LinkedList; protected View newStandAloneView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + View view = inflater.inflate(R.layout.new_call_log_list_item, parent, false); findAndCacheViews(view); return view; } @@ -458,7 +458,7 @@ import java.util.LinkedList; protected View newChildView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + View view = inflater.inflate(R.layout.new_call_log_list_item, parent, false); findAndCacheViews(view); return view; } @@ -472,7 +472,7 @@ import java.util.LinkedList; protected View newGroupView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + View view = inflater.inflate(R.layout.new_call_log_list_item, parent, false); findAndCacheViews(view); return view; } @@ -515,9 +515,10 @@ import java.util.LinkedList; final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); - /*views.primaryActionView.setTag( + views.primaryActionView.setTag( IntentProvider.getCallDetailIntentProvider( - this, c.getPosition(), c.getLong(CallLogQuery.ID), count));*/ + getCursor(), c.getPosition(), c.getLong(CallLogQuery.ID), count)); + // Store away the voicemail information so we can play it directly. if (callType == Calls.VOICEMAIL_TYPE) { String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); @@ -715,7 +716,7 @@ import java.util.LinkedList; private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) { views.quickContactView.assignContactUri(contactUri); - mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, true); + mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */); } /** diff --git a/src/com/android/dialer/calllog/NewCallLogFragment.java b/src/com/android/dialer/calllog/NewCallLogFragment.java index d5b17952c..c470c55d4 100644 --- a/src/com/android/dialer/calllog/NewCallLogFragment.java +++ b/src/com/android/dialer/calllog/NewCallLogFragment.java @@ -34,7 +34,6 @@ import android.provider.ContactsContract; import android.telephony.PhoneNumberUtils; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -59,10 +58,11 @@ import com.google.common.annotations.VisibleForTesting; import java.util.List; /** - * Displays a list of call log entries. + * Displays a list of call log entries. To filter for a particular kind of call + * (all, missed or voicemails), specify it in the constructor. */ public class NewCallLogFragment extends ListFragment - implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { + implements CallLogQueryHandler.Listener, NewCallLogAdapter.CallFetcher { private static final String TAG = "CallLogFragment"; /** @@ -70,7 +70,7 @@ public class NewCallLogFragment extends ListFragment */ private static final int EMPTY_LOADER_ID = 0; - private CallLogAdapter mAdapter; + private NewCallLogAdapter mAdapter; private CallLogQueryHandler mCallLogQueryHandler; private boolean mScrollToTop; @@ -81,7 +81,6 @@ public class NewCallLogFragment extends ListFragment private View mStatusMessageView; private TextView mStatusMessageText; private TextView mStatusMessageAction; - private TextView mFilterStatusView; private KeyguardManager mKeyguardManager; private boolean mEmptyLoaderRunning; @@ -114,11 +113,30 @@ public class NewCallLogFragment extends ListFragment // Default to all calls. private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; + // Log limit - if no limit is specified, then the default in {@link NewCallLogQueryHandler} + // will be used. + private int mLogLimit = -1; + + public NewCallLogFragment() { + this(CallLogQueryHandler.CALL_TYPE_ALL, -1); + } + + public NewCallLogFragment(int filterType) { + this(filterType, -1); + } + + public NewCallLogFragment(int filterType, int logLimit) { + super(); + mCallTypeFilter = filterType; + mLogLimit = logLimit; + } + @Override public void onCreate(Bundle state) { super.onCreate(state); - mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); + mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), + this, mLogLimit); mKeyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); getActivity().getContentResolver().registerContentObserver( @@ -126,6 +144,7 @@ public class NewCallLogFragment extends ListFragment getActivity().getContentResolver().registerContentObserver( ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); setHasOptionsMenu(true); + updateCallList(mCallTypeFilter); } /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ @@ -205,20 +224,20 @@ public class NewCallLogFragment extends ListFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { - View view = inflater.inflate(R.layout.call_log_fragment, container, false); + View view = inflater.inflate(R.layout.new_call_log_fragment, container, false); mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); mStatusMessageView = view.findViewById(R.id.voicemail_status); mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); - mFilterStatusView = (TextView) view.findViewById(R.id.filter_status); return view; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + updateEmptyMessage(mCallTypeFilter); String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); - mAdapter = new CallLogAdapter(getActivity(), this, + mAdapter = new NewCallLogAdapter(getActivity(), this, new ContactInfoHelper(getActivity(), currentCountryIso)); setListAdapter(mAdapter); getListView().setItemsCanFocus(true); @@ -250,6 +269,7 @@ public class NewCallLogFragment extends ListFragment @Override public void onResume() { + Log.d(TAG, "Call Log Fragment resume"); super.onResume(); refreshData(); } @@ -320,132 +340,30 @@ public class NewCallLogFragment extends ListFragment mCallLogQueryHandler.fetchVoicemailStatus(); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.call_log_options, menu); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); - // Check if all the menu items are inflated correctly. As a shortcut, we assume all - // menu items are ready if the first item is non-null. - if (itemDeleteAll != null) { - itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty()); - - showAllFilterMenuOptions(menu); - hideCurrentFilterMenuOption(menu); - - // Only hide if not available. Let the above calls handle showing. - if (!mVoicemailSourcesAvailable) { - menu.findItem(R.id.show_voicemails_only).setVisible(false); - } + private void updateCallList(int filterType) { + if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { + unregisterPhoneCallReceiver(); + } else { + // TODO krelease: Make this work + //registerPhoneCallReceiver(); } + mCallLogQueryHandler.fetchCalls(filterType); } - private void hideCurrentFilterMenuOption(Menu menu) { - MenuItem item = null; - switch (mCallTypeFilter) { - case CallLogQueryHandler.CALL_TYPE_ALL: - item = menu.findItem(R.id.show_all_calls); - break; - case Calls.INCOMING_TYPE: - item = menu.findItem(R.id.show_incoming_only); - break; - case Calls.OUTGOING_TYPE: - item = menu.findItem(R.id.show_outgoing_only); - break; + private void updateEmptyMessage(int filterType) { + final String message; + switch (filterType) { case Calls.MISSED_TYPE: - item = menu.findItem(R.id.show_missed_only); + message = getString(R.string.recentMissed_empty); break; case Calls.VOICEMAIL_TYPE: - menu.findItem(R.id.show_voicemails_only); + message = getString(R.string.recentVoicemails_empty); break; - } - if (item != null) { - item.setVisible(false); - } - } - - private void showAllFilterMenuOptions(Menu menu) { - menu.findItem(R.id.show_all_calls).setVisible(true); - menu.findItem(R.id.show_incoming_only).setVisible(true); - menu.findItem(R.id.show_outgoing_only).setVisible(true); - menu.findItem(R.id.show_missed_only).setVisible(true); - menu.findItem(R.id.show_voicemails_only).setVisible(true); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.delete_all: - ClearCallLogDialog.show(getFragmentManager()); - return true; - - case R.id.show_outgoing_only: - // We only need the phone call receiver when there is an active call type filter. - // Not many people may use the filters so don't register the receiver until now . - registerPhoneCallReceiver(); - mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE); - updateFilterTypeAndHeader(Calls.OUTGOING_TYPE); - return true; - - case R.id.show_incoming_only: - registerPhoneCallReceiver(); - mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE); - updateFilterTypeAndHeader(Calls.INCOMING_TYPE); - return true; - - case R.id.show_missed_only: - registerPhoneCallReceiver(); - mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE); - updateFilterTypeAndHeader(Calls.MISSED_TYPE); - return true; - - case R.id.show_voicemails_only: - registerPhoneCallReceiver(); - mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE); - updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE); - return true; - - case R.id.show_all_calls: - // Filter is being turned off, receiver no longer needed. - unregisterPhoneCallReceiver(); - mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); - updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); - return true; - default: - return false; - } - } - - private void updateFilterTypeAndHeader(int filterType) { - mCallTypeFilter = filterType; - - switch (filterType) { - case CallLogQueryHandler.CALL_TYPE_ALL: - mFilterStatusView.setVisibility(View.GONE); - break; - case Calls.INCOMING_TYPE: - showFilterStatus(R.string.call_log_incoming_header); - break; - case Calls.OUTGOING_TYPE: - showFilterStatus(R.string.call_log_outgoing_header); - break; - case Calls.MISSED_TYPE: - showFilterStatus(R.string.call_log_missed_header); - break; - case Calls.VOICEMAIL_TYPE: - showFilterStatus(R.string.call_log_voicemail_header); + message = getString(R.string.recentCalls_empty); break; } - } - - private void showFilterStatus(int resId) { - mFilterStatusView.setText(resId); - mFilterStatusView.setVisibility(View.VISIBLE); + ((TextView) getListView().getEmptyView()).setText(message); } public void callSelectedEntry() { @@ -489,8 +407,7 @@ public class NewCallLogFragment extends ListFragment } } - @VisibleForTesting - CallLogAdapter getAdapter() { + NewCallLogAdapter getAdapter() { return mAdapter; } @@ -547,6 +464,8 @@ public class NewCallLogFragment extends ListFragment updateOnTransition(true); } + // TODO krelease: Figure out if we still need this. If so, it should be probably be moved to + // the call log activity instead, or done only in a single call log fragment. private void updateOnTransition(boolean onEntry) { // We don't want to update any call data when keyguard is on because the user has likely not // seen the new calls yet. @@ -570,9 +489,13 @@ public class NewCallLogFragment extends ListFragment getActivity().startService(serviceIntent); } + // TODO krelease: Make the ViewPager switch to the correct tab (All) when a phone call is + // placed. + // This should probably be moved to the call log activity. /** * Register a phone call filter to reset the call type when a phone call is place. */ + /* private void registerPhoneCallReceiver() { if (mPhoneStateListener != null) { return; // Already registered. @@ -592,13 +515,13 @@ public class NewCallLogFragment extends ListFragment if (getActivity() == null || getActivity().isFinishing()) { return; } - updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); } }); } }; mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } + */ /** * Un-registers the phone call receiver. diff --git a/src/com/android/dialer/calllog/NewCallLogListItemHelper.java b/src/com/android/dialer/calllog/NewCallLogListItemHelper.java index 371094d34..6cb80a051 100644 --- a/src/com/android/dialer/calllog/NewCallLogListItemHelper.java +++ b/src/com/android/dialer/calllog/NewCallLogListItemHelper.java @@ -27,8 +27,12 @@ import com.android.dialer.R; /** * Helper class to fill in the views of a call log entry. + * TODO krelease: The only difference between this and the original is that we don't touch + * divider views, which are not present in the new dialer. Once the new dialer replaces + * the old one, we can replace it entirely. Otherwise we would have redundant divider=null + * checks all over the place. */ -/*package*/ class NewCallLogListItemHelper { +/* package */class NewCallLogListItemHelper { /** Helper for populating the details of a phone call. */ private final PhoneCallDetailsHelper mPhoneCallDetailsHelper; /** Helper for handling phone numbers. */ @@ -67,15 +71,12 @@ import com.android.dialer.R; if (canPlay) { // Playback action takes preference. configurePlaySecondaryAction(views, isHighlighted); - views.dividerView.setVisibility(View.VISIBLE); } else if (canCall) { // Call is the secondary action. configureCallSecondaryAction(views, details); - views.dividerView.setVisibility(View.VISIBLE); } else { // No action available. views.secondaryActionView.setVisibility(View.GONE); - views.dividerView.setVisibility(View.GONE); } } diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java index fd402c9ec..a802825fc 100644 --- a/src/com/android/dialer/database/DialerDatabaseHelper.java +++ b/src/com/android/dialer/database/DialerDatabaseHelper.java @@ -31,12 +31,12 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Directory; -import com.android.contacts.common.test.NeededForTesting; import android.util.Log; import com.android.contacts.common.util.StopWatch; import com.android.dialer.dialpad.SmartDialNameMatcher; import com.android.dialer.dialpad.SmartDialPrefix; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.common.base.Preconditions; @@ -68,7 +68,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * 0-98 KeyLimePie * </pre> */ - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; private static final String SMARTDIAL_DATABASE_NAME = "dialer.db"; /** @@ -77,7 +77,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer_smartdial"; private static final String LAST_UPDATED_MILLIS = "last_updated_millis"; - private static final int MAX_ENTRIES = 3; + private static final int MAX_ENTRIES = 20; public interface Tables { /** Saves the necessary smart dial information of all contacts. */ @@ -88,10 +88,12 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { public interface SmartDialDbColumns { static final String _ID = "id"; + static final String DATA_ID = "data_id"; static final String NUMBER = "phone_number"; static final String CONTACT_ID = "contact_id"; static final String LOOKUP_KEY = "lookup_key"; static final String DISPLAY_NAME_PRIMARY = "display_name"; + static final String PHOTO_ID = "photo_id"; static final String LAST_TIME_USED = "last_time_used"; static final String TIMES_USED = "times_used"; static final String STARRED = "starred"; @@ -122,12 +124,13 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { Phone.CONTACT_ID, // 4 Phone.LOOKUP_KEY, // 5 Phone.DISPLAY_NAME_PRIMARY, // 6 - Data.LAST_TIME_USED, // 7 - Data.TIMES_USED, // 8 - Contacts.STARRED, // 9 - Data.IS_SUPER_PRIMARY, // 10 - Contacts.IN_VISIBLE_GROUP, // 11 - Data.IS_PRIMARY, // 12 + Phone.PHOTO_ID, // 7 + Data.LAST_TIME_USED, // 8 + Data.TIMES_USED, // 9 + Contacts.STARRED, // 10 + Data.IS_SUPER_PRIMARY, // 11 + Contacts.IN_VISIBLE_GROUP, // 12 + Data.IS_PRIMARY, // 13 }; static final int PHONE_ID = 0; @@ -137,12 +140,13 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { static final int PHONE_CONTACT_ID = 4; static final int PHONE_LOOKUP_KEY = 5; static final int PHONE_DISPLAY_NAME = 6; - static final int PHONE_LAST_TIME_USED = 7; - static final int PHONE_TIMES_USED = 8; - static final int PHONE_STARRED = 9; - static final int PHONE_IS_SUPER_PRIMARY = 10; - static final int PHONE_IN_VISIBLE_GROUP = 11; - static final int PHONE_IS_PRIMARY = 12; + static final int PHONE_PHOTO_ID = 7; + static final int PHONE_LAST_TIME_USED = 8; + static final int PHONE_TIMES_USED = 9; + static final int PHONE_STARRED = 10; + static final int PHONE_IS_SUPER_PRIMARY = 11; + static final int PHONE_IN_VISIBLE_GROUP = 12; + static final int PHONE_IS_PRIMARY = 13; /** Selects only rows that have been updated after a certain time stamp.*/ static final String SELECT_UPDATED_CLAUSE = @@ -210,21 +214,26 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * smart dial interface. */ public static class ContactNumber { - public final String displayName; - public final String lookupKey; public final long id; + public final long dataId; + public final String displayName; public final String phoneNumber; + public final String lookupKey; + public final long photoId; - public ContactNumber(long id, String displayName, String phoneNumber, String lookupKey) { - this.displayName = displayName; - this.lookupKey = lookupKey; + public ContactNumber(long id, long dataID, String displayName, String phoneNumber, + String lookupKey, long photoId) { + this.dataId = dataID; this.id = id; + this.displayName = displayName; this.phoneNumber = phoneNumber; + this.lookupKey = lookupKey; + this.photoId = photoId; } @Override public int hashCode() { - return Objects.hashCode(displayName, id, lookupKey, phoneNumber); + return Objects.hashCode(id, dataId, displayName, phoneNumber, lookupKey, photoId); } @Override @@ -234,10 +243,12 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { } if (object instanceof ContactNumber) { final ContactNumber that = (ContactNumber) object; - return Objects.equal(this.displayName, that.displayName) - && Objects.equal(this.id, that.id) + return Objects.equal(this.id, that.id) + && Objects.equal(this.dataId, that.dataId) + && Objects.equal(this.displayName, that.displayName) + && Objects.equal(this.phoneNumber, that.phoneNumber) && Objects.equal(this.lookupKey, that.lookupKey) - && Objects.equal(this.phoneNumber, that.phoneNumber); + && Objects.equal(this.photoId, that.photoId); } return false; } @@ -290,7 +301,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { /** * Returns a new instance for unit tests. The database will be created in memory. */ - @NeededForTesting + @VisibleForTesting static DialerDatabaseHelper getNewInstanceForTest(Context context) { return new DialerDatabaseHelper(context, null); } @@ -309,10 +320,12 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + SmartDialDbColumns.DATA_ID + " INTEGER, " + SmartDialDbColumns.NUMBER + " TEXT," + SmartDialDbColumns.CONTACT_ID + " INTEGER," + SmartDialDbColumns.LOOKUP_KEY + " TEXT," + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " + + SmartDialDbColumns.PHOTO_ID + " INTEGER, " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " + SmartDialDbColumns.LAST_TIME_USED + " LONG, " + SmartDialDbColumns.TIMES_USED + " INTEGER, " + @@ -329,6 +342,21 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { ");"); } + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, oldVersion + " to " + newVersion + ", rebuilding table"); + + final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences( + DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE); + final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit(); + editor.putLong(LAST_UPDATED_MILLIS, 0); + editor.commit(); + + db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE); + onCreate(db); + } + /** * Starts the database upgrade process in the background. */ @@ -468,10 +496,12 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { db.beginTransaction(); try { final String sqlInsert = "INSERT INTO " + Tables.SMARTDIAL_TABLE + " (" + + SmartDialDbColumns.DATA_ID + ", " + SmartDialDbColumns.NUMBER + ", " + SmartDialDbColumns.CONTACT_ID + ", " + SmartDialDbColumns.LOOKUP_KEY + ", " + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + + SmartDialDbColumns.PHOTO_ID + ", " + SmartDialDbColumns.LAST_TIME_USED + ", " + SmartDialDbColumns.TIMES_USED + ", " + SmartDialDbColumns.STARRED + ", " + @@ -479,7 +509,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { SmartDialDbColumns.IN_VISIBLE_GROUP+ ", " + SmartDialDbColumns.IS_PRIMARY + ", " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ") " + - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; final SQLiteStatement insert = db.compileStatement(sqlInsert); final String numberSqlInsert = "INSERT INTO " + Tables.PREFIX_TABLE + " (" + @@ -490,17 +520,19 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { updatedContactCursor.moveToPosition(-1); while (updatedContactCursor.moveToNext()) { - insert.bindString(1, updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER)); - insert.bindLong(2, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID)); - insert.bindString(3, updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY)); - insert.bindString(4, updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME)); - insert.bindLong(5, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED)); - insert.bindLong(6, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED)); - insert.bindLong(7, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED)); - insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY)); - insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP)); - insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY)); - insert.bindLong(11, currentMillis); + insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID)); + insert.bindString(2, updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER)); + insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID)); + insert.bindString(4, updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY)); + insert.bindString(5, updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME)); + insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID)); + insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED)); + insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED)); + insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED)); + insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY)); + insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP)); + insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY)); + insert.bindLong(13, currentMillis); insert.executeInsert(); insert.clearBindings(); @@ -719,7 +751,6 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { } } - /** * Returns a list of candidate contacts where the query is a prefix of the dialpad index of * the contact's name or phone number. @@ -747,7 +778,9 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { /** Queries the database to find contacts that have an index matching the query prefix. */ final Cursor cursor = db.rawQuery("SELECT " + + SmartDialDbColumns.DATA_ID + ", " + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + + SmartDialDbColumns.PHOTO_ID + ", " + SmartDialDbColumns.NUMBER + ", " + SmartDialDbColumns.CONTACT_ID + ", " + SmartDialDbColumns.LOOKUP_KEY + @@ -765,10 +798,12 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { } /** Gets the column ID from the cursor.*/ - final int columnDisplayNamePrimary = 0; - final int columnNumber = 1; - final int columnId = 2; - final int columnLookupKey = 3; + final int columnDataId = 0; + final int columnDisplayNamePrimary = 1; + final int columnPhotoId = 2; + final int columnNumber = 3; + final int columnId = 4; + final int columnLookupKey = 5; if (DEBUG) { stopWatch.lap("Found column IDs"); } @@ -781,9 +816,11 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { } /** Iterates the cursor to find top contact suggestions without duplication.*/ while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) { + final long dataID = cursor.getLong(columnDataId); final String displayName = cursor.getString(columnDisplayNamePrimary); final String phoneNumber = cursor.getString(columnNumber); final long id = cursor.getLong(columnId); + final long photoId = cursor.getLong(columnPhotoId); final String lookupKey = cursor.getString(columnLookupKey); /** If a contact already exists and another phone number of the contact is being @@ -798,11 +835,14 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * If the contact has either the name or number that matches the query, add to the * result. */ - if (nameMatcher.matches(displayName) || - nameMatcher.matchesNumber(phoneNumber, query) != null) { + final boolean nameMatches = nameMatcher.matches(displayName); + final boolean numberMatches = + (nameMatcher.matchesNumber(phoneNumber, query) != null); + if (nameMatches || numberMatches) { /** If a contact has not been added, add it to the result and the hash set.*/ duplicates.add(contactMatch); - result.add(new ContactNumber(id, displayName, phoneNumber, lookupKey)); + result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey, + photoId)); counter++; if (DEBUG) { stopWatch.lap("Added one result"); @@ -818,9 +858,4 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { } return result; } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - - } } diff --git a/src/com/android/dialer/dialpad/NewDialpadFragment.java b/src/com/android/dialer/dialpad/NewDialpadFragment.java index 707651726..46f5d06cb 100644 --- a/src/com/android/dialer/dialpad/NewDialpadFragment.java +++ b/src/com/android/dialer/dialpad/NewDialpadFragment.java @@ -32,7 +32,6 @@ import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.ToneGenerator; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; @@ -50,6 +49,7 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.RelativeSizeSpan; +import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; @@ -60,13 +60,15 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnPreDrawListener; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupMenu; -import android.widget.RelativeLayout; import android.widget.TextView; import com.android.contacts.common.CallUtil; @@ -76,6 +78,8 @@ import com.android.contacts.common.preference.ContactsPreferences; import com.android.contacts.common.util.PhoneNumberFormatter; import com.android.contacts.common.util.StopWatch; import com.android.dialer.DialtactsActivity; +import com.android.dialer.NeededForReflection; +import com.android.dialer.NewDialtactsActivity; import com.android.dialer.R; import com.android.dialer.SpecialCharSequenceMgr; import com.android.dialer.database.DialerDatabaseHelper; @@ -86,8 +90,6 @@ import com.android.phone.common.CallLogAsync; import com.android.phone.common.HapticFeedback; import com.google.common.annotations.VisibleForTesting; -import java.util.List; - /** * Fragment that displays a twelve-key phone dialpad. */ @@ -96,10 +98,42 @@ public class NewDialpadFragment extends Fragment View.OnLongClickListener, View.OnKeyListener, AdapterView.OnItemClickListener, TextWatcher, PopupMenu.OnMenuItemClickListener, - DialpadImageButton.OnPressedListener, - SmartDialLoaderTask.SmartDialLoaderCallback { + DialpadImageButton.OnPressedListener { private static final String TAG = NewDialpadFragment.class.getSimpleName(); + /** + * LinearLayout with getter and setter methods for the translationY property using floats, + * for animation purposes. + */ + public static class DialpadSlidingLinearLayout extends LinearLayout { + + public DialpadSlidingLinearLayout(Context context) { + super(context); + } + + public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @NeededForReflection + public float getYFraction() { + return getTranslationY() / getHeight(); + } + + @NeededForReflection + public void setYFraction(float yFraction) { + setTranslationY(yFraction * getHeight()); + } + } + + public interface OnDialpadQueryChangedListener { + void onDialpadQueryChanged(String query); + } + private static final boolean DEBUG = DialtactsActivity.DEBUG; private static final String EMPTY_NUMBER = ""; @@ -118,6 +152,10 @@ public class NewDialpadFragment extends Fragment private ContactsPreferences mContactsPrefs; + private OnDialpadQueryChangedListener mDialpadQueryListener; + + private View mFragmentView; + /** * View (usually FrameLayout) containing mDigits field. This can be null, in which mDigits * isn't enclosed by the container. @@ -145,25 +183,6 @@ public class NewDialpadFragment extends Fragment private ListView mDialpadChooser; private DialpadChooserAdapter mDialpadChooserAdapter; - /** Will be set only if the view has the smart dialing section. */ - private RelativeLayout mSmartDialContainer; - - /** - * Will be set only if the view has the smart dialing section. - */ - private SmartDialController mSmartDialAdapter; - - /** - * Use latin character map by default - */ - private SmartDialMap mSmartDialMap = new LatinSmartDialMap(); - - /** - * Master switch controlling whether or not smart dialing is enabled, and whether the - * smart dialing suggestion strip is visible. - */ - private boolean mSmartDialEnabled = false; - private DialerDatabaseHelper mDialerDatabaseHelper; /** @@ -287,8 +306,10 @@ public class NewDialpadFragment extends Fragment mDigits.setCursorVisible(false); } + if (mDialpadQueryListener != null) { + mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString()); + } updateDialAndDeleteButtonEnabledState(); - loadSmartDialEntries(); } @Override @@ -308,8 +329,6 @@ public class NewDialpadFragment extends Fragment Log.e(TAG, "Vibrate control bool missing.", nfe); } - setHasOptionsMenu(true); - mProhibitedPhoneNumberRegexp = getResources().getString( R.string.config_prohibited_phone_number_regexp); @@ -320,7 +339,29 @@ public class NewDialpadFragment extends Fragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { - View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false); + View fragmentView = inflater.inflate(R.layout.new_dialpad_fragment, container, false); + mFragmentView = fragmentView; + mFragmentView.buildLayer(); + + // TODO krelease: Get rid of this ugly hack which is to prevent the first frame of the + // animation from drawing the fragment at translationY = 0 + final ViewTreeObserver vto = mFragmentView.getViewTreeObserver(); + final OnPreDrawListener preDrawListener = new OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + if (isHidden()) return true; + if (mFragmentView.getTranslationY() == 0) { + ((DialpadSlidingLinearLayout) mFragmentView).setYFraction(0.67f); + } + final ViewTreeObserver vto = mFragmentView.getViewTreeObserver(); + vto.removeOnPreDrawListener(this); + return true; + } + + }; + + vto.addOnPreDrawListener(preDrawListener); // Load up the resources for the text field. Resources r = getResources(); @@ -379,15 +420,6 @@ public class NewDialpadFragment extends Fragment mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser); mDialpadChooser.setOnItemClickListener(this); - // Smart dial container. This is null if in landscape mode since it is not present - // in the landscape dialer layout. - mSmartDialContainer = (RelativeLayout) fragmentView.findViewById( - R.id.dialpad_smartdial_container); - - if (mSmartDialContainer != null) { - mSmartDialAdapter = new SmartDialController(getActivity(), mSmartDialContainer, - new OnSmartDialShortClick(), new OnSmartDialLongClick()); - } return fragmentView; } @@ -560,6 +592,9 @@ public class NewDialpadFragment extends Fragment public void onResume() { super.onResume(); + final NewDialtactsActivity activity = (NewDialtactsActivity) getActivity(); + mDialpadQueryListener = activity; + final StopWatch stopWatch = StopWatch.start("Dialpad.onResume"); // Query the last dialed number. Do it first because hitting @@ -574,10 +609,6 @@ public class NewDialpadFragment extends Fragment mDTMFToneEnabled = Settings.System.getInt(contentResolver, Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; - // retrieve dialpad autocomplete setting - mSmartDialEnabled = Settings.Secure.getInt(contentResolver, - Settings.Secure.DIALPAD_AUTOCOMPLETE, 0) == 1 && mSmartDialContainer != null; - stopWatch.lap("dtwd"); // Retrieve the haptic feedback setting. @@ -678,6 +709,7 @@ public class NewDialpadFragment extends Fragment @Override public void onStop() { super.onStop(); + if (mClearDigitsOnStop) { mClearDigitsOnStop = false; mDigits.getText().clear(); @@ -690,28 +722,6 @@ public class NewDialpadFragment extends Fragment outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, mDigitsFilledByIntent); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - // Landscape dialer uses the real actionbar menu, whereas portrait uses a fake one - // that is created using constructPopupMenu() - if (OrientationUtil.isLandscape(this.getActivity()) || - ViewConfiguration.get(getActivity()).hasPermanentMenuKey() && - isLayoutReady() && mDialpadChooser != null) { - inflater.inflate(R.menu.dialpad_options, menu); - } - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - // Hardware menu key should be available and Views should already be ready. - if (OrientationUtil.isLandscape(this.getActivity()) || - ViewConfiguration.get(getActivity()).hasPermanentMenuKey() && - isLayoutReady() && mDialpadChooser != null) { - setupMenuItems(menu); - } - } - private void setupMenuItems(Menu menu) { final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_dialpad); final MenuItem addToContactMenuItem = menu.findItem(R.id.menu_add_contacts); @@ -1481,24 +1491,6 @@ public class NewDialpadFragment extends Fragment return getTelephonyManager().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK; } - /** - * Returns true whenever any one of the options from the menu is selected. - * Code changes to support dialpad options - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_2s_pause: - updateDialString(PAUSE); - return true; - case R.id.menu_add_wait: - updateDialString(WAIT); - return true; - default: - return false; - } - } - @Override public boolean onMenuItemClick(MenuItem item) { return onOptionsItemSelected(item); @@ -1507,6 +1499,7 @@ public class NewDialpadFragment extends Fragment /** * Updates the dial string (mDigits) after inserting a Pause character (,) * or Wait character (;). + * TODO krelease: add new dialpad buttons to add PAUSE and WAIT characters */ private void updateDialString(char newDigit) { if(newDigit != WAIT && newDigit != PAUSE) { @@ -1654,79 +1647,22 @@ public class NewDialpadFragment extends Fragment return intent; } - private String mLastDigitsForSmartDial; - - private void loadSmartDialEntries() { - if (!mSmartDialEnabled || mSmartDialAdapter == null) { - // No smart dial views. Landscape? - return; - } - - // Update only when the digits have changed. - final String digits = SmartDialNameMatcher.normalizeNumber(mDigits.getText().toString(), - mSmartDialMap); - if (TextUtils.equals(digits, mLastDigitsForSmartDial)) { - return; - } - mLastDigitsForSmartDial = digits; - - if (digits.length() < 1) { - mSmartDialAdapter.clear(); - } else { - final SmartDialLoaderTask task = new SmartDialLoaderTask(this, digits, getActivity()); - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new String[] {}); - } - } - - @Override - public void setSmartDialAdapterEntries(List<SmartDialEntry> data, String query) { - if (data == null || query == null || !query.equals(mLastDigitsForSmartDial)) { - return; - } - mSmartDialAdapter.setEntries(data); - } - private void initializeSmartDialingState() { // Handle smart dialing related state - if (mSmartDialEnabled) { - mSmartDialContainer.setVisibility(View.VISIBLE); - - if (DEBUG) { - Log.w(TAG, "Creating smart dial database"); - } - mDialerDatabaseHelper.startSmartDialUpdateThread(); - } else { - if (mSmartDialContainer != null) { - mSmartDialContainer.setVisibility(View.GONE); - } - } - } - - private class OnSmartDialLongClick implements View.OnLongClickListener { - @Override - public boolean onLongClick(View view) { - final SmartDialEntry entry = (SmartDialEntry) view.getTag(); - if (entry == null) return false; // just in case. - mClearDigitsOnStop = true; - // Show the phone number disambiguation dialog without using the primary - // phone number so that the user can decide which number to call - PhoneNumberInteraction.startInteractionForPhoneCall( - (TransactionSafeActivity) getActivity(), entry.contactUri, false); - return true; - } + // TODO krelease: This should probably be moved to somewhere more appropriate, maybe + // into DialtactsActivity + mDialerDatabaseHelper.startSmartDialUpdateThread(); } - private class OnSmartDialShortClick implements View.OnClickListener { - @Override - public void onClick(View view) { - final SmartDialEntry entry = (SmartDialEntry) view.getTag(); - if (entry == null) return; // just in case. - // Dial the displayed phone number immediately - final Intent intent = CallUtil.getCallIntent(entry.phoneNumber.toString(), - (getActivity() instanceof DialtactsActivity ? - ((DialtactsActivity) getActivity()).getCallOrigin() : null)); - startActivity(intent); - mClearDigitsOnStop = true; + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + final NewDialtactsActivity activity = (NewDialtactsActivity) getActivity(); + if (activity == null) return; + if (hidden) { + activity.showSearchBar(); + } else { + activity.hideSearchBar(); } } } diff --git a/src/com/android/dialer/dialpad/SmartDialCursorLoader.java b/src/com/android/dialer/dialpad/SmartDialCursorLoader.java new file mode 100644 index 000000000..715f1e7cc --- /dev/null +++ b/src/com/android/dialer/dialpad/SmartDialCursorLoader.java @@ -0,0 +1,182 @@ +/* + * 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.dialpad; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; + +import java.util.ArrayList; + +/** + * Implements a Loader<Cursor> class to asynchronously load SmartDial search results. + */ +public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { + + private final String TAG = SmartDialCursorLoader.class.getSimpleName(); + private final boolean DEBUG = false; + + private final Context mContext; + + private Cursor mCursor; + + private String mQuery; + private SmartDialNameMatcher mNameMatcher; + + /** Constructs the columns of the cursor to be used. */ + public static class SmartDialPhoneQuery { + public static final String[] PROJECTION_PRIMARY = new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_PRIMARY, // 7 + }; + + public static final int SMARTDIAL_ID = 0; + public static final int SMARTDIAL_TYPE = 1; + public static final int SMARTDIAL_LABEL = 2; + public static final int SMARTDIAL_NUMBER = 3; + public static final int SMARTDIAL_CONTACT_ID = 4; + public static final int SMARTDIAL_LOOKUP_KEY = 5; + public static final int SMARTDIAL_PHOTO_ID = 6; + public static final int SMARTDIAL_DISPLAY_NAME = 7; + } + + public SmartDialCursorLoader(Context context) { + super(context); + mContext = context; + } + + /** + * Configures the query string to be used to find SmartDial matches. + * @param query The query string user typed. + */ + public void configureQuery(String query) { + if (DEBUG) { + Log.v(TAG, "Configure new query to be " + query); + } + mQuery = query; + + /** Constructs a name matcher object for matching names. */ + mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query), + SmartDialPrefix.getMap()); + } + + /** + * Queries the SmartDial database and loads results in background. + * @return Cursor of contacts that matches the SmartDial query. + */ + @Override + public Cursor loadInBackground() { + if (DEBUG) { + Log.v(TAG, "Load in background " + mQuery); + } + + /** Loads results from the database helper. */ + DialerDatabaseHelper dialerDatabaseHelper = DialerDatabaseHelper.getInstance(mContext); + final ArrayList<ContactNumber> allMatches = dialerDatabaseHelper.getLooseMatches(mQuery, + mNameMatcher); + + if (DEBUG) { + Log.v(TAG, "Loaded matches " + String.valueOf(allMatches.size())); + } + + /** Constructs a cursor for the returned array of results. */ + final MatrixCursor cursor = new MatrixCursor(SmartDialPhoneQuery.PROJECTION_PRIMARY); + for (ContactNumber contact : allMatches) { + cursor.addRow(new Object[] {contact.dataId, null, null, contact.phoneNumber, contact.id, + contact.lookupKey, contact.photoId, contact.displayName}); + } + return cursor; + } + + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + /** The Loader has been reset; ignore the result and invalidate the data. */ + releaseResources(cursor); + return; + } + + /** Hold a reference to the old data so it doesn't get garbage collected. */ + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + /** If the Loader is in a started state, deliver the results to the client. */ + super.deliverResult(cursor); + } + + /** Invalidate the old data as we don't need it any more. */ + if (oldCursor != null && oldCursor != cursor) { + releaseResources(oldCursor); + } + } + + @Override + protected void onStartLoading() { + if (mCursor != null) { + /** Deliver any previously loaded data immediately. */ + deliverResult(mCursor); + } + if (mCursor == null) { + /** Force loads every time as our results change with queries. */ + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ + cancelLoad(); + } + + @Override + protected void onReset() { + /** Ensure the loader has been stopped. */ + onStopLoading(); + + /** Release all previously saved query results. */ + if (mCursor != null) { + releaseResources(mCursor); + mCursor = null; + } + } + + @Override + public void onCanceled(Cursor cursor) { + super.onCanceled(cursor); + + /** The load has been canceled, so we should release the resources associated with 'data'.*/ + releaseResources(cursor); + } + + private void releaseResources(Cursor cursor) { + cursor.close(); + } +} diff --git a/src/com/android/dialer/dialpad/SmartDialMatchPosition.java b/src/com/android/dialer/dialpad/SmartDialMatchPosition.java index 434874646..452ac9110 100644 --- a/src/com/android/dialer/dialpad/SmartDialMatchPosition.java +++ b/src/com/android/dialer/dialpad/SmartDialMatchPosition.java @@ -28,7 +28,7 @@ import java.util.ArrayList; * in the query. Used by {@link SmartDialController} to highlight certain parts of the contact's * display name to indicate that those ranges matched the user's query. */ -class SmartDialMatchPosition { +public class SmartDialMatchPosition { public int start; public int end; diff --git a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java index fe88e930d..c160bd258 100644 --- a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java +++ b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java @@ -45,10 +45,13 @@ public class SmartDialNameMatcher { private final ArrayList<SmartDialMatchPosition> mMatchPositions = Lists.newArrayList(); - private static final SmartDialMap LATIN_SMART_DIAL_MAP = new LatinSmartDialMap(); + public static final SmartDialMap LATIN_SMART_DIAL_MAP = new LatinSmartDialMap(); private final SmartDialMap mMap; + private String mNameMatchMask = ""; + private String mPhoneNumberMatchMask = ""; + @VisibleForTesting public SmartDialNameMatcher(String query) { this(query, LATIN_SMART_DIAL_MAP); @@ -60,6 +63,29 @@ public class SmartDialNameMatcher { } /** + * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means + * there is a match and should be highlighted in the TextView. + * @param builder StringBuilder object + * @param length Length of the desired mask. + */ + private void constructEmptyMask(StringBuilder builder, int length) { + for (int i = 0; i < length; ++i) { + builder.append("0"); + } + } + + /** + * Replaces the 0-bit at a position with 1-bit, indicating that there is a match. + * @param builder StringBuilder object. + * @param matchPos Match Positions to mask as 1. + */ + private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) { + for (int i = matchPos.start; i < matchPos.end; ++i) { + builder.replace(i, i + 1, "1"); + } + } + + /** * Strips a phone number of unnecessary characters (spaces, dashes, etc.) * * @param number Phone number we want to normalize @@ -98,6 +124,10 @@ public class SmartDialNameMatcher { */ @VisibleForTesting public SmartDialMatchPosition matchesNumber(String phoneNumber, String query, boolean useNanp) { + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, phoneNumber.length()); + mPhoneNumberMatchMask = builder.toString(); + // Try matching the number as is SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0); if (matchPos == null) { @@ -105,6 +135,10 @@ public class SmartDialNameMatcher { SmartDialPrefix.parsePhoneNumber(phoneNumber); if (phoneNumberTokens == null) { + if (matchPos != null) { + replaceBitInMask(builder, matchPos); + mPhoneNumberMatchMask = builder.toString(); + } return matchPos; } if (phoneNumberTokens.countryCodeOffset != 0) { @@ -116,10 +150,26 @@ public class SmartDialNameMatcher { phoneNumberTokens.nanpCodeOffset); } } + if (matchPos != null) { + replaceBitInMask(builder, matchPos); + mPhoneNumberMatchMask = builder.toString(); + } return matchPos; } /** + * Matches a phone number against the saved query, taking care of formatting characters and also + * taking into account country code prefixes and special NANP number treatment. + * + * @param phoneNumber - Raw phone number + * @return {@literal null} if the number and the query don't match, a valid + * SmartDialMatchPosition with the matching positions otherwise + */ + public SmartDialMatchPosition matchesNumber(String phoneNumber) { + return matchesNumber(phoneNumber, mQuery, true); + } + + /** * Matches a phone number against a query, taking care of formatting characters and also * taking into account country code prefixes and special NANP number treatment. * @@ -210,6 +260,9 @@ public class SmartDialNameMatcher { @VisibleForTesting boolean matchesCombination(String displayName, String query, ArrayList<SmartDialMatchPosition> matchList) { + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, displayName.length()); + mNameMatchMask = builder.toString(); final int nameLength = displayName.length(); final int queryLength = query.length(); @@ -286,6 +339,10 @@ public class SmartDialNameMatcher { // one so if we find a full token match, we can return right away matchList.add(new SmartDialMatchPosition( tokenStart, queryLength + tokenStart + seperatorCount)); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + mNameMatchMask = builder.toString(); return true; } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) { // we matched the first character. @@ -343,6 +400,10 @@ public class SmartDialNameMatcher { // then partial will always be empty. if (!partial.isEmpty()) { matchList.addAll(partial); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + mNameMatchMask = builder.toString(); return true; } return false; @@ -359,6 +420,14 @@ public class SmartDialNameMatcher { return new ArrayList<SmartDialMatchPosition>(mMatchPositions); } + public String getNameMatchPositionsInString() { + return mNameMatchMask; + } + + public String getNumberMatchPositionsInString() { + return mPhoneNumberMatchMask; + } + public String getQuery() { return mQuery; } diff --git a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java index e694d6067..db2999ccf 100644 --- a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java +++ b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * 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. @@ -27,14 +27,10 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.provider.ContactsContract; import android.provider.ContactsContract.Directory; import android.provider.Settings; import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -47,18 +43,16 @@ import android.widget.TextView; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactTileLoaderFactory; -import com.android.contacts.common.dialog.ClearFrequentsDialog; -import com.android.contacts.common.list.ContactListFilter; -import com.android.contacts.common.list.ContactListFilterController; +import com.android.contacts.common.GeoUtil; import com.android.contacts.common.list.ContactListItemView; -import com.android.contacts.common.list.ContactTileAdapter; import com.android.contacts.common.list.ContactTileView; import com.android.contacts.common.list.PhoneNumberListAdapter; import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.AccountFilterUtil; -import com.android.contacts.common.interactions.ImportExportDialogFragment; -import com.android.dialer.DialtactsActivity; +import com.android.dialer.NewDialtactsActivity; import com.android.dialer.R; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.calllog.NewCallLogAdapter; +import com.android.dialer.calllog.CallLogQueryHandler; /** * Fragment for Phone UI's favorite screen. @@ -68,7 +62,8 @@ import com.android.dialer.R; * {@link com.android.contacts.common.list.PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}. * A contact filter header is also inserted between those adapters' results. */ -public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickListener { +public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickListener, + CallLogQueryHandler.Listener, NewCallLogAdapter.CallFetcher { private static final String TAG = NewPhoneFavoriteFragment.class.getSimpleName(); private static final boolean DEBUG = false; @@ -78,9 +73,9 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis private static int LOADER_ID_CONTACT_TILE = 1; private static int LOADER_ID_ALL_CONTACTS = 2; - private static final String KEY_FILTER = "filter"; - - private static final int REQUEST_CODE_ACCOUNT_FILTER = 1; + public interface OnPhoneFavoriteFragmentStartedListener { + public void onPhoneFavoriteFragmentStarted(); + } public interface Listener { public void onContactSelected(Uri contactUri); @@ -113,11 +108,7 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis mAllContactsLoaderStarted = true; // Show the filter header with "loading" state. - updateFilterHeaderView(); mAccountFilterHeader.setVisibility(View.VISIBLE); - - // invalidate the options menu if needed - invalidateOptionsMenuIfNeeded(); } @Override @@ -139,7 +130,6 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished"); mAllContactsAdapter.changeCursor(0, data); - updateFilterHeaderView(); mHandler.removeMessages(MESSAGE_SHOW_LOADING_EFFECT); mLoadingView.setVisibility(View.VISIBLE); } @@ -171,16 +161,6 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis } } - private class FilterHeaderClickListener implements OnClickListener { - @Override - public void onClick(View view) { - AccountFilterUtil.startAccountFilterActivityForResult( - NewPhoneFavoriteFragment.this, - REQUEST_CODE_ACCOUNT_FILTER, - mFilter); - } - } - private class ContactsPreferenceChangeListener implements ContactsPreferences.ChangeListener { @Override @@ -204,10 +184,13 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis mListView.setFastScrollAlwaysVisible(shouldShow); mShouldShowFastScroller = shouldShow; } + + } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); } } @@ -225,10 +208,15 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis }; private Listener mListener; - private PhoneFavoriteMergedAdapter mAdapter; - private ContactTileAdapter mContactTileAdapter; + + private OnListFragmentScrolledListener mActivityScrollListener; + private NewPhoneFavoriteMergedAdapter mAdapter; + private PhoneFavoritesTileAdapter mContactTileAdapter; private PhoneNumberListAdapter mAllContactsAdapter; + private NewCallLogAdapter mCallLogAdapter; + private CallLogQueryHandler mCallLogQueryHandler; + /** * true when the loader for {@link PhoneNumberListAdapter} has started already. */ @@ -241,7 +229,6 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis private boolean mAllContactsForceReload; private ContactsPreferences mContactsPrefs; - private ContactListFilter mFilter; private TextView mEmptyView; private ListView mListView; @@ -263,7 +250,6 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis new ContactTileLoaderListener(); private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener = new AllContactsLoaderListener(); - private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener(); private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener = new ContactsPreferenceChangeListener(); private final ScrollListener mScrollListener = new ScrollListener(); @@ -281,9 +267,9 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis // We don't construct the resultant adapter at this moment since it requires LayoutInflater // that will be available on onCreateView(). - mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener, - getResources().getInteger(R.integer.contact_tile_column_count_in_favorites), - ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY); + mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener, + getResources().getInteger(R.integer.contact_tile_column_count_in_favorites_new), + 1); mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment. @@ -293,7 +279,7 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis mAllContactsAdapter.setSearchMode(false); mAllContactsAdapter.setIncludeProfile(false); mAllContactsAdapter.setSelectionVisible(false); - mAllContactsAdapter.setDarkTheme(true); + mAllContactsAdapter.setDarkTheme(false); mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); // Disable directory header. mAllContactsAdapter.setHasHeader(0, false); @@ -318,27 +304,27 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis public void onCreate(Bundle savedState) { if (DEBUG) Log.d(TAG, "onCreate()"); super.onCreate(savedState); - if (savedState != null) { - mFilter = savedState.getParcelable(KEY_FILTER); - if (mFilter != null) { - mAllContactsAdapter.setFilter(mFilter); - } - } + mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), + this, 1); + final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + mCallLogAdapter = new NewCallLogAdapter(getActivity(), this, + new ContactInfoHelper(getActivity(), currentCountryIso)); setHasOptionsMenu(true); } @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelable(KEY_FILTER, mFilter); + public void onResume() { + super.onResume(); + mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); + mCallLogAdapter.setLoading(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View listLayout = inflater.inflate( - R.layout.phone_contact_tile_list, container, false); + R.layout.new_phone_favorites_fragment, container, false); mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list); mListView.setItemsCanFocus(true); @@ -347,18 +333,18 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + // TODO krelease: Don't show this header anymore // Create the account filter header but keep it hidden until "all" contacts are loaded. mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null); mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite, mListView, false); - mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener); mAccountFilterHeaderContainer.addView(mAccountFilterHeader); mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false); - mAdapter = new PhoneFavoriteMergedAdapter(getActivity(), + mAdapter = new NewPhoneFavoriteMergedAdapter(getActivity(), mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter, - mLoadingView); + mCallLogAdapter, mLoadingView); mListView.setAdapter(mAdapter); @@ -370,68 +356,48 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis mEmptyView.setText(getString(R.string.listTotalAllContactsZero)); mListView.setEmptyView(mEmptyView); - updateFilterHeaderView(); - return listLayout; } + // TODO krelease: update the options menu when displaying the popup menu instead. We could + // possibly get rid of this method entirely. private boolean isOptionsMenuChanged() { return mOptionsMenuHasFrequents != hasFrequents(); } - private void invalidateOptionsMenuIfNeeded() { - if (isOptionsMenuChanged()) { - getActivity().invalidateOptionsMenu(); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.phone_favorite_options, menu); - } - + // TODO krelease: Configure the menu items properly. Since the menu items show up as a PopupMenu + // rather than a normal actionbar menu, the initialization should be done there. + /* @Override public void onPrepareOptionsMenu(Menu menu) { final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); mOptionsMenuHasFrequents = hasFrequents(); clearFrequents.setVisible(mOptionsMenuHasFrequents); - } + }*/ private boolean hasFrequents() { return mContactTileAdapter.getNumFrequents() > 0; } @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_import_export: - // We hard-code the "contactsAreAvailable" argument because doing it properly would - // involve querying a {@link ProviderStatusLoader}, which we don't want to do right - // now in Dialtacts for (potential) performance reasons. Compare with how it is - // done in {@link PeopleActivity}. - ImportExportDialogFragment.show(getFragmentManager(), true, - DialtactsActivity.class); - return true; - case R.id.menu_accounts: - final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); - intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] { - ContactsContract.AUTHORITY - }); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - startActivity(intent); - return true; - case R.id.menu_clear_frequents: - ClearFrequentsDialog.show(getFragmentManager()); - return true; - } - return false; - } - - @Override public void onStart() { super.onStart(); + final Activity activity = getActivity(); + + try { + ((OnPhoneFavoriteFragmentStartedListener) activity).onPhoneFavoriteFragmentStarted(); + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement OnPhoneFavoriteFragmentStartedListener"); + } + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement OnListFragmentScrolledListener"); + } mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener); // If ContactsPreferences has changed, we need to reload "all" contacts with the new @@ -478,18 +444,6 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) { - if (getActivity() != null) { - AccountFilterUtil.handleAccountFilterResult( - ContactListFilterController.getInstance(getActivity()), resultCode, data); - } else { - Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()"); - } - } - } - private boolean loadContactsPreferences() { if (mContactsPrefs == null || mAllContactsAdapter == null) { return false; @@ -536,38 +490,24 @@ public class NewPhoneFavoriteFragment extends Fragment implements OnItemClickLis getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener); } - private void updateFilterHeaderView() { - final ContactListFilter filter = getFilter(); - if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) { - return; - } - AccountFilterUtil.updateAccountFilterTitleForPhone(mAccountFilterHeader, filter, true); + public void setListener(Listener listener) { + mListener = listener; } - public ContactListFilter getFilter() { - return mFilter; + // TODO krelease: Implement this + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { } - public void setFilter(ContactListFilter filter) { - if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { - return; - } - - if (DEBUG) { - Log.d(TAG, "setFilter(). old filter (" + mFilter - + ") will be replaced with new filter (" + filter + ")"); - } - - mFilter = filter; - - if (mAllContactsAdapter != null) { - mAllContactsAdapter.setFilter(mFilter); - requestReloadAllContacts(); - updateFilterHeaderView(); - } + @Override + public void onCallsFetched(Cursor cursor) { + mCallLogAdapter.setLoading(false); + mCallLogAdapter.changeCursor(cursor); + mAdapter.notifyDataSetChanged(); } - public void setListener(Listener listener) { - mListener = listener; + // TODO krelease: Implement this + @Override + public void fetchCalls() { } } diff --git a/src/com/android/dialer/list/NewPhoneFavoriteMergedAdapter.java b/src/com/android/dialer/list/NewPhoneFavoriteMergedAdapter.java index 047609f7d..49d46a851 100644 --- a/src/com/android/dialer/list/NewPhoneFavoriteMergedAdapter.java +++ b/src/com/android/dialer/list/NewPhoneFavoriteMergedAdapter.java @@ -27,13 +27,15 @@ import android.widget.SectionIndexer; import com.android.contacts.common.list.ContactEntryListAdapter; import com.android.contacts.common.list.ContactListItemView; -import com.android.contacts.common.list.ContactTileAdapter; import com.android.dialer.R; +import com.android.dialer.calllog.NewCallLogAdapter; /** * An adapter that combines items from {@link com.android.contacts.common.list.ContactTileAdapter} and - * {@link com.android.contacts.common.list.ContactEntryListAdapter} into a single list. In between those two results, - * an account filter header will be inserted. + * {@link com.android.contacts.common.list.ContactEntryListAdapter} into a single list. + * In between those two results, an account filter header will be inserted. + * + * Has one extra view at the top: The most recent call/voicemail/missed call. */ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements SectionIndexer { @@ -44,38 +46,41 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio } } - private final ContactTileAdapter mContactTileAdapter; + private static final String TAG = NewPhoneFavoriteMergedAdapter.class.getSimpleName(); + + private final PhoneFavoritesTileAdapter mContactTileAdapter; private final ContactEntryListAdapter mContactEntryListAdapter; + private final NewCallLogAdapter mCallLogAdapter; private final View mAccountFilterHeaderContainer; private final View mLoadingView; + // TODO krelease: Add a setting to toggle mShowAllContacts, and really handle it + // properly below. + private boolean mShowAllContacts = false; + private final int mItemPaddingLeft; private final int mItemPaddingRight; - // Make frequent header consistent with account filter header. - private final int mFrequentHeaderPaddingTop; - private final DataSetObserver mObserver; public NewPhoneFavoriteMergedAdapter(Context context, - ContactTileAdapter contactTileAdapter, + PhoneFavoritesTileAdapter contactTileAdapter, View accountFilterHeaderContainer, ContactEntryListAdapter contactEntryListAdapter, + NewCallLogAdapter callLogAdapter, View loadingView) { - Resources resources = context.getResources(); + final Resources resources = context.getResources(); mItemPaddingLeft = resources.getDimensionPixelSize(R.dimen.detail_item_side_margin); mItemPaddingRight = resources.getDimensionPixelSize(R.dimen.list_visible_scrollbar_padding); - mFrequentHeaderPaddingTop = resources.getDimensionPixelSize( - R.dimen.contact_browser_list_top_margin); mContactTileAdapter = contactTileAdapter; mContactEntryListAdapter = contactEntryListAdapter; + mCallLogAdapter = callLogAdapter; mAccountFilterHeaderContainer = accountFilterHeaderContainer; mObserver = new CustomDataSetObserver(); mContactTileAdapter.registerDataSetObserver(mObserver); mContactEntryListAdapter.registerDataSetObserver(mObserver); - mLoadingView = loadingView; } @@ -83,34 +88,52 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio public boolean isEmpty() { // Cannot use the super's method here because we add extra rows in getCount() to account // for headers - return mContactTileAdapter.getCount() + mContactEntryListAdapter.getCount() == 0; + return mCallLogAdapter.getCount() + mContactTileAdapter.getCount() + + mContactEntryListAdapter.getCount() == 0; } @Override public int getCount() { final int contactTileAdapterCount = mContactTileAdapter.getCount(); - final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount(); - if (mContactEntryListAdapter.isLoading()) { + final int contactEntryListAdapterCount = mShowAllContacts ? + mContactEntryListAdapter.getCount() : 0; + + final int callLogAdapterCount = mCallLogAdapter.getCount(); + + if (mShowAllContacts && mContactEntryListAdapter.isLoading()) { // Hide "all" contacts during its being loaded. Instead show "loading" view. // // "+2" for mAccountFilterHeaderContainer and mLoadingView - return contactTileAdapterCount + 2; + return contactTileAdapterCount + 1 + (mShowAllContacts ? 1 : 0) + + callLogAdapterCount; } else { // "+1" for mAccountFilterHeaderContainer - return contactTileAdapterCount + contactEntryListAdapterCount + 1; + return contactTileAdapterCount + contactEntryListAdapterCount + + (mShowAllContacts ? 1 : 0) + callLogAdapterCount; } } @Override public Object getItem(int position) { final int contactTileAdapterCount = mContactTileAdapter.getCount(); - final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount(); + final int contactEntryListAdapterCount = mShowAllContacts ? + mContactEntryListAdapter.getCount() : 0; + + final int callLogAdapterCount = mCallLogAdapter.getCount(); + + // TODO krelease: Calculate the position properly. + if (callLogAdapterCount > 0) { + if (position < callLogAdapterCount) { + return mCallLogAdapter.getItem(position); + } + position -= callLogAdapterCount; + } + if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections return mContactTileAdapter.getItem(position); - } else if (position == contactTileAdapterCount) { // For "all" section's account header - return mAccountFilterHeaderContainer; } else { // For "all" section - if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded. + if (mShowAllContacts && mContactEntryListAdapter.isLoading()) { + // "All" section is being loaded. return mLoadingView; } else { // "-1" for mAccountFilterHeaderContainer @@ -127,16 +150,31 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio @Override public int getViewTypeCount() { - // "+2" for mAccountFilterHeaderContainer and mLoadingView + // "+1" for mLoadingView return (mContactTileAdapter.getViewTypeCount() + mContactEntryListAdapter.getViewTypeCount() - + 2); + + 1 + + mCallLogAdapter.getViewTypeCount()); } @Override public int getItemViewType(int position) { final int contactTileAdapterCount = mContactTileAdapter.getCount(); - final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount(); + + final int contactEntryListAdapterCount = mShowAllContacts ? + mContactEntryListAdapter.getCount() : 0; + + final int callLogAdapterCount = mCallLogAdapter.getCount(); + + if (callLogAdapterCount > 0) { + if (position == 0) { + return mContactTileAdapter.getViewTypeCount() + + mContactEntryListAdapter.getViewTypeCount() + 2; + } + // Ignore the first position when calculating view types of all other items + position -= callLogAdapterCount; + } + // There should be four kinds of types that are usually used, and one more exceptional // type (IGNORE_ITEM_VIEW_TYPE), which sometimes comes from mContactTileAdapter. // @@ -190,19 +228,28 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio @Override public View getView(int position, View convertView, ViewGroup parent) { final int contactTileAdapterCount = mContactTileAdapter.getCount(); - final int contactEntryListAdapterCount = mContactEntryListAdapter.getCount(); + + final int contactEntryListAdapterCount = mShowAllContacts ? + mContactEntryListAdapter.getCount() : 0; + + final int callLogAdapterCount = mCallLogAdapter.getCount(); + + // TODO krelease: Handle the new callLogAdapterCount and position offsets properly + if (callLogAdapterCount > 0) { + if (position == 0) { + final View view = mCallLogAdapter.getView(position, convertView, parent); + return view; + } + position -= callLogAdapterCount; + } // Obtain a View relevant for that position, and adjust its horizontal padding. Each // View has different implementation, so we use different way to control those padding. if (position < contactTileAdapterCount) { // For "tile" and "frequent" sections final View view = mContactTileAdapter.getView(position, convertView, parent); final int frequentHeaderPosition = mContactTileAdapter.getFrequentHeaderPosition(); - if (position < frequentHeaderPosition) { // "starred" contacts - // No padding adjustment. - } else if (position == frequentHeaderPosition) { - view.setPadding(mItemPaddingLeft, mFrequentHeaderPaddingTop, - mItemPaddingRight, view.getPaddingBottom()); - } else { + // TODO krelease: Get rid of frequent header position, we don't need it anymore + if (position >= frequentHeaderPosition) { // Views for "frequent" contacts use FrameLayout's margins instead of padding. final FrameLayout frameLayout = (FrameLayout) view; final View child = frameLayout.getChildAt(0); @@ -213,28 +260,16 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio child.setLayoutParams(params); } return view; - } else if (position == contactTileAdapterCount) { // For "all" section's account header - mAccountFilterHeaderContainer.setPadding(mItemPaddingLeft, - mAccountFilterHeaderContainer.getPaddingTop(), - mItemPaddingRight, - mAccountFilterHeaderContainer.getPaddingBottom()); - - // Show a single "No Contacts" label under the "all" section account header - // if no contacts are displayed. - mAccountFilterHeaderContainer.findViewById( - R.id.contact_list_all_empty).setVisibility( - contactEntryListAdapterCount == 0 ? View.VISIBLE : View.GONE); - return mAccountFilterHeaderContainer; } else { // For "all" section - if (mContactEntryListAdapter.isLoading()) { // "All" section is being loaded. + if (mShowAllContacts && mContactEntryListAdapter.isLoading()) { + // "All" section is being loaded. mLoadingView.setPadding(mItemPaddingLeft, mLoadingView.getPaddingTop(), mItemPaddingRight, mLoadingView.getPaddingBottom()); return mLoadingView; } else { - // "-1" for mAccountFilterHeaderContainer - final int localPosition = position - contactTileAdapterCount - 1; + final int localPosition = position - contactTileAdapterCount; final ContactListItemView itemView = (ContactListItemView) mContactEntryListAdapter.getView(localPosition, convertView, null); itemView.setPadding(mItemPaddingLeft, itemView.getPaddingTop(), @@ -278,9 +313,10 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio @Override public int getPositionForSection(int sectionIndex) { + // frequent header view. final int contactTileAdapterCount = mContactTileAdapter.getCount(); final int localPosition = mContactEntryListAdapter.getPositionForSection(sectionIndex); - return contactTileAdapterCount + 1 + localPosition; + return contactTileAdapterCount + localPosition; } @Override @@ -289,8 +325,7 @@ public class NewPhoneFavoriteMergedAdapter extends BaseAdapter implements Sectio if (position <= contactTileAdapterCount) { return 0; } else { - // "-1" for mAccountFilterHeaderContainer - final int localPosition = position - contactTileAdapterCount - 1; + final int localPosition = position - contactTileAdapterCount; return mContactEntryListAdapter.getSectionForPosition(localPosition); } } diff --git a/src/com/android/dialer/list/OnListFragmentScrolledListener.java b/src/com/android/dialer/list/OnListFragmentScrolledListener.java new file mode 100644 index 000000000..cc5f3cd3a --- /dev/null +++ b/src/com/android/dialer/list/OnListFragmentScrolledListener.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2013 Google Inc. + * Licensed to 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.list; + +/* + * Interface to provide callback to activity when a child fragment is scrolled + */ +public interface OnListFragmentScrolledListener { + public void onListFragmentScrollStateChange(int scrollState); +} diff --git a/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java index ba291a00f..bb758a710 100644 --- a/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java +++ b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java @@ -130,7 +130,7 @@ public class PhoneFavoriteMergedAdapter extends BaseAdapter implements SectionIn // "+2" for mAccountFilterHeaderContainer and mLoadingView return (mContactTileAdapter.getViewTypeCount() + mContactEntryListAdapter.getViewTypeCount() - + 2); + + 1); } @Override diff --git a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java new file mode 100644 index 000000000..2f5921eaf --- /dev/null +++ b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2011 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.list; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; + +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileView; +import com.android.contacts.common.util.ViewUtil; + +/** + * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in Dialtacts + * for frequently called contacts. Slightly different behavior from superclass... + * when you tap it, you want to call the frequently-called number for the + * contact, even if that is not the default number for that contact. + */ +public class PhoneFavoriteRegularRowView extends ContactTileView { + private String mPhoneNumberString; + + public PhoneFavoriteRegularRowView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override + protected int getApproximateImageSize() { + return ViewUtil.getConstantPreLayoutWidth(getQuickContact()); + } + + @Override + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + mPhoneNumberString = null; // ... in case we're reusing the view + if (entry != null) { + // Grab the phone-number to call directly... see {@link onClick()} + mPhoneNumberString = entry.phoneNumber; + } + } + + @Override + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) return; + if (TextUtils.isEmpty(mPhoneNumberString)) { + // Copy "superclass" implementation + mListener.onContactSelected(getLookupUri(), MoreContactUtils + .getTargetRectFromView( + mContext, PhoneFavoriteRegularRowView.this)); + } else { + // When you tap a frequently-called contact, you want to + // call them at the number that you usually talk to them + // at (i.e. the one displayed in the UI), regardless of + // whether that's their default number. + mListener.onCallNumberDirectly(mPhoneNumberString); + } + } + }; + } +} diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java new file mode 100644 index 000000000..d87e2a837 --- /dev/null +++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2011 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.list; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; + +import com.android.contacts.common.R; +import com.android.contacts.common.list.ContactTileView; + +/** + * Displays the contact's picture overlayed with their name + * in a perfect square. It also has an additional touch target for a secondary action. + */ +public class PhoneFavoriteTileView extends ContactTileView { + private ImageButton mSecondaryButton; + + public PhoneFavoriteTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button); + mSecondaryButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW, getLookupUri()); + // Secondary target will be visible only from phone's favorite screen, then + // we want to launch it as a separate People task. + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + getContext().startActivity(intent); + } + }); + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override + protected int getApproximateImageSize() { + // The picture is the full size of the tile (minus some padding, but we can be generous) + return mListener.getApproximateTileWidth(); + } +} diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java new file mode 100644 index 000000000..36fc34608 --- /dev/null +++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java @@ -0,0 +1,515 @@ +/* + * 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.list; + +import android.content.ContentUris; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; + +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.R; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileAdapter; +import com.android.contacts.common.list.ContactTileView; + +import java.util.ArrayList; + +/** + * Also allows for a configurable number of columns as well as a maximum row of tiled contacts. + * + * This adapter has been rewritten to only support a maximum of one row for favorites. + * + * TODO Krelease: Move to PhoneContactTileAdapter in Dialer package. + */ +public class PhoneFavoritesTileAdapter extends BaseAdapter { + private static final String TAG = ContactTileAdapter.class.getSimpleName(); + + public static final int NO_ROW_LIMIT = -1; + + private ContactTileView.Listener mListener; + private Context mContext; + private Resources mResources; + protected Cursor mContactCursor = null; + private ContactPhotoManager mPhotoManager; + protected int mNumFrequents; + + /** + * Index of the first NON starred contact in the {@link Cursor} + * Only valid when {@link DisplayType#STREQUENT} is true + */ + private int mDividerPosition; + protected int mColumnCount; + private int mMaxTiledRows = NO_ROW_LIMIT; + private int mStarredIndex; + + protected int mIdIndex; + protected int mLookupIndex; + protected int mPhotoUriIndex; + protected int mNameIndex; + protected int mPresenceIndex; + protected int mStatusIndex; + + /** + * Only valid when {@link DisplayType#STREQUENT_PHONE_ONLY} is true + */ + private int mPhoneNumberIndex; + private int mPhoneNumberTypeIndex; + private int mPhoneNumberLabelIndex; + + private boolean mIsQuickContactEnabled = false; + private final int mPaddingInPixels; + + public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols) { + this(context, listener, numCols, NO_ROW_LIMIT); + } + + public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols, + int maxTiledRows) { + mListener = listener; + mContext = context; + mResources = context.getResources(); + mColumnCount = numCols; + mNumFrequents = 0; + mMaxTiledRows = maxTiledRows; + + // Converting padding in dips to padding in pixels + mPaddingInPixels = mContext.getResources() + .getDimensionPixelSize(R.dimen.contact_tile_divider_padding); + bindColumnIndices(); + } + + public void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoManager = photoLoader; + } + + public void setMaxRowCount(int maxRows) { + mMaxTiledRows = maxRows; + } + + public void setColumnCount(int columnCount) { + mColumnCount = columnCount; + } + + public void enableQuickContact(boolean enableQuickContact) { + mIsQuickContactEnabled = enableQuickContact; + } + + /** + * Sets the column indices for expected {@link Cursor} + * based on {@link DisplayType}. + */ + protected void bindColumnIndices() { + mIdIndex = ContactTileLoaderFactory.CONTACT_ID; + mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY; + mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; + mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; + mStarredIndex = ContactTileLoaderFactory.STARRED; + mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE; + mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS; + + mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; + mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; + mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; + } + + /** + * Gets the number of frequents from the passed in cursor. + * + * This methods is needed so the GroupMemberTileAdapter can override this. + * + * @param cursor The cursor to get number of frequents from. + */ + protected void saveNumFrequentsFromCursor(Cursor cursor) { + mNumFrequents = cursor.getCount() - mDividerPosition; + } + + /** + * Creates {@link ContactTileView}s for each item in {@link Cursor}. + * + * Else use {@link ContactTileLoaderFactory} + */ + public void setContactCursor(Cursor cursor) { + mContactCursor = cursor; + mDividerPosition = getDividerPosition(cursor); + + saveNumFrequentsFromCursor(cursor); + + // cause a refresh of any views that rely on this data + notifyDataSetChanged(); + } + + /** + * Iterates over the {@link Cursor} + * Returns position of the first NON Starred Contact + * Returns -1 if {@link DisplayType#STARRED_ONLY} + * Returns 0 if {@link DisplayType#FREQUENT_ONLY} + */ + protected int getDividerPosition(Cursor cursor) { + if (cursor == null || cursor.isClosed()) { + throw new IllegalStateException("Unable to access cursor"); + } + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + if (cursor.getInt(mStarredIndex) == 0) { + return cursor.getPosition(); + } + } + + // There are not NON Starred contacts in cursor + // Set divider positon to end + return cursor.getCount(); + } + + protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) { + // If the loader was canceled we will be given a null cursor. + // In that case, show an empty list of contacts. + if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null; + + cursor.moveToPosition(position); + long id = cursor.getLong(mIdIndex); + String photoUri = cursor.getString(mPhotoUriIndex); + String lookupKey = cursor.getString(mLookupIndex); + + ContactEntry contact = new ContactEntry(); + String name = cursor.getString(mNameIndex); + contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); + contact.status = cursor.getString(mStatusIndex); + contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); + contact.lookupKey = ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); + + // Set phone number and label + int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); + String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); + contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, + phoneNumberCustomLabel); + contact.phoneNumber = cursor.getString(mPhoneNumberIndex); + + return contact; + } + + /** + * Returns the number of frequents that will be displayed in the list. + */ + public int getNumFrequents() { + return mNumFrequents; + } + + @Override + public int getCount() { + if (mContactCursor == null || mContactCursor.isClosed()) { + return 0; + } + + // Takes numbers of rows the Starred Contacts Occupy + int starredRowCount = getRowCount(mDividerPosition) + + (mMaxTiledRows == NO_ROW_LIMIT ? 0 : Math.max(0, mDividerPosition - + mMaxTiledRows * mColumnCount)); + + // Compute the frequent row count which is 1 plus the number of frequents + // (to account for the divider) or 0 if there are no frequents. + int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents; + + // Return the number of starred plus frequent rows + return starredRowCount + frequentRowCount; + } + + /** + * Returns the number of rows required to show the provided number of entries + * with the current number of columns. + */ + protected int getRowCount(int entryCount) { + if (entryCount == 0) return 0; + final int nonLimitedRows = ((entryCount - 1) / mColumnCount) + 1; + return mMaxTiledRows == NO_ROW_LIMIT ? nonLimitedRows : Math.min(mMaxTiledRows, + nonLimitedRows); + } + + public int getColumnCount() { + return mColumnCount; + } + + /** + * Returns an ArrayList of the {@link ContactEntry}s that are to appear + * on the row for the given position. + */ + @Override + public ArrayList<ContactEntry> getItem(int position) { + ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); + int contactIndex = position * mColumnCount; + if (position < getRowCount(mDividerPosition)) { + for (int columnCounter = 0; columnCounter < mColumnCount && + contactIndex != mDividerPosition; columnCounter++) { + resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + contactIndex++; + } + } else { + /* + * Current position minus how many rows are before the divider and Minus 1 for the + * divider itself provides the relative index of the frequent contact being displayed. + * Then add the dividerPostion to give the offset into the contacts cursor to get the + * absolute index. + */ + final int rowCount = getRowCount(mDividerPosition); + contactIndex = position - rowCount + Math.min(mDividerPosition, + rowCount * mColumnCount); + resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + } + return resultList; + } + + @Override + public long getItemId(int position) { + // As we show several selectable items for each ListView row, + // we can not determine a stable id. But as we don't rely on ListView's selection, + // this should not be a problem. + return position; + } + + @Override + public boolean areAllItemsEnabled() { + // No dividers, so all items are enabled. + return true; + } + + @Override + public boolean isEnabled(int position) { + return position != getRowCount(mDividerPosition); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + int itemViewType = getItemViewType(position); + + ContactTileRow contactTileRowView = (ContactTileRow) convertView; + ArrayList<ContactEntry> contactList = getItem(position); + + if (contactTileRowView == null) { + // Creating new row if needed + contactTileRowView = new ContactTileRow(mContext, itemViewType); + } + + contactTileRowView.configureRow(contactList, position == getCount() - 1); + return contactTileRowView; + } + + private int getLayoutResourceId(int viewType) { + switch (viewType) { + case ViewTypes.FREQUENT: + return R.layout.phone_favorite_regular_row_view; + case ViewTypes.STARRED_PHONE: + return R.layout.phone_favorite_tile_view; + default: + throw new IllegalArgumentException("Unrecognized viewType " + viewType); + } + } + @Override + public int getViewTypeCount() { + return ViewTypes.COUNT; + } + + @Override + public int getItemViewType(int position) { + if (position < getRowCount(mDividerPosition)) { + return ViewTypes.STARRED_PHONE; + } else { + return ViewTypes.FREQUENT; + } + } + + /** + * Returns the "frequent header" position. Only available when STREQUENT or + * STREQUENT_PHONE_ONLY is used for its display type. + */ + public int getFrequentHeaderPosition() { + return getRowCount(mDividerPosition); + } + + /** + * Acts as a row item composed of {@link ContactTileView} + * + * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return + */ + private class ContactTileRow extends FrameLayout { + private int mItemViewType; + private int mLayoutResId; + + public ContactTileRow(Context context, int itemViewType) { + super(context); + mItemViewType = itemViewType; + mLayoutResId = getLayoutResourceId(mItemViewType); + + // Remove row (but not children) from accessibility node tree. + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } + + /** + * Configures the row to add {@link ContactEntry}s information to the views + */ + public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) { + int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; + + // Adding tiles to row and filling in contact information + for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { + ContactEntry entry = + columnCounter < list.size() ? list.get(columnCounter) : null; + addTileFromEntry(entry, columnCounter, isLastRow); + } + } + + private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { + final ContactTileView contactTile; + + if (getChildCount() <= childIndex) { + + contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); + // Note: the layoutparam set here is only actually used for FREQUENT. + // We override onMeasure() for STARRED and we don't care the layout param there. + Resources resources = mContext.getResources(); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + + params.setMargins( + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), + 0, + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), + 0); + contactTile.setLayoutParams(params); + contactTile.setPhotoManager(mPhotoManager); + contactTile.setListener(mListener); + addView(contactTile); + } else { + contactTile = (ContactTileView) getChildAt(childIndex); + } + contactTile.loadFromContact(entry); + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + // Setting divider visibilities + contactTile.setPaddingRelative(0, 0, + childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, + isLastRow ? 0 : mPaddingInPixels); + break; + case ViewTypes.FREQUENT: + contactTile.setHorizontalDividerVisibility( + isLastRow ? View.GONE : View.VISIBLE); + break; + default: + break; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + onLayoutForTiles(); + return; + default: + super.onLayout(changed, left, top, right, bottom); + return; + } + } + + private void onLayoutForTiles() { + final int count = getChildCount(); + + // Just line up children horizontally. + int childLeft = 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + // Note MeasuredWidth includes the padding. + final int childWidth = child.getMeasuredWidth(); + child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); + childLeft += childWidth; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + switch (mItemViewType) { + case ViewTypes.STARRED_PHONE: + case ViewTypes.STARRED: + onMeasureForTiles(widthMeasureSpec); + return; + default: + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + } + + private void onMeasureForTiles(int widthMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + + final int childCount = getChildCount(); + if (childCount == 0) { + // Just in case... + setMeasuredDimension(width, 0); + return; + } + + // 1. Calculate image size. + // = ([total width] - [total padding]) / [child count] + // + // 2. Set it to width/height of each children. + // If we have a remainder, some tiles will have 1 pixel larger width than its height. + // + // 3. Set the dimensions of itself. + // Let width = given width. + // Let height = image size + bottom paddding. + + final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels; + + // Preferred width / height for images (excluding the padding). + // The actual width may be 1 pixel larger than this if we have a remainder. + final int imageSize = (width - totalPaddingsInPixels) / mColumnCount; + final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels; + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childWidth = imageSize + child.getPaddingRight() + // Compensate for the remainder + + (i < remainder ? 1 : 0); + final int childHeight = imageSize + child.getPaddingBottom(); + child.measure( + MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY) + ); + } + setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom()); + } + } + + protected static class ViewTypes { + public static final int COUNT = 3; + public static final int STARRED = 0; + public static final int FREQUENT = 1; + public static final int STARRED_PHONE = 2; + } +} diff --git a/src/com/android/dialer/list/SmartDialNumberListAdapter.java b/src/com/android/dialer/list/SmartDialNumberListAdapter.java new file mode 100644 index 000000000..0413c4ee5 --- /dev/null +++ b/src/com/android/dialer/list/SmartDialNumberListAdapter.java @@ -0,0 +1,114 @@ +/* + * 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.list; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Callable; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.dialer.dialpad.SmartDialCursorLoader; +import com.android.dialer.dialpad.SmartDialCursorLoader.SmartDialPhoneQuery; +import com.android.dialer.dialpad.SmartDialNameMatcher; +import com.android.dialer.dialpad.SmartDialPrefix; +import com.android.dialer.dialpad.SmartDialMatchPosition; + +import java.util.ArrayList; + +/** + * List adapter to display the SmartDial search results. + */ +public class SmartDialNumberListAdapter extends PhoneNumberListAdapter{ + + private static final String TAG = SmartDialNumberListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + private SmartDialNameMatcher mNameMatcher; + + public SmartDialNumberListAdapter(Context context) { + super(context); + if (DEBUG) { + Log.v(TAG, "Constructing List Adapter"); + } + } + + /** + * Sets query for the SmartDialCursorLoader. + */ + public void configureLoader(SmartDialCursorLoader loader) { + if (DEBUG) { + Log.v(TAG, "Congifugure Loader with query" + getQueryString()); + } + + if (getQueryString() == null) { + mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap()); + loader.configureQuery(""); + } else { + loader.configureQuery(getQueryString()); + mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber( + getQueryString()), SmartDialPrefix.getMap()); + } + } + + /** + * Sets highlight options for a List item in the SmartDial search results. + * @param view ContactListItemView where the result will be displayed. + * @param cursor Object containing information of the associated List item. + */ + @Override + protected void setHighlight(ContactListItemView view, Cursor cursor) { + view.clearHighlightSequences(); + + if (mNameMatcher.matches(cursor.getString(SmartDialPhoneQuery.SMARTDIAL_DISPLAY_NAME))) { + final ArrayList<SmartDialMatchPosition> nameMatches = mNameMatcher.getMatchPositions(); + for (SmartDialMatchPosition match:nameMatches) { + view.addNameHighlightSequence(match.start, match.end); + if (DEBUG) { + Log.v(TAG, cursor.getString(SmartDialPhoneQuery.SMARTDIAL_DISPLAY_NAME) + " " + + mNameMatcher.getQuery() + " " + String.valueOf(match.start)); + } + } + } + + final SmartDialMatchPosition numberMatch = mNameMatcher.matchesNumber(cursor.getString( + SmartDialPhoneQuery.SMARTDIAL_NUMBER)); + if (numberMatch != null) { + view.addNumberHighlightSequence(numberMatch.start, numberMatch.end); + } + } + + /** + * Gets Uri for the list item at the given position. + * @param position Location of the data of interest. + * @return Data Uri of the entry. + */ + public Uri getDataUri(int position) { + Cursor cursor = ((Cursor)getItem(position)); + if (cursor != null) { + long id = cursor.getLong(SmartDialPhoneQuery.SMARTDIAL_ID); + return ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, id); + } else { + Log.w(TAG, "Cursor was null in getDataUri() call. Returning null instead."); + return null; + } + } +} diff --git a/src/com/android/dialer/list/SmartDialNumberPickerFragment.java b/src/com/android/dialer/list/SmartDialNumberPickerFragment.java new file mode 100644 index 000000000..0892de64a --- /dev/null +++ b/src/com/android/dialer/list/SmartDialNumberPickerFragment.java @@ -0,0 +1,76 @@ +/* + * 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.list; + +import android.content.Loader; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.Directory; +import android.util.Log; + +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.PhoneNumberPickerFragment; +import com.android.dialer.dialpad.SmartDialCursorLoader; + +/** + * Implements a fragment to load and display SmartDial search results. + */ +public class SmartDialNumberPickerFragment extends PhoneNumberPickerFragment { + + private static final String TAG = SmartDialNumberPickerFragment.class.getSimpleName(); + + /** + * Creates a SmartDialListAdapter to display and operate on search results. + * @return + */ + @Override + protected ContactEntryListAdapter createListAdapter() { + SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(super.usesCallableUri()); + return adapter; + } + + /** + * Creates a SmartDialCursorLoader object to load query results. + */ + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + /** SmartDial does not support Directory Load, falls back to normal search instead. */ + if (id == getDirectoryLoaderId()) { + Log.v(TAG, "Directory load"); + return super.onCreateLoader(id, args); + } else { + Log.v(TAG, "Creating loader"); + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext()); + adapter.configureLoader(loader); + return loader; + } + } + + /** + * Gets the Phone Uri of an entry for calling. + * @param position Location of the data of interest. + * @return Phone Uri to establish a phone call. + */ + @Override + protected Uri getPhoneUri(int position) { + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + return adapter.getDataUri(position); + } +} diff --git a/src/com/android/dialer/list/SmartDialSearchFragment.java b/src/com/android/dialer/list/SmartDialSearchFragment.java new file mode 100644 index 000000000..3c1e51343 --- /dev/null +++ b/src/com/android/dialer/list/SmartDialSearchFragment.java @@ -0,0 +1,120 @@ +/* + * 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.list; + +import android.app.Activity; +import android.content.Loader; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.Directory; +import android.util.Log; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; + +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.PhoneNumberPickerFragment; +import com.android.dialer.dialpad.SmartDialCursorLoader; + +/** + * Implements a fragment to load and display SmartDial search results. + */ +public class SmartDialSearchFragment extends PhoneNumberPickerFragment { + private static final String TAG = SmartDialSearchFragment.class.getSimpleName(); + + private OnListFragmentScrolledListener mActivityScrollListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + setQuickContactEnabled(true); + setDarkTheme(false); + setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(true /* opposite */)); + setUseCallableUri(true); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement OnListFragmentScrolledListener"); + } + } + + @Override + public void onStart() { + super.onStart(); + getListView().setOnScrollListener(new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + } + }); + } + + /** + * Creates a SmartDialListAdapter to display and operate on search results. + */ + @Override + protected ContactEntryListAdapter createListAdapter() { + SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); + adapter.setUseCallableUri(super.usesCallableUri()); + adapter.setQuickContactEnabled(true); + return adapter; + } + + /** + * Creates a SmartDialCursorLoader object to load query results. + */ + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // Smart dialing does not support Directory Load, falls back to normal search instead. + if (id == getDirectoryLoaderId()) { + Log.v(TAG, "Directory load"); + return super.onCreateLoader(id, args); + } else { + Log.v(TAG, "Creating loader"); + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext()); + adapter.configureLoader(loader); + return loader; + } + } + + /** + * Gets the Phone Uri of an entry for calling. + * @param position Location of the data of interest. + * @return Phone Uri to establish a phone call. + */ + @Override + protected Uri getPhoneUri(int position) { + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + return adapter.getDataUri(position); + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + // This hides the "All contacts with phone numbers" header in the search fragment + getAdapter().setHasHeader(0, false); + } +} |