First pass at tabbed contact viewing.
ScrollingTabWidget - This is a generic widget for displaying tab like
elements, that may not fit on the screen. The visual design for this
widget is still in flux.
BaseContactCardActivity - This is an abstract class that should be
extended by any Activity that displays information about a specific
contact and allows filtering on different RawContacts associated with
the contact. ViewContactActivity.java extends this class.
EditContactActivity will want to eventually. The abstract class
implements OnTabSelectionChangedListener and will get called on
onTabSelectionChanged() when a new tab is selected. This way the
activity can react to tab selection changes.
ViewContactActivity - This now extends BaseContactActivity.
diff --git a/src/com/android/contacts/BaseContactCardActivity.java b/src/com/android/contacts/BaseContactCardActivity.java
new file mode 100644
index 0000000..d6ed5fd
--- /dev/null
+++ b/src/com/android/contacts/BaseContactCardActivity.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2008 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.contacts;
+
+import com.android.contacts.ScrollingTabWidget.OnTabSelectionChangedListener;
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.SocialContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.SocialContract.Activities;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * The base Activity class for viewing and editing a contact.
+ */
+public abstract class BaseContactCardActivity extends Activity
+ implements QueryCompleteListener, OnTabSelectionChangedListener, View.OnClickListener {
+
+ private static final String TAG = "BaseContactCardActivity";
+
+ private SparseArray<Long> mTabRawContactIdMap;
+ protected Uri mUri;
+ protected long mContactId;
+ protected ScrollingTabWidget mTabWidget;
+ private NotifyingAsyncQueryHandler mHandler;
+ private TextView mDisplayNameView;
+ private TextView mPhoneticNameView;
+ private CheckBox mStarredView;
+ private ImageView mPhotoView;
+ private TextView mStatusView;
+
+ private int mNoPhotoResource;
+
+ protected LayoutInflater mInflater;
+
+ //Projection used for the query that determines which tabs to add.
+ protected static final String[] TAB_PROJECTION = new String[] {
+ RawContacts._ID,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE
+ };
+ protected static final int TAB_CONTACT_ID_COLUMN_INDEX = 0;
+ protected static final int TAB_ACCOUNT_NAME_COLUMN_INDEX = 1;
+ protected static final int TAB_ACCOUNT_TYPE_COLUMN_INDEX = 2;
+
+ //Projection used for the summary info in the header.
+ protected static final String[] HEADER_PROJECTION = new String[] {
+ Contacts.DISPLAY_NAME,
+ Contacts.STARRED,
+ Contacts.PHOTO_ID,
+ };
+ protected static final int HEADER_DISPLAY_NAME_COLUMN_INDEX = 0;
+ //TODO: We need to figure out how we're going to get the phonetic name.
+ //static final int HEADER_PHONETIC_NAME_COLUMN_INDEX
+ protected static final int HEADER_STARRED_COLUMN_INDEX = 1;
+ protected static final int HEADER_PHOTO_ID_COLUMN_INDEX = 2;
+
+ //Projection used for finding the most recent social status.
+ protected static final String[] SOCIAL_PROJECTION = new String[] {
+ Activities.TITLE,
+ Activities.PUBLISHED,
+ };
+ protected static final int SOCIAL_TITLE_COLUMN_INDEX = 0;
+ protected static final int SOCIAL_PUBLISHED_COLUMN_INDEX = 1;
+
+ private static final int TOKEN_HEADER = 0;
+ private static final int TOKEN_SOCIAL = 1;
+ private static final int TOKEN_TABS = 2;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final Intent intent = getIntent();
+ mUri = intent.getData();
+ mContactId = ContentUris.parseId(mUri);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.contact_card_layout);
+
+ mTabWidget = (ScrollingTabWidget) findViewById(R.id.tab_widget);
+ mDisplayNameView = (TextView) findViewById(R.id.name);
+ mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
+ mStarredView = (CheckBox) findViewById(R.id.star);
+ mStarredView.setOnClickListener(this);
+ mPhotoView = (ImageView) findViewById(R.id.photo);
+ mStatusView = (TextView) findViewById(R.id.status);
+
+ mTabWidget.setTabSelectionListener(this);
+ mTabRawContactIdMap = new SparseArray<Long>();
+
+ // Set the photo with a random "no contact" image
+ long now = SystemClock.elapsedRealtime();
+ int num = (int) now & 0xf;
+ if (num < 9) {
+ // Leaning in from right, common
+ mNoPhotoResource = R.drawable.ic_contact_picture;
+ } else if (num < 14) {
+ // Leaning in from left uncommon
+ mNoPhotoResource = R.drawable.ic_contact_picture_2;
+ } else {
+ // Coming in from the top, rare
+ mNoPhotoResource = R.drawable.ic_contact_picture_3;
+ }
+
+ mHandler = new NotifyingAsyncQueryHandler(this, this);
+
+ setupTabs();
+ setupHeader();
+ }
+
+ private void setupHeader() {
+ Uri headerUri = Uri.withAppendedPath(mUri, "data");
+
+ Uri socialUri = ContentUris.withAppendedId(
+ SocialContract.Activities.CONTENT_CONTACT_STATUS_URI, mContactId);
+
+ mHandler.startQuery(TOKEN_HEADER, null, headerUri, HEADER_PROJECTION, null, null, null);
+ mHandler.startQuery(TOKEN_SOCIAL, null, socialUri, SOCIAL_PROJECTION, null, null, null);
+ }
+
+ private void setupTabs() {
+ Uri tabsUri = Uri.withAppendedPath(mUri, "raw_contacts");
+ mHandler.startQuery(TOKEN_TABS, null, tabsUri, TAB_PROJECTION, null, null, null);
+ }
+
+ /**
+ * Return the contactId associated with the tab at an index.
+ *
+ * @param index The index of the tab in question.
+ * @return The contactId associated with the tab at the specified index.
+ */
+ protected long getTabRawContactId(int index) {
+ return mTabRawContactIdMap.get(index);
+ }
+
+ /** {@inheritDoc} */
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+ try{
+ if (token == TOKEN_HEADER) {
+ bindHeader(cursor);
+ } else if (token == TOKEN_SOCIAL) {
+ bindSocial(cursor);
+ } else if (token == TOKEN_TABS) {
+ clearCurrentTabs();
+ bindTabs(cursor);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Adds a tab for each {@link RawContact} associated with this contact.
+ * Override this method if you want to additional tabs and/or different
+ * tabs for your activity.
+ *
+ * @param tabsCursor A cursor over all the RawContacts associated with
+ * the contact being displayed. Use {@link TAB_CONTACT_ID_COLUMN_INDEX},
+ * {@link TAB_ACCOUNT_NAME_COLUMN_INDEX}, {@link TAB_ACCOUNT_TYPE_COLUMN_INDEX},
+ * and {@link TAB_PACKAGE_COLUMN_INDEX} as column indexes on the cursor.
+ */
+ protected void bindTabs(Cursor tabsCursor) {
+ while (tabsCursor.moveToNext()) {
+ long contactId = tabsCursor.getLong(TAB_CONTACT_ID_COLUMN_INDEX);
+
+ //TODO: figure out how to get the icon
+ Drawable tabIcon = null;
+ addTab(contactId, null, tabIcon);
+ }
+ selectDefaultTab();
+
+ }
+
+ //TODO: This will be part of the ContactHeaderWidget eventually.
+ protected void bindHeader(Cursor c) {
+ if (c == null) {
+ return;
+ }
+ if (c.moveToFirst()) {
+ //Set name
+ String displayName = c.getString(HEADER_DISPLAY_NAME_COLUMN_INDEX);
+ Log.i(TAG, displayName);
+ mDisplayNameView.setText(displayName);
+ //TODO: Bring back phonetic name
+ /*if (mPhoneticNameView != null) {
+ String phoneticName = c.getString(CONTACT_PHONETIC_NAME_COLUMN);
+ mPhoneticNameView.setText(phoneticName);
+ }*/
+
+ //Set starred
+ boolean starred = c.getInt(HEADER_STARRED_COLUMN_INDEX) == 1;
+ mStarredView.setChecked(starred);
+
+ //Set the photo
+ long photoId = c.getLong(HEADER_PHOTO_ID_COLUMN_INDEX);
+ Bitmap photoBitmap = ContactsUtils.loadContactPhoto(
+ this, photoId, null);
+ if (photoBitmap == null) {
+ photoBitmap = ContactsUtils.loadPlaceholderPhoto(mNoPhotoResource, this, null);
+ }
+ mPhotoView.setImageBitmap(photoBitmap);
+ }
+ }
+
+ //TODO: This will be part of the ContactHeaderWidget eventually.
+ protected void bindSocial(Cursor c) {
+ if (c == null) {
+ return;
+ }
+ if (c.moveToFirst()) {
+ String status = c.getString(SOCIAL_TITLE_COLUMN_INDEX);
+ mStatusView.setText(status);
+ }
+ }
+
+ //TODO: This will be part of the ContactHeaderWidget eventually.
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.star: {
+ Uri uri = Uri.withAppendedPath(mUri, "data");
+ Cursor c = getContentResolver().query(uri, HEADER_PROJECTION, null, null, null);
+ try {
+ c.moveToFirst();
+ int oldStarredState = c.getInt(HEADER_STARRED_COLUMN_INDEX);
+ ContentValues values = new ContentValues(1);
+ values.put(Contacts.STARRED, oldStarredState == 1 ? 0 : 1);
+ getContentResolver().update(mUri, values, null, null);
+ } finally {
+ c.close();
+ }
+ setupHeader();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Add a tab to be displayed in the {@link ScrollingTabWidget}.
+ *
+ * @param contactId The contact id associated with the tab.
+ * @param label A label to display in the tab indicator.
+ * @param icon An icon to display in the tab indicator.
+ */
+ protected void addTab(long contactId, String label, Drawable icon) {
+ addTab(contactId, createTabIndicatorView(label, icon));
+ }
+
+ /**
+ * Add a tab to be displayed in the {@link ScrollingTabWidget}.
+ *
+ * @param contactId The contact id associated with the tab.
+ * @param view A view to use as the tab indicator.
+ */
+ protected void addTab(long contactId, View view) {
+ mTabRawContactIdMap.put(mTabWidget.getTabCount(), contactId);
+ mTabWidget.addTab(view);
+ }
+
+
+ protected void clearCurrentTabs() {
+ mTabRawContactIdMap.clear();
+ mTabWidget.removeAllTabs();
+ }
+
+ /**
+ * Makes the default tab selection. This is called after the tabs have been
+ * bound for the first time, and whenever a new intent is received. Override
+ * this method if you want to customize the default tab behavior.
+ */
+ protected void selectDefaultTab() {
+ // Select the first tab.
+ mTabWidget.setCurrentTab(0);
+ }
+
+ @Override
+ public void onNewIntent(Intent newIntent) {
+ setIntent(newIntent);
+ selectDefaultTab();
+ mUri = newIntent.getData();
+ }
+
+ /**
+ * Utility for creating a standard tab indicator view.
+ *
+ * @param label The label to display in the tab indicator. If null, not label will be displayed.
+ * @param icon The icon to display. If null, no icon will be displayed.
+ * @return The tab indicator View.
+ */
+ protected View createTabIndicatorView(String label, Drawable icon) {
+ View tabIndicator = mInflater.inflate(R.layout.tab_indicator, mTabWidget, false);
+
+ final TextView tv = (TextView) tabIndicator.findViewById(R.id.tab_title);
+ tv.setText(label);
+
+ final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.tab_icon);
+ iconView.setImageDrawable(icon);
+
+ return tabIndicator;
+ }
+
+}
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 5b944cb..825ded0 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -163,7 +163,7 @@
return null;
}
- byte[] data = cursor.getBlob(bitmapColumnIndex);;
+ byte[] data = cursor.getBlob(bitmapColumnIndex);
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
}
@@ -184,7 +184,7 @@
placeholderImageResource, options);
}
- public static Bitmap loadContactPhoto(Context context, int photoId,
+ public static Bitmap loadContactPhoto(Context context, long photoId,
BitmapFactory.Options options) {
Cursor photoCursor = null;
Bitmap photoBm = null;
diff --git a/src/com/android/contacts/NoDragHorizontalScrollView.java b/src/com/android/contacts/NoDragHorizontalScrollView.java
new file mode 100644
index 0000000..94d8c13
--- /dev/null
+++ b/src/com/android/contacts/NoDragHorizontalScrollView.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2009 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.contacts;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.HorizontalScrollView;
+
+/* Simple extension of {@link HorizontalScrollView} that disallows drag scrolling */
+public class NoDragHorizontalScrollView extends HorizontalScrollView {
+
+ public NoDragHorizontalScrollView(Context context) {
+ super(context);
+ }
+
+ public NoDragHorizontalScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NoDragHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return false;
+ }
+
+}
diff --git a/src/com/android/contacts/ScrollingTabWidget.java b/src/com/android/contacts/ScrollingTabWidget.java
new file mode 100644
index 0000000..f6f1ab1
--- /dev/null
+++ b/src/com/android/contacts/ScrollingTabWidget.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2009 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.contacts;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TabHost;
+
+/*
+ * Tab widget that can contain more tabs than can fit on screen at once and scroll over them.
+ */
+public class ScrollingTabWidget extends RelativeLayout
+ implements OnClickListener {
+
+ private static final String TAG = "ScrollingTabWidget";
+
+ private OnTabSelectionChangedListener mSelectionChangedListener;
+ private int mSelectedTab = 0;
+ private LinearLayout mLeftArrowView;
+ private LinearLayout mRightArrowView;
+ private NoDragHorizontalScrollView mTabsScrollWrapper;
+ private LinearLayout mTabsView;
+ private LayoutInflater mInflater;
+ private Drawable mDividerDrawable;
+
+ // Keeps track of the left most visible tab.
+ private int mLeftMostVisibleTabIndex = 0;
+
+ public ScrollingTabWidget(Context context) {
+ this(context, null);
+ }
+
+ public ScrollingTabWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ScrollingTabWidget(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+
+ mInflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ mLeftArrowView = (LinearLayout) mInflater.inflate(R.layout.tab_left_arrow, this, false);
+ mLeftArrowView.setOnClickListener(this);
+ mRightArrowView = (LinearLayout) mInflater.inflate(R.layout.tab_right_arrow, this, false);
+ mRightArrowView.setOnClickListener(this);
+ mTabsScrollWrapper = (NoDragHorizontalScrollView) mInflater.inflate(
+ R.layout.tab_layout, this, false);
+ mTabsView = (LinearLayout) mTabsScrollWrapper.findViewById(android.R.id.tabs);
+ mDividerDrawable = mContext.getResources().getDrawable(
+ R.drawable.tab_divider);
+
+ mLeftArrowView.setVisibility(View.INVISIBLE);
+ mRightArrowView.setVisibility(View.INVISIBLE);
+
+ addView(mTabsScrollWrapper);
+ addView(mLeftArrowView);
+ addView(mRightArrowView);
+ }
+
+ protected void updateArrowVisibility() {
+ int scrollViewLeftEdge = mTabsScrollWrapper.getScrollX();
+ int tabsViewLeftEdge = mTabsView.getLeft();
+ int scrollViewRightEdge = scrollViewLeftEdge + mTabsScrollWrapper.getWidth();
+ int tabsViewRightEdge = mTabsView.getRight();
+
+ int rightArrowCurrentVisibility = mRightArrowView.getVisibility();
+ if (scrollViewRightEdge == tabsViewRightEdge
+ && rightArrowCurrentVisibility == View.VISIBLE) {
+ mRightArrowView.setVisibility(View.INVISIBLE);
+ } else if (scrollViewRightEdge < tabsViewRightEdge
+ && rightArrowCurrentVisibility != View.VISIBLE) {
+ mRightArrowView.setVisibility(View.VISIBLE);
+ }
+
+ int leftArrowCurrentVisibility = mLeftArrowView.getVisibility();
+ if (scrollViewLeftEdge == tabsViewLeftEdge
+ && leftArrowCurrentVisibility == View.VISIBLE) {
+ mLeftArrowView.setVisibility(View.INVISIBLE);
+ } else if (scrollViewLeftEdge > tabsViewLeftEdge
+ && leftArrowCurrentVisibility != View.VISIBLE) {
+ mLeftArrowView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the tab indicator view at the given index.
+ *
+ * @param index the zero-based index of the tab indicator view to return
+ * @return the tab indicator view at the given index
+ */
+ public View getChildTabViewAt(int index) {
+ return mTabsView.getChildAt(index*2);
+ }
+
+ /**
+ * Returns the number of tab indicator views.
+ *
+ * @return the number of tab indicator views.
+ */
+ public int getTabCount() {
+ int children = mTabsView.getChildCount();
+ return (children + 1) / 2;
+ }
+
+ public void removeAllTabs() {
+ mTabsView.removeAllViews();
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ updateArrowVisibility();
+ super.dispatchDraw(canvas);
+ }
+
+ /**
+ * Sets the current tab.
+ * This method is used to bring a tab to the front of the Widget,
+ * and is used to post to the rest of the UI that a different tab
+ * has been brought to the foreground.
+ *
+ * Note, this is separate from the traditional "focus" that is
+ * employed from the view logic.
+ *
+ * For instance, if we have a list in a tabbed view, a user may be
+ * navigating up and down the list, moving the UI focus (orange
+ * highlighting) through the list items. The cursor movement does
+ * not effect the "selected" tab though, because what is being
+ * scrolled through is all on the same tab. The selected tab only
+ * changes when we navigate between tabs (moving from the list view
+ * to the next tabbed view, in this example).
+ *
+ * To move both the focus AND the selected tab at once, please use
+ * {@link #focusCurrentTab}. Normally, the view logic takes care of
+ * adjusting the focus, so unless you're circumventing the UI,
+ * you'll probably just focus your interest here.
+ *
+ * @param index The tab that you want to indicate as the selected
+ * tab (tab brought to the front of the widget)
+ *
+ * @see #focusCurrentTab
+ */
+ public void setCurrentTab(int index) {
+ if (index < 0 || index >= getTabCount()) {
+ return;
+ }
+
+ getChildTabViewAt(mSelectedTab).setSelected(false);
+ mSelectedTab = index;
+ getChildTabViewAt(mSelectedTab).setSelected(true);
+ }
+
+ /**
+ * Sets the current tab and focuses the UI on it.
+ * This method makes sure that the focused tab matches the selected
+ * tab, normally at {@link #setCurrentTab}. Normally this would not
+ * be an issue if we go through the UI, since the UI is responsible
+ * for calling TabWidget.onFocusChanged(), but in the case where we
+ * are selecting the tab programmatically, we'll need to make sure
+ * focus keeps up.
+ *
+ * @param index The tab that you want focused (highlighted in orange)
+ * and selected (tab brought to the front of the widget)
+ *
+ * @see #setCurrentTab
+ */
+ public void focusCurrentTab(int index) {
+ final int oldTab = mSelectedTab;
+
+ // set the tab
+ setCurrentTab(index);
+
+ // change the focus if applicable.
+ if (oldTab != index) {
+ getChildTabViewAt(index).requestFocus();
+ }
+ }
+
+ /**
+ * Adds a tab to the list of tabs. The tab's indicator view is specified
+ * by a layout id. InflateException will be thrown if there is a problem
+ * inflating.
+ *
+ * @param layoutResId The layout id to be inflated to make the tab indicator.
+ */
+ public void addTab(int layoutResId) {
+ addTab(mInflater.inflate(layoutResId, mTabsView, false));
+ }
+
+ /**
+ * Adds a tab to the list of tabs. The tab's indicator view must be provided.
+ *
+ * @param child
+ */
+ public void addTab(View child) {
+ if (child == null) {
+ return;
+ }
+
+ if (child.getLayoutParams() == null) {
+ final LayoutParams lp = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(0, 0, 0, 0);
+ child.setLayoutParams(lp);
+ }
+
+ // Ensure you can navigate to the tab with the keyboard, and you can touch it
+ child.setFocusable(true);
+ child.setClickable(true);
+ child.setOnClickListener(new TabClickListener());
+ child.setOnFocusChangeListener(new TabFocusListener());
+
+ // If we already have at least one tab, then add a divider before adding the next tab.
+ if (getTabCount() > 0) {
+ View divider = new View(mContext);
+ final LayoutParams lp = new LayoutParams(
+ mDividerDrawable.getIntrinsicWidth(),
+ ViewGroup.LayoutParams.FILL_PARENT);
+ lp.setMargins(0, 0, 0, 0);
+ divider.setLayoutParams(lp);
+ divider.setBackgroundDrawable(mDividerDrawable);
+ mTabsView.addView(divider);
+ }
+ mTabsView.addView(child);
+ }
+
+ /**
+ * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
+ * user clicked on a tab indicator.
+ */
+ void setTabSelectionListener(OnTabSelectionChangedListener listener) {
+ mSelectionChangedListener = listener;
+ }
+
+ private class TabFocusListener implements OnFocusChangeListener {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ for (int i = 0; i < getTabCount(); i++) {
+ if (getChildTabViewAt(i) == v) {
+ setCurrentTab(i);
+ mSelectionChangedListener.onTabSelectionChanged(i, false);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private class TabClickListener implements OnClickListener {
+ public void onClick(View v) {
+ for (int i = 0; i < getTabCount(); i++) {
+ if (getChildTabViewAt(i) == v) {
+ setCurrentTab(i);
+ mSelectionChangedListener.onTabSelectionChanged(i, true);
+ break;
+ }
+ }
+ }
+ }
+
+ static interface OnTabSelectionChangedListener {
+ /**
+ * Informs the tab widget host which tab was selected. It also indicates
+ * if the tab was clicked/pressed or just focused into.
+ *
+ * @param tabIndex index of the tab that was selected
+ * @param clicked whether the selection changed due to a touch/click
+ * or due to focus entering the tab through navigation. Pass true
+ * if it was due to a press/click and false otherwise.
+ */
+ void onTabSelectionChanged(int tabIndex, boolean clicked);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ boolean handled = super.dispatchKeyEvent(event);
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // If tabs move from left/right events we must update mLeftMostVisibleTabIndex.
+ updateLeftMostVisible();
+ break;
+ }
+ }
+
+ return handled;
+ }
+
+ public void onClick(View v) {
+ if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
+ tabScroll(true /* right */);
+ } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
+ tabScroll(false /* left */);
+ }
+ }
+
+ /*
+ * Updates our record of the left most visible tab. We keep track of this explicitly
+ * on arrow clicks, but need to re-calibrate after focus navigation.
+ */
+ protected void updateLeftMostVisible() {
+ int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
+
+ if (mLeftArrowView.getVisibility() == View.VISIBLE) {
+ viewableLeftEdge += mLeftArrowView.getWidth();
+ }
+
+ for (int i = 0; i < getTabCount(); i++) {
+ View tab = getChildTabViewAt(i);
+ int tabLeftEdge = tab.getLeft();
+ if (tabLeftEdge >= viewableLeftEdge) {
+ mLeftMostVisibleTabIndex = i;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Scrolls the tabs by exactly one tab width.
+ *
+ * @param directionRight if true, scroll to the right, if false, scroll to the left.
+ */
+ protected void tabScroll(boolean directionRight) {
+ int scrollWidth = 0;
+ View newLeftMostVisibleTab = null;
+ if (directionRight) {
+ newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
+ } else {
+ newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
+ }
+
+ scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
+ if (mLeftMostVisibleTabIndex > 0) {
+ scrollWidth -= mLeftArrowView.getWidth();
+ }
+ mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
+ }
+
+}
diff --git a/src/com/android/contacts/StyleManager.java b/src/com/android/contacts/StyleManager.java
index d14b1c5..d2f6e09 100644
--- a/src/com/android/contacts/StyleManager.java
+++ b/src/com/android/contacts/StyleManager.java
@@ -80,7 +80,13 @@
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
- context.registerReceiver(this, filter);
+
+ // We use getApplicationContext() so that the broadcast reciever can stay registered for
+ // the length of the application lifetime (instead of the calling activity's lifetime).
+ // This is so that we can notified of package changes, and purge the cache accordingly,
+ // but not be woken up if the application process isn't already running, since we will
+ // have no cache to clear at that point.
+ context.getApplicationContext().registerReceiver(this, filter);
}
@Override
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index 789e7a7..e549da7 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -71,6 +71,7 @@
import android.view.ContextMenu;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -78,6 +79,7 @@
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.CheckBox;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
@@ -88,9 +90,9 @@
/**
* Displays the details of a specific contact.
*/
-public class ViewContactActivity extends ListActivity
- implements View.OnCreateContextMenuListener, View.OnClickListener,
- DialogInterface.OnClickListener {
+public class ViewContactActivity extends BaseContactCardActivity
+ implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener,
+ AdapterView.OnItemClickListener {
private static final String TAG = "ViewContact";
private static final String SHOW_BARCODE_INTENT = "com.google.zxing.client.android.ENCODE";
@@ -113,10 +115,13 @@
private ViewAdapter mAdapter;
private int mNumPhoneNumbers = 0;
+ private static final long ALL_CONTACTS_ID = -1;
+ private long mRawContactId = ALL_CONTACTS_ID;
+
/**
* A list of distinct contact IDs included in the current contact.
*/
- private ArrayList<Long> mContactIds = new ArrayList<Long>();
+ private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
/* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
/* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
@@ -158,55 +163,21 @@
finish();
}
- public void onClick(View view) {
- if (!mObserverRegistered) {
- return;
- }
- switch (view.getId()) {
- case R.id.star: {
- mCursor.moveToFirst();
- int oldStarredState = mCursor.getInt(CONTACT_STARRED_COLUMN);
- ContentValues values = new ContentValues(1);
- values.put(Contacts.STARRED, oldStarredState == 1 ? 0 : 1);
- getContentResolver().update(mUri, values, null, null);
- break;
- }
- }
- }
-
- private TextView mNameView;
- private TextView mPhoneticNameView; // may be null in some locales
- private ImageView mPhotoView;
- private int mNoPhotoResource;
- private CheckBox mStarView;
+ private FrameLayout mTabContentLayout;
+ private ListView mListView;
private boolean mShowSmsLinksForAllPhones;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- setContentView(R.layout.view_contact);
- getListView().setOnCreateContextMenuListener(this);
+ mListView = new ListView(this);
+ mListView.setOnCreateContextMenuListener(this);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ mListView.setOnItemClickListener(this);
- mNameView = (TextView) findViewById(R.id.name);
- mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
- mPhotoView = (ImageView) findViewById(R.id.photo);
- mStarView = (CheckBox) findViewById(R.id.star);
- mStarView.setOnClickListener(this);
-
- // Set the photo with a random "no contact" image
- long now = SystemClock.elapsedRealtime();
- int num = (int) now & 0xf;
- if (num < 9) {
- // Leaning in from right, common
- mNoPhotoResource = R.drawable.ic_contact_picture;
- } else if (num < 14) {
- // Leaning in from left uncommon
- mNoPhotoResource = R.drawable.ic_contact_picture_2;
- } else {
- // Coming in from the top, rare
- mNoPhotoResource = R.drawable.ic_contact_picture_3;
- }
+ mTabContentLayout = (FrameLayout) findViewById(android.R.id.tabcontent);
+ mTabContentLayout.addView(mListView);
mUri = getIntent().getData();
mAggDataUri = Uri.withAppendedPath(mUri, "data");
@@ -279,20 +250,28 @@
return null;
}
+ @Override
+ protected void bindTabs(Cursor tabsCursor) {
+ if (tabsCursor.getCount() > 1) {
+ addAllTab();
+ }
+ super.bindTabs(tabsCursor);
+ }
+
+ private void addAllTab() {
+ View allTabIndicator = mInflater.inflate(R.layout.all_tab_indicator, mTabWidget, false);
+ addTab(ALL_CONTACTS_ID, allTabIndicator);
+ }
+
+ public void onTabSelectionChanged(int tabIndex, boolean clicked) {
+ long rawContactId = getTabRawContactId(tabIndex);
+ mRawContactId = rawContactId;
+ dataChanged();
+ }
+
private void dataChanged() {
mCursor.requery();
if (mCursor.moveToFirst()) {
- // Set the star
- mStarView.setChecked(mCursor.getInt(CONTACT_STARRED_COLUMN) == 1 ? true : false);
-
- //Set the photo
- int photoId = mCursor.getInt(CONTACT_PHOTO_ID);
- Bitmap photoBitmap = ContactsUtils.loadContactPhoto(
- this, photoId, null);
- if (photoBitmap == null) {
- photoBitmap = ContactsUtils.loadPlaceholderPhoto(mNoPhotoResource, this, null);
- }
- mPhotoView.setImageBitmap(photoBitmap);
// Build up the contact entries
buildEntries(mCursor);
@@ -305,7 +284,7 @@
if (mAdapter == null) {
mAdapter = new ViewAdapter(this, mSections);
- setListAdapter(mAdapter);
+ mListView.setAdapter(mAdapter);
} else {
mAdapter.setSections(mSections, SHOW_SEPARATORS);
}
@@ -347,7 +326,7 @@
menu.removeItem(MENU_ITEM_SHOW_BARCODE);
}
- boolean isAggregate = mContactIds.size() > 1;
+ boolean isAggregate = mRawContactIds.size() > 1;
menu.findItem(MENU_ITEM_SPLIT_AGGREGATE).setEnabled(isAggregate);
return true;
}
@@ -626,7 +605,7 @@
// Fall through and try to call the contact
}
- int index = getListView().getSelectedItemPosition();
+ int index = mListView.getSelectedItemPosition();
if (index != -1) {
ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
if (entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
@@ -649,8 +628,7 @@
return super.onKeyDown(keyCode, event);
}
- @Override
- protected void onListItemClick(ListView l, View v, int position, long id) {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
if (entry != null) {
Intent intent = entry.intent;
@@ -697,11 +675,10 @@
mSections.get(i).clear();
}
- mContactIds.clear();
+ mRawContactIds.clear();
// Build up method entries
if (mUri != null) {
- Bitmap photoBitmap = null;
aggCursor.moveToPosition(-1);
while (aggCursor.moveToNext()) {
final String mimetype = aggCursor.getString(DATA_MIMETYPE_COLUMN);
@@ -713,9 +690,15 @@
entry.id = id;
entry.uri = uri;
entry.mimetype = mimetype;
+ // TODO: entry.contactId should be renamed to entry.rawContactId
entry.contactId = aggCursor.getLong(DATA_CONTACT_ID_COLUMN);
- if (!mContactIds.contains(entry.contactId)) {
- mContactIds.add(entry.contactId);
+ if (!mRawContactIds.contains(entry.contactId)) {
+ mRawContactIds.add(entry.contactId);
+ }
+
+ // This performs the tab filtering
+ if (mRawContactId != entry.contactId && mRawContactId != ALL_CONTACTS_ID) {
+ continue;
}
if (mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
@@ -828,20 +811,6 @@
entry.actionIcon = android.R.drawable.sym_action_chat;
mImEntries.add(entry);
}
- // Set the name
- } else if (mimetype.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
- String name = mCursor.getString(DATA_9_COLUMN);
- if (TextUtils.isEmpty(name)) {
- mNameView.setText(getText(android.R.string.unknownName));
- } else {
- mNameView.setText(name);
- }
- /*
- if (mPhoneticNameView != null) {
- String phoneticName = mCursor.getString(CONTACT_PHONETIC_NAME_COLUMN);
- mPhoneticNameView.setText(phoneticName);
- }
- */
// Build organization entries
} else if (mimetype.equals(CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) {
final String company = aggCursor.getString(DATA_3_COLUMN);