diff options
author | Yorke Lee <yorkelee@google.com> | 2013-06-26 18:24:32 -0700 |
---|---|---|
committer | Yorke Lee <yorkelee@google.com> | 2013-07-16 12:28:32 -0700 |
commit | dfb2eee7d98f8540fd1614db66bb03e8e1f3a26a (patch) | |
tree | a33447c38bbfa4a055e78c909176dde351b419d9 /src | |
parent | 6b049128c51b90e17ae14856d98130a22d3a5433 (diff) |
Initial commit of all new dialer activities, layouts and styles
As far as possible, this change does not modify any behavior/look
of the existing dialer. All modifications to classes/layouts/style attributes
that would affect functionality of the old dialer are instead done in
separate new files.
Added new versions of all activities, fragments, layouts, menus
and resources that have been rewritten or modified.
The activities do not have intent filters yet in order to not interfere
with the existing dialer.
Added some new values in colors/styles/dimens for the newly added
layouts.
Added NewCallLogActivity to separate the CallLogFragment from
DialtactsActivity. All call log and voicemail entries are now presented
in a separate (New)CallLogActivity.
IntentProvider.getCallDetailIntent now takes a cursor instead of an
adapter for more flexibility.
Add OnListFragmentScrolledListener interface for DialtactsActivity
to receive callback when one of its children fragment is scrolled.
Added slide in and slide out animations for DialpadFragment.
Add slide up and hide animations for search view
All menu options now show up in a PopupMenu in NewPhoneFavoriteFragment.
Added call log adapter into NewPhoneFavoriteMergedAdapter.
Rewrote layout of DialpadFragment to show up as a fragment partially overlaying
the screen.
Removed some unused code in DialpadFragment (smart dialing, menu handling)
Add fragments and adapters for SmartDial.
- Update Dialer database to support highlight masking and record contact
data Uri, as well as photo uri
- Add a fragment and adapter for smart dialing
- Add SmartDialCursorLoader to load SmartDial results.
- Typing in the dialpad now returns smart dialing results instead of regular
search results
QuickContactTiles for all sub-adapters of PhoneFavoriteMergedAdapter now use
light theme instead of the the default dark theme.
Removed all redundant call log filter and contacts to display filter code
Moved Dialer specific UI list-related classes to the Dialer package
Change-Id: I34885813e4fa79b69e29ac870a87a56d6f08a5e7
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); + } +} |