Modal field editors
Bug:2680726
Change-Id: I167b02c50653abfa22ed72023993c9dd31f752a9
diff --git a/src/com/android/contacts/Collapser.java b/src/com/android/contacts/Collapser.java
index 3872dfd..d072dce 100644
--- a/src/com/android/contacts/Collapser.java
+++ b/src/com/android/contacts/Collapser.java
@@ -16,7 +16,6 @@
package com.android.contacts;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
@@ -44,7 +43,7 @@
/**
* Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed
- * if {@link Collapsible#shouldCollapseWith(Object) return strue, and are collapsed
+ * if {@link Collapsible#shouldCollapseWith(Object)} returns strue, and are collapsed
* through the {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
*
* @param list ArrayList of Objects of type <T extends Collapsible<T>> to be collapsed.
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
deleted file mode 100644
index 90a41ca..0000000
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright (C) 2007 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.net.Uri;
-import android.os.Parcel;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import java.util.ArrayList;
-
-public abstract class ContactEntryAdapter<E extends ContactEntryAdapter.Entry>
- extends BaseAdapter {
-
- protected ArrayList<ArrayList<E>> mSections;
- protected LayoutInflater mInflater;
- protected Context mContext;
- protected boolean mSeparators;
-
- /**
- * Base class for adapter entries.
- */
- public static class Entry {
- public int type = -1;
- public String label;
- public String data;
- public Uri uri;
- public long id = 0;
- public long contactId;
- public int maxLines = 1;
- public String mimetype;
-
- /**
- * Helper for making subclasses parcelable.
- */
- protected void writeToParcel(Parcel p) {
- p.writeInt(type);
- p.writeString(label);
- p.writeString(data);
- p.writeParcelable(uri, 0);
- p.writeLong(id);
- p.writeInt(maxLines);
- p.writeString(mimetype);
- }
-
- /**
- * Helper for making subclasses parcelable.
- */
- protected void readFromParcel(Parcel p) {
- final ClassLoader loader = getClass().getClassLoader();
- type = p.readInt();
- label = p.readString();
- data = p.readString();
- uri = p.readParcelable(loader);
- id = p.readLong();
- maxLines = p.readInt();
- mimetype = p.readString();
- }
- }
-
- protected ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections,
- boolean separators) {
- mContext = context;
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mSections = sections;
- mSeparators = separators;
- }
-
- /**
- * Resets the section data.
- *
- * @param sections the section data
- */
- public final void setSections(ArrayList<ArrayList<E>> sections, boolean separators) {
- mSections = sections;
- mSeparators = separators;
- notifyDataSetChanged();
- }
-
- /**
- * Resets the section data and returns the position of the given entry.
- *
- * @param sections the section data
- * @param entry the entry to return the position for
- * @return the position of entry, or -1 if it isn't found
- */
- public final int setSections(ArrayList<ArrayList<E>> sections, E entry) {
- mSections = sections;
- notifyDataSetChanged();
-
- int numSections = mSections.size();
- int position = 0;
- for (int i = 0; i < numSections; i++) {
- ArrayList<E> section = mSections.get(i);
- int sectionSize = section.size();
- for (int j = 0; j < sectionSize; j++) {
- E e = section.get(j);
- if (e.equals(entry)) {
- position += j;
- return position;
- }
- }
- position += sectionSize;
- }
- return -1;
- }
-
- /**
- * @see android.widget.ListAdapter#getCount()
- */
- public final int getCount() {
- return countEntries(mSections, mSeparators);
- }
-
- /**
- * @see android.widget.ListAdapter#hasSeparators()
- */
- @Override
- public final boolean areAllItemsEnabled() {
- return mSeparators == false;
- }
-
- /**
- * @see android.widget.ListAdapter#isSeparator(int)
- */
- @Override
- public final boolean isEnabled(int position) {
- if (!mSeparators) {
- return true;
- }
-
- int numSections = mSections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<E> section = mSections.get(i);
- int sectionSize = section.size();
- if (sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- if (position == 0) {
- // The first item in a section is always the separator
- return false;
- }
- position -= sectionSize;
- }
- return true;
- }
-
- /**
- * @see android.widget.ListAdapter#getItem(int)
- */
- public final Object getItem(int position) {
- return getEntry(mSections, position, mSeparators);
- }
-
- /**
- * Get the entry for the given position.
- *
- * @param sections the list of sections
- * @param position the position for the desired entry
- * @return the ContactEntry for the given position
- */
- public final static <T extends Entry> T getEntry(ArrayList<ArrayList<T>> sections,
- int position, boolean separators) {
- int numSections = sections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<T> section = sections.get(i);
- int sectionSize = section.size();
- if (separators && sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- if (position < section.size()) {
- return section.get(position);
- }
- position -= section.size();
- }
- return null;
- }
-
- /**
- * Get the count of entries in all sections
- *
- * @param sections the list of sections
- * @return the count of entries in all sections
- */
- public static <T extends Entry> int countEntries(ArrayList<ArrayList<T>> sections,
- boolean separators) {
- int count = 0;
- int numSections = sections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<T> section = sections.get(i);
- int sectionSize = section.size();
- if (separators && sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- count += sections.get(i).size();
- }
- return count;
- }
-
- /**
- * @see android.widget.ListAdapter#getItemId(int)
- */
- public final long getItemId(int position) {
- Entry entry = getEntry(mSections, position, mSeparators);
- if (entry != null) {
- return entry.id;
- } else {
- return -1;
- }
- }
-
- /**
- * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
- */
- public View getView(int position, View convertView, ViewGroup parent) {
- View v;
- if (convertView == null) {
- v = newView(position, parent);
- } else {
- v = convertView;
- }
- bindView(v, getEntry(mSections, position, mSeparators));
- return v;
- }
-
- /**
- * Create a new view for an entry.
- *
- * @parent the parent ViewGroup
- * @return the newly created view
- */
- protected abstract View newView(int position, ViewGroup parent);
-
- /**
- * Binds the data from an entry to a view.
- *
- * @param view the view to display the entry in
- * @param entry the data to bind
- */
- protected abstract void bindView(View view, E entry);
-}
diff --git a/src/com/android/contacts/ScrollingTabWidget.java b/src/com/android/contacts/ScrollingTabWidget.java
deleted file mode 100644
index b45abe4..0000000
--- a/src/com/android/contacts/ScrollingTabWidget.java
+++ /dev/null
@@ -1,418 +0,0 @@
-/*
- * 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.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-
-/*
- * 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, ViewTreeObserver.OnGlobalFocusChangeListener,
- OnFocusChangeListener {
-
- private static final String TAG = "ScrollingTabWidget";
-
- private OnTabSelectionChangedListener mSelectionChangedListener;
- private int mSelectedTab = 0;
- private ImageView mLeftArrowView;
- private ImageView mRightArrowView;
- private HorizontalScrollView mTabsScrollWrapper;
- private TabStripView mTabsView;
- private LayoutInflater mInflater;
-
- // 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);
-
- setFocusable(true);
- setOnFocusChangeListener(this);
- if (!hasFocus()) {
- setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
- }
-
- mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false);
- mLeftArrowView.setOnClickListener(this);
- mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false);
- mRightArrowView.setOnClickListener(this);
- mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate(
- R.layout.tab_layout, this, false);
- mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs);
- View accountNameView = mInflater.inflate(R.layout.tab_account_name, this, false);
-
- mLeftArrowView.setVisibility(View.INVISIBLE);
- mRightArrowView.setVisibility(View.INVISIBLE);
-
- addView(mTabsScrollWrapper);
- addView(mLeftArrowView);
- addView(mRightArrowView);
- addView(accountNameView);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- if (treeObserver != null) {
- treeObserver.addOnGlobalFocusChangeListener(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- if (treeObserver != null) {
- treeObserver.removeOnGlobalFocusChangeListener(this);
- }
- }
-
- 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);
- }
-
- /**
- * Returns the number of tab indicator views.
- *
- * @return the number of tab indicator views.
- */
- public int getTabCount() {
- return mTabsView.getChildCount();
- }
-
- /**
- * Returns the {@link ViewGroup} that actually contains the tabs. This is where the tab
- * views should be attached to when being inflated.
- */
- public ViewGroup getTabParent() {
- return mTabsView;
- }
-
- 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;
- }
-
- if (mSelectedTab < getTabCount()) {
- mTabsView.setSelected(mSelectedTab, false);
- }
- mSelectedTab = index;
- mTabsView.setSelected(mSelectedTab, true);
- }
-
- /**
- * Return index of the currently selected tab.
- */
- public int getCurrentTab() {
- return mSelectedTab;
- }
-
- /**
- * 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) {
- if (index < 0 || index >= getTabCount()) {
- return;
- }
-
- setCurrentTab(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(this);
-
- mTabsView.addView(child);
- }
-
- /**
- * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
- * user clicked on a tab indicator.
- */
- public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
- mSelectionChangedListener = listener;
- }
-
- public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- if (isTab(oldFocus) && !isTab(newFocus)) {
- onLoseFocus();
- }
- }
-
- public void onFocusChange(View v, boolean hasFocus) {
- if (v == this && hasFocus) {
- onObtainFocus();
- return;
- }
-
- if (hasFocus) {
- for (int i = 0; i < getTabCount(); i++) {
- if (getChildTabViewAt(i) == v) {
- setCurrentTab(i);
- mSelectionChangedListener.onTabSelectionChanged(i, false);
- break;
- }
- }
- }
- }
-
- /**
- * Called when the {@link ScrollingTabWidget} gets focus. Here the
- * widget decides which of it's tabs should have focus.
- */
- protected void onObtainFocus() {
- // Setting this flag, allows the children of this View to obtain focus.
- setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
- // Assign focus to the last selected tab.
- focusCurrentTab(mSelectedTab);
- mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
- }
-
- /**
- * Called when the focus has left the {@link ScrollingTabWidget} or its
- * descendants. At this time we want the children of this view to be marked
- * as un-focusable, so that next time focus is moved to the widget, the widget
- * gets control, and can assign focus where it wants.
- */
- protected void onLoseFocus() {
- // Setting this flag will effectively make the tabs unfocusable. This will
- // be toggled when the widget obtains focus again.
- setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
- }
-
- public boolean isTab(View v) {
- for (int i = 0; i < getTabCount(); i++) {
- if (getChildTabViewAt(i) == v) {
- return true;
- }
- }
- return false;
- }
-
- 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;
- }
- }
- }
- }
-
- public 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);
- }
-
- public void onClick(View v) {
- updateLeftMostVisible();
- 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/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index f1a3c7e..856899e 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -29,27 +29,23 @@
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
-import android.view.View;
public class ContactDetailActivity extends Activity {
private static final String TAG = "ContactDetailActivity";
private ContactDetailFragment mFragment;
- private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
-
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setContentView(R.layout.contact_detail_activity);
- Log.i(TAG, getIntent().getData().toString());
-
- final View view = findViewById(R.id.contact_detail_fragment);
mFragment = (ContactDetailFragment) findFragmentById(R.id.contact_detail_fragment);
- mFragment.setCallbacks(mCallbackHandler);
+ mFragment.setListener(mFragmentListener);
mFragment.loadUri(getIntent().getData());
+
+ Log.i(TAG, getIntent().getData().toString());
}
@Override
@@ -113,17 +109,22 @@
return super.onKeyDown(keyCode, event);
}
- private class FragmentCallbackHandler implements ContactDetailFragment.Callbacks {
- public void closeBecauseContactNotFound() {
+ private final ContactDetailFragment.Listener mFragmentListener =
+ new ContactDetailFragment.Listener() {
+ public void onContactNotFound() {
finish();
}
- public void editContact(Uri rawContactUri) {
+ public void onEditRequested(Uri rawContactUri) {
startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
}
- public void itemClicked(Intent intent) {
+ public void onItemClicked(Intent intent) {
startActivity(intent);
}
- }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
+ };
}
diff --git a/src/com/android/contacts/activities/ContactEditActivity.java b/src/com/android/contacts/activities/ContactEditActivity.java
deleted file mode 100644
index 22aae18..0000000
--- a/src/com/android/contacts/activities/ContactEditActivity.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * 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.activities;
-
-import com.android.contacts.ContactsSearchManager;
-import com.android.contacts.R;
-import com.android.contacts.util.DialogManager;
-import com.android.contacts.views.edit.ContactEditFragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-public class ContactEditActivity extends Activity implements
- DialogManager.DialogShowingViewActivity {
-
- private static final String TAG = "ContactEditActivity";
- private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
- private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
-
- private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
- private final DialogManager mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1,
- DIALOG_VIEW_DIALOGS_ID2);
-
- private ContactEditFragment mFragment;
-
- @Override
- public void onCreate(Bundle savedState) {
- super.onCreate(savedState);
-
- setContentView(R.layout.contact_edit_activity);
-
- final Intent intent = getIntent();
- final String action = intent.getAction();
- final Uri uri = intent.getData();
- final String mimeType = intent.resolveType(getContentResolver());
- final Bundle intentExtras = intent.getExtras();
-
- mFragment = (ContactEditFragment) findFragmentById(R.id.contact_edit_fragment);
- mFragment.setCallbacks(mCallbackHandler);
- mFragment.load(action, uri, mimeType, intentExtras);
- }
-
- private class FragmentCallbackHandler implements ContactEditFragment.Callbacks {
- public void closeAfterRevert() {
- finish();
- }
-
- public void closeAfterDelete() {
- finish();
- }
-
- public void closeBecauseContactNotFound() {
- finish();
- }
-
- public void closeAfterSplit() {
- finish();
- }
-
- public void closeBecauseAccountSelectorAborted() {
- finish();
- }
-
- public void setTitleTo(int resourceId) {
- setTitle(resourceId);
- }
-
- public void closeAfterSaving(int resultCode, Intent resultIntent) {
- setResult(resultCode, resultIntent);
- finish();
- }
- }
-
- public DialogManager getDialogManager() {
- return mDialogManager;
- }
-
- @Override
- protected Dialog onCreateDialog(int id, Bundle args) {
- // If this is a dynamic dialog, use the DialogManager
- if (id == DIALOG_VIEW_DIALOGS_ID1 || id == DIALOG_VIEW_DIALOGS_ID2) {
- final Dialog dialog = mDialogManager.onCreateDialog(id, args);
- if (dialog != null) return dialog;
- return super.onCreateDialog(id, args);
- }
-
- // ask the Fragment whether it knows about the dialog
- final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
- if (fragmentResult != null) return fragmentResult;
-
- // Nobody knows about the Dialog
- Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
- return null;
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- // TODO: This is too hardwired.
- mFragment.onActivityResult(requestCode, resultCode, data);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // TODO: This is too hardwired.
- if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
-
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- // TODO: This is too hardwired.
- if (mFragment.onPrepareOptionsMenu(menu)) return true;
-
- return super.onPrepareOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // TODO: This is too hardwired.
- if (mFragment.onOptionsItemSelected(item)) return true;
-
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- if (globalSearch) {
- super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
- } else {
- ContactsSearchManager.startSearch(this, initialQuery);
- }
- }
-}
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
new file mode 100644
index 0000000..377648a
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.views.editor.ContactEditorFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactEditorActivity extends Activity {
+ private static final String TAG = "ContactEditorActivity";
+
+ private ContactEditorFragment mFragment;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ setContentView(R.layout.contact_editor_activity);
+
+ mFragment = (ContactEditorFragment) findFragmentById(R.id.contact_editor_fragment);
+ mFragment.setListener(mFragmentListener);
+ mFragment.loadUri(getIntent().getData());
+
+ Log.i(TAG, getIntent().getData().toString());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mFragment.onPrepareOptionsMenu(menu)) return true;
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mFragment.onOptionsItemSelected(item)) return true;
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ // ask the Fragment whether it knows about the dialog
+ final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+ if (fragmentResult != null) return fragmentResult;
+
+ // Nobody knows about the Dialog
+ Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+ return null;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mFragment.onContextItemSelected(item)) return true;
+
+ return super.onContextItemSelected(item);
+ }
+
+ @Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+ boolean globalSearch) {
+ if (globalSearch) {
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+ } else {
+ ContactsSearchManager.startSearch(this, initialQuery);
+ }
+ }
+
+ private final ContactEditorFragment.Listener mFragmentListener =
+ new ContactEditorFragment.Listener() {
+ public void onContactNotFound() {
+ // TODO: Show error
+ finish();
+ }
+
+ public void onError() {
+ // TODO: Show error message
+ finish();
+ }
+
+ public void onItemClicked(Intent intent) {
+ startActivity(intent);
+ }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
+ };
+}
diff --git a/src/com/android/contacts/activities/ContactFieldEditorActivity.java b/src/com/android/contacts/activities/ContactFieldEditorActivity.java
new file mode 100644
index 0000000..3358ee3
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactFieldEditorActivity.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.views.editor.ContactFieldEditorBaseFragment;
+import com.android.contacts.views.editor.ContactFieldEditorEmailFragment;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+public class ContactFieldEditorActivity extends Activity {
+ public final static String BUNDLE_RAW_CONTACT_URI = "RawContactUri";
+
+ private ContactFieldEditorBaseFragment mFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mFragment = new ContactFieldEditorEmailFragment();
+ mFragment.setListener(mFragmentListener);
+
+ openFragmentTransaction()
+ .add(android.R.id.content, mFragment)
+ .commit();
+
+ final Intent intent = getIntent();
+ final Uri rawContactUri = Uri.parse(intent.getStringExtra(BUNDLE_RAW_CONTACT_URI));
+ final boolean isInsert;
+ if (Intent.ACTION_EDIT.equals(intent.getAction())) {
+ isInsert = false;
+ } else if (Intent.ACTION_INSERT.equals(intent.getAction())) {
+ isInsert = true;
+ } else throw new IllegalArgumentException("Action is neither EDIT nor INSERT");
+
+ if (isInsert) {
+ mFragment.setupUris(rawContactUri, null);
+ } else {
+ mFragment.setupUris(rawContactUri, intent.getData());
+ }
+ }
+
+ private ContactFieldEditorBaseFragment.Listener mFragmentListener =
+ new ContactFieldEditorBaseFragment.Listener() {
+ public void onCancel() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public void onContactNotFound() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public void onDataNotFound() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public void onSaved() {
+ setResult(RESULT_OK);
+ finish();
+ }
+ };
+}
diff --git a/src/com/android/contacts/activities/TwoPaneActivity.java b/src/com/android/contacts/activities/TwoPaneActivity.java
index 3b52156..ec6d4e3 100644
--- a/src/com/android/contacts/activities/TwoPaneActivity.java
+++ b/src/com/android/contacts/activities/TwoPaneActivity.java
@@ -34,8 +34,8 @@
private final static String TAG = "TwoPaneActivity";
private DefaultContactBrowseListFragment mListFragment;
private ContactDetailFragment mDetailFragment;
- private DetailCallbackHandler mDetailCallbackHandler = new DetailCallbackHandler();
- private ListCallbackHandler mListCallbackHandler = new ListCallbackHandler();
+ private DetailFragmentListener mDetailFragmentListener = new DetailFragmentListener();
+ private ListFragmentListener mListFragmentListener = new ListFragmentListener();
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -44,10 +44,10 @@
setContentView(R.layout.two_pane_activity);
mListFragment = (DefaultContactBrowseListFragment) findFragmentById(R.id.two_pane_list);
- mListFragment.setOnContactListActionListener(mListCallbackHandler);
+ mListFragment.setOnContactListActionListener(mListFragmentListener);
mDetailFragment = (ContactDetailFragment) findFragmentById(R.id.two_pane_detail);
- mDetailFragment.setCallbacks(mDetailCallbackHandler);
+ mDetailFragment.setListener(mDetailFragmentListener);
setupSearchUI();
}
@@ -65,7 +65,7 @@
});
}
- private class ListCallbackHandler implements OnContactBrowserActionListener {
+ private class ListFragmentListener implements OnContactBrowserActionListener {
public void onAddToFavoritesAction(Uri contactUri) {
Toast.makeText(TwoPaneActivity.this, "onAddToFavoritesAction",
Toast.LENGTH_LONG).show();
@@ -116,19 +116,24 @@
}
}
- private class DetailCallbackHandler implements ContactDetailFragment.Callbacks {
- public void closeBecauseContactNotFound() {
- Toast.makeText(TwoPaneActivity.this, "closeBecauseContactNotFound",
- Toast.LENGTH_LONG).show();
+ private class DetailFragmentListener implements ContactDetailFragment.Listener {
+ public void onContactNotFound() {
+ Toast.makeText(TwoPaneActivity.this, "onContactNotFound", Toast.LENGTH_LONG).show();
}
- public void editContact(Uri rawContactUri) {
- Toast.makeText(TwoPaneActivity.this, "editContact",
- Toast.LENGTH_LONG).show();
+ public void onEditRequested(Uri contactLookupUri) {
+// final ContactEditFragment fragment = new ContactEditFragment();
+// openFragmentTransaction().replace(mDetailFragment.getView().getId(), fragment).commit();
+// fragment.loadUri(contactLookupUri);
+ Toast.makeText(TwoPaneActivity.this, "editContact", Toast.LENGTH_LONG).show();
}
- public void itemClicked(Intent intent) {
+ public void onItemClicked(Intent intent) {
startActivity(intent);
}
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
}
}
diff --git a/src/com/android/contacts/model/FallbackSource.java b/src/com/android/contacts/model/FallbackSource.java
index 9cc855c..a23426c 100644
--- a/src/com/android/contacts/model/FallbackSource.java
+++ b/src/com/android/contacts/model/FallbackSource.java
@@ -107,6 +107,8 @@
if (kind == null) {
kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
R.string.nameLabelsGroup, -1, -1, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
}
if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
diff --git a/src/com/android/contacts/views/detail/ContactDetailLoader.java b/src/com/android/contacts/views/ContactLoader.java
similarity index 92%
rename from src/com/android/contacts/views/detail/ContactDetailLoader.java
rename to src/com/android/contacts/views/ContactLoader.java
index e8ec96a..f03d269 100644
--- a/src/com/android/contacts/views/detail/ContactDetailLoader.java
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.contacts.views.detail;
+package com.android.contacts.views;
import com.android.contacts.util.DataStatus;
@@ -43,7 +43,7 @@
/**
* Loads a single Contact and all it constituent RawContacts.
*/
-public class ContactDetailLoader extends Loader<ContactDetailLoader.Result> {
+public class ContactLoader extends Loader<ContactLoader.Result> {
private static final String TAG = "ContactLoader";
private Uri mLookupUri;
@@ -51,7 +51,7 @@
private ForceLoadContentObserver mObserver;
private boolean mDestroyed;
- public interface Callbacks {
+ public interface Listener {
public void onContactLoaded(Result contact);
}
@@ -237,7 +237,7 @@
protected Result doInBackground(Void... args) {
try {
final ContentResolver resolver = getContext().getContentResolver();
- final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+ final Uri uriCurrentFormat = ensureIsContactUri(resolver, mLookupUri);
Result result = loadContactHeaderData(resolver, uriCurrentFormat);
if (result == Result.NOT_FOUND) {
// No record found. Try to lookup up a new record with the same lookupKey.
@@ -258,6 +258,7 @@
return result;
} catch (Exception e) {
+ Log.w(TAG, "Error loading the contact: " + e.getMessage());
return Result.ERROR;
}
}
@@ -265,15 +266,30 @@
/**
* Transforms the given Uri and returns a Lookup-Uri that represents the contact.
* For legacy contacts, a raw-contact lookup is performed.
+ * @param resolver
*/
- private Uri convertLegacyIfNecessary(Uri uri) {
+ private Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) {
if (uri == null) throw new IllegalArgumentException("uri must not be null");
final String authority = uri.getAuthority();
- // Current Style Uri? Just return it
+ // Current Style Uri?
if (ContactsContract.AUTHORITY.equals(authority)) {
- return uri;
+ final String type = resolver.getType(uri);
+ // Contact-Uri? Good, return it
+ if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ return uri;
+ }
+
+ // RawContact-Uri? Transform it to ContactUri
+ if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ // Anything else? We don't know what this is
+ throw new IllegalArgumentException("uri format is unknown");
}
// Legacy Style? Convert to RawContact
@@ -281,11 +297,11 @@
if (OBSOLETE_AUTHORITY.equals(authority)) {
// Legacy Format. Convert to RawContact-Uri and then lookup the contact
final long rawContactId = ContentUris.parseId(uri);
- return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+ return RawContacts.getContactLookupUri(resolver,
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
}
- throw new IllegalArgumentException("uri format is unknown");
+ throw new IllegalArgumentException("uri authority is unknown");
}
/**
@@ -440,6 +456,7 @@
}
mContact = result;
+ mLookupUri = result.getLookupUri();
if (result != null) {
if (mObserver == null) {
mObserver = new ForceLoadContentObserver();
@@ -455,7 +472,7 @@
}
}
- public ContactDetailLoader(Context context, Uri lookupUri) {
+ public ContactLoader(Context context, Uri lookupUri) {
super(context);
mLookupUri = lookupUri;
}
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index d85e7a3..0a967bd 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -17,7 +17,6 @@
package com.android.contacts.views.detail;
import com.android.contacts.Collapser;
-import com.android.contacts.ContactEntryAdapter;
import com.android.contacts.ContactOptionsActivity;
import com.android.contacts.ContactPresenceIconUtil;
import com.android.contacts.ContactsUtils;
@@ -29,8 +28,8 @@
import com.android.contacts.model.ContactsSource.DataKind;
import com.android.contacts.util.Constants;
import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.ContactLoader;
import com.android.internal.telephony.ITelephony;
-import com.android.internal.widget.ContactHeaderWidget;
import android.app.Activity;
import android.app.AlertDialog;
@@ -65,6 +64,7 @@
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.telephony.PhoneNumberUtils;
@@ -82,6 +82,7 @@
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.widget.AdapterView;
+import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
@@ -90,10 +91,9 @@
import java.util.ArrayList;
-public class ContactDetailFragment extends LoaderManagingFragment<ContactDetailLoader.Result>
+public class ContactDetailFragment extends LoaderManagingFragment<ContactLoader.Result>
implements OnCreateContextMenuListener, OnItemClickListener {
- private static final String TAG = "ContactDetailsView";
- private static final boolean SHOW_SEPARATORS = false;
+ private static final String TAG = "ContactDetailFragment";
private static final int MENU_ITEM_MAKE_DEFAULT = 3;
@@ -101,10 +101,10 @@
private Context mContext;
private Uri mLookupUri;
- private Callbacks mCallbacks;
+ private Listener mListener;
- private ContactDetailLoader.Result mContactData;
- private ContactHeaderWidget mContactHeaderWidget;
+ private ContactLoader.Result mContactData;
+ private ContactDetailHeaderView mHeaderView;
private ListView mListView;
private boolean mShowSmsLinksForAllPhones;
private ViewAdapter mAdapter;
@@ -137,6 +137,7 @@
private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
+ private LayoutInflater mInflater;
public ContactDetailFragment() {
// Explicit constructor for inflation
@@ -162,13 +163,16 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
- final View view = inflater.inflate(R.layout.contact_detail, container, false);
+ final View view = inflater.inflate(R.layout.contact_detail_fragment, container, false);
- mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
- mContactHeaderWidget.showStar(true);
- mContactHeaderWidget.setExcludeMimes(new String[] {
+ mInflater = inflater;
+
+ mHeaderView = (ContactDetailHeaderView) view.findViewById(R.id.contact_header_widget);
+ mHeaderView.showStar(true);
+ mHeaderView.setExcludeMimes(new String[] {
Contacts.CONTENT_ITEM_TYPE
});
+ mHeaderView.setListener(mHeaderViewListener);
mListView = (ListView) view.findViewById(android.R.id.list);
mListView.setOnCreateContextMenuListener(this);
@@ -183,25 +187,24 @@
return view;
}
- public void setCallbacks(Callbacks value) {
- mCallbacks = value;
+ public void setListener(Listener value) {
+ mListener = value;
}
public void loadUri(Uri lookupUri) {
mLookupUri = lookupUri;
- super.startLoading(LOADER_DETAILS, null);
+ startLoading(LOADER_DETAILS, null);
}
@Override
protected void onInitializeLoaders() {
-// startLoading(LOADER_DETAILS, null);
}
@Override
- protected Loader<ContactDetailLoader.Result> onCreateLoader(int id, Bundle args) {
+ protected Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_DETAILS: {
- return new ContactDetailLoader(mContext, mLookupUri);
+ return new ContactLoader(mContext, mLookupUri);
}
default: {
Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
@@ -211,15 +214,15 @@
}
@Override
- protected void onLoadFinished(Loader<ContactDetailLoader.Result> loader,
- ContactDetailLoader.Result data) {
+ protected void onLoadFinished(Loader<ContactLoader.Result> loader,
+ ContactLoader.Result data) {
final int id = loader.getId();
switch (id) {
case LOADER_DETAILS:
- if (data == ContactDetailLoader.Result.NOT_FOUND) {
+ if (data == ContactLoader.Result.NOT_FOUND) {
// Item has been deleted
Log.i(TAG, "No contact found. Closing activity");
- mCallbacks.closeBecauseContactNotFound();
+ mListener.onContactNotFound();
return;
}
mContactData = data;
@@ -233,13 +236,13 @@
private void bindData() {
// Set the header
- mContactHeaderWidget.setContactUri(mContactData.getLookupUri());
- mContactHeaderWidget.setDisplayName(mContactData.getDisplayName(),
+ mHeaderView.setContactUri(mContactData.getLookupUri());
+ mHeaderView.setDisplayName(mContactData.getDisplayName(),
mContactData.getPhoneticName());
- mContactHeaderWidget.setPhotoId(mContactData.getPhotoId(), mContactData.getLookupUri());
- mContactHeaderWidget.setStared(mContactData.getStarred());
- mContactHeaderWidget.setPresence(mContactData.getPresence());
- mContactHeaderWidget.setStatus(
+ mHeaderView.setPhotoId(mContactData.getPhotoId(), mContactData.getLookupUri());
+ mHeaderView.setStared(mContactData.getStarred());
+ mHeaderView.setPresence(mContactData.getPresence());
+ mHeaderView.setStatus(
mContactData.getStatus(), mContactData.getStatusTimestamp(),
mContactData.getStatusLabel(), mContactData.getStatusResPackage());
@@ -254,10 +257,10 @@
Collapser.collapseList(mImEntries);
if (mAdapter == null) {
- mAdapter = new ViewAdapter(mContext, mSections);
+ mAdapter = new ViewAdapter();
mListView.setAdapter(mAdapter);
} else {
- mAdapter.setSections(mSections, SHOW_SEPARATORS);
+ mAdapter.notifyDataSetChanged();
}
mListView.setEmptyView(mEmptyView);
}
@@ -282,6 +285,7 @@
mWritableRawContactIds.clear();
+ // TODO: This should be done in the background thread
final Sources sources = Sources.getInstance(mContext);
// Build up method entries
@@ -330,7 +334,9 @@
final boolean isSuperPrimary = entryValues.getAsInteger(
Data.IS_SUPER_PRIMARY) != 0;
- if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Always ignore the name. It is shown in the header if set
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build phone entries
mNumPhoneNumbers++;
@@ -460,7 +466,7 @@
}
}
- /* package */ static String buildActionString(DataKind kind, ContentValues values,
+ private static String buildActionString(DataKind kind, ContentValues values,
boolean lowerCase, Context context) {
if (kind.actionHeader == null) {
return null;
@@ -472,7 +478,7 @@
return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
}
- /* package */ static String buildDataString(DataKind kind, ContentValues values,
+ private static String buildDataString(DataKind kind, ContentValues values,
Context context) {
if (kind.actionBody == null) {
return null;
@@ -484,7 +490,16 @@
/**
* A basic structure with the data for a contact entry in the list.
*/
- static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+ private static class ViewEntry implements Collapsible<ViewEntry> {
+ // Copied from baseclass
+ public int type = -1;
+ public String label;
+ public String data;
+ public Uri uri;
+ public long id = 0;
+ public int maxLines = 1;
+ public String mimetype;
+
public Context context = null;
public String resPackageName = null;
public int actionIcon = -1;
@@ -510,7 +525,6 @@
long rawContactId, long dataId, ContentValues values) {
final ViewEntry entry = new ViewEntry();
entry.context = context;
- entry.contactId = rawContactId;
entry.id = dataId;
entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
entry.mimetype = mimeType;
@@ -610,19 +624,11 @@
public ImageView primaryIcon;
public ImageView secondaryActionButton;
public View secondaryActionDivider;
-
- // Need to keep track of this too
- public ViewEntry entry;
}
- final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
- ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
- super(context, sections, SHOW_SEPARATORS);
- }
-
- @Override
+ private final class ViewAdapter extends BaseAdapter {
public View getView(int position, View convertView, ViewGroup parent) {
- final ViewEntry entry = getEntry(mSections, position, false);
+ final ViewEntry entry = getEntry(position);
final View v;
final ViewCache viewCache;
@@ -644,26 +650,16 @@
viewCache.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
viewCache.secondaryActionButton = (ImageView) v.findViewById(
R.id.secondary_action_button);
- viewCache.secondaryActionButton.setOnClickListener(this);
+ viewCache.secondaryActionButton.setOnClickListener(mSecondaryActionClickListener);
viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
v.setTag(viewCache);
}
- // Update the entry in the view cache
- viewCache.entry = entry;
-
// Bind the data to the view
bindView(v, entry);
return v;
}
- @Override
- protected View newView(int position, ViewGroup parent) {
- // getView() handles this
- throw new UnsupportedOperationException();
- }
-
- @Override
protected void bindView(View view, ViewEntry entry) {
final Resources resources = mContext.getResources();
ViewCache views = (ViewCache) view.getTag();
@@ -697,7 +693,7 @@
views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
// Set the action icon
- ImageView action = views.actionIcon;
+ final ImageView action = views.actionIcon;
if (entry.actionIcon != -1) {
Drawable actionIcon;
if (entry.resPackageName != null) {
@@ -715,9 +711,9 @@
}
// Set the presence icon
- Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+ final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
mContext, entry.presence);
- ImageView presenceIconView = views.presenceIcon;
+ final ImageView presenceIconView = views.presenceIcon;
if (presenceIcon != null) {
presenceIconView.setImageDrawable(presenceIcon);
presenceIconView.setVisibility(View.VISIBLE);
@@ -726,7 +722,7 @@
}
// Set the secondary action button
- ImageView secondaryActionView = views.secondaryActionButton;
+ final ImageView secondaryActionView = views.secondaryActionButton;
Drawable secondaryActionIcon = null;
if (entry.secondaryActionIcon != -1) {
secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
@@ -753,14 +749,52 @@
}
}
- public void onClick(View v) {
- if (mCallbacks == null) return;
- if (v == null) return;
- final ViewEntry entry = (ViewEntry) v.getTag();
- if (entry == null) return;
- final Intent intent = entry.secondaryIntent;
- if (intent == null) return;
- mCallbacks.itemClicked(intent);
+ private OnClickListener mSecondaryActionClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mListener == null) return;
+ if (v == null) return;
+ final ViewEntry entry = (ViewEntry) v.getTag();
+ if (entry == null) return;
+ final Intent intent = entry.secondaryIntent;
+ if (intent == null) return;
+ mListener.onItemClicked(intent);
+ }
+ };
+
+ public int getCount() {
+ int count = 0;
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ final ArrayList<ViewEntry> section = mSections.get(i);
+ count += section.size();
+ }
+ return count;
+ }
+
+ public Object getItem(int position) {
+ return getEntry(position);
+ }
+
+ public long getItemId(int position) {
+ final ViewEntry entry = getEntry(position);
+ if (entry != null) {
+ return entry.id;
+ } else {
+ return -1;
+ }
+ }
+
+ private ViewEntry getEntry(int position) {
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ final ArrayList<ViewEntry> section = mSections.get(i);
+ final int sectionSize = section.size();
+ if (position < sectionSize) {
+ return section.get(position);
+ }
+ position -= sectionSize;
+ }
+ return null;
}
}
@@ -783,16 +817,8 @@
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_edit: {
- if (mRawContactIds.size() > 0) {
- final long rawContactIdToEdit = mRawContactIds.get(0);
- final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
- rawContactIdToEdit);
- mCallbacks.editContact(rawContactUri);
- return true;
- } else {
- // There is no rawContact to edit.
- return false;
- }
+ mListener.onEditRequested(mLookupUri);
+ break;
}
case R.id.menu_delete: {
showDeleteConfirmationDialog();
@@ -830,15 +856,17 @@
}
private void showDeleteConfirmationDialog() {
+ final int id;
if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
- getActivity().showDialog(R.id.detail_dialog_confirm_readonly_delete);
+ id = R.id.detail_dialog_confirm_readonly_delete;
} else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
- getActivity().showDialog(R.id.detail_dialog_confirm_readonly_hide);
+ id = R.id.detail_dialog_confirm_readonly_hide;
} else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
- getActivity().showDialog(R.id.detail_dialog_confirm_multiple_delete);
+ id = R.id.detail_dialog_confirm_multiple_delete;
} else {
- getActivity().showDialog(R.id.detail_dialog_confirm_delete);
+ id = R.id.detail_dialog_confirm_delete;
}
+ if (mListener != null) mListener.onDialogRequested(id, null);
}
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
@@ -856,7 +884,7 @@
return;
}
- ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ final ViewEntry entry = mAdapter.getEntry(info.position);
menu.setHeaderTitle(R.string.contactOptionsTitle);
if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
@@ -875,12 +903,12 @@
}
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- if (mCallbacks == null) return;
- final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+ if (mListener == null) return;
+ final ViewEntry entry = mAdapter.getEntry(position);
if (entry == null) return;
final Intent intent = entry.intent;
if (intent == null) return;
- mCallbacks.itemClicked(intent);
+ mListener.onItemClicked(intent);
}
private final DialogInterface.OnClickListener mDeleteListener =
@@ -969,7 +997,7 @@
return null;
}
- return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ return mAdapter.getEntry(info.position);
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
@@ -988,7 +1016,7 @@
int index = mListView.getSelectedItemPosition();
if (index != -1) {
- final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
+ final ViewEntry entry = mAdapter.getEntry(index);
if (entry != null &&
entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
mContext.startActivity(entry.intent);
@@ -1013,20 +1041,39 @@
return false;
}
- public static interface Callbacks {
+ private ContactDetailHeaderView.Listener mHeaderViewListener =
+ new ContactDetailHeaderView.Listener() {
+ public void onDisplayNameClick(View view) {
+ }
+
+ public void onEditClicked() {
+ if (mListener != null) mListener.onEditRequested(mLookupUri);
+ }
+
+ public void onPhotoClick(View view) {
+ }
+ };
+
+
+ public static interface Listener {
/**
* Contact was not found, so somehow close this fragment.
*/
- public void closeBecauseContactNotFound();
+ public void onContactNotFound();
/**
* User decided to go to Edit-Mode
*/
- public void editContact(Uri rawContactUri);
+ public void onEditRequested(Uri lookupUri);
/**
* User clicked a single item (e.g. mail)
*/
- public void itemClicked(Intent intent);
+ public void onItemClicked(Intent intent);
+
+ /**
+ * Show a dialog using the globally unique id
+ */
+ public void onDialogRequested(int id, Bundle bundle);
}
}
diff --git a/src/com/android/contacts/views/detail/ContactDetailHeaderView.java b/src/com/android/contacts/views/detail/ContactDetailHeaderView.java
new file mode 100644
index 0000000..fcff6c1
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailHeaderView.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 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.views.detail;
+
+import com.android.contacts.R;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.ImageButton;
+import android.widget.QuickContactBadge;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Header for displaying a title bar with contact info. You
+ * can bind specific values on the header
+ */
+public class ContactDetailHeaderView extends FrameLayout implements View.OnClickListener {
+
+ private static final String TAG = "ContactDetailHeaderView";
+
+ private TextView mDisplayNameView;
+ private View mAggregateBadge;
+ private TextView mPhoneticNameView;
+ private CheckBox mStarredView;
+ private QuickContactBadge mPhotoView;
+ private ImageView mPresenceView;
+ private TextView mStatusView;
+ private TextView mStatusAttributionView;
+ private int mNoPhotoResource;
+ private QueryHandler mQueryHandler;
+ private ImageButton mEditButton;
+
+ private Uri mContactUri;
+
+ protected String[] mExcludeMimes = null;
+
+ protected ContentResolver mContentResolver;
+
+ /**
+ * Interface for callbacks invoked when the user interacts with a header.
+ */
+ public interface Listener {
+ public void onPhotoClick(View view);
+ public void onDisplayNameClick(View view);
+ public void onEditClicked();
+ }
+
+ private Listener mListener;
+
+
+ private interface ContactQuery {
+ //Projection used for the summary info in the header.
+ String[] COLUMNS = new String[] {
+ Contacts._ID,
+ Contacts.LOOKUP_KEY,
+ Contacts.PHOTO_ID,
+ Contacts.DISPLAY_NAME,
+ Contacts.PHONETIC_NAME,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ };
+ int _ID = 0;
+ int LOOKUP_KEY = 1;
+ int PHOTO_ID = 2;
+ int DISPLAY_NAME = 3;
+ int PHONETIC_NAME = 4;
+ //TODO: We need to figure out how we're going to get the phonetic name.
+ //static final int HEADER_PHONETIC_NAME_COLUMN_INDEX
+ int STARRED = 5;
+ int CONTACT_PRESENCE_STATUS = 6;
+ int CONTACT_STATUS = 7;
+ int CONTACT_STATUS_TIMESTAMP = 8;
+ int CONTACT_STATUS_RES_PACKAGE = 9;
+ int CONTACT_STATUS_LABEL = 10;
+ }
+
+ private interface PhotoQuery {
+ String[] COLUMNS = new String[] {
+ Photo.PHOTO
+ };
+
+ int PHOTO = 0;
+ }
+
+ //Projection used for looking up contact id from phone number
+ protected static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.LOOKUP_KEY,
+ };
+ protected static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
+ protected static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
+
+ //Projection used for looking up contact id from email address
+ protected static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
+ RawContacts.CONTACT_ID,
+ Contacts.LOOKUP_KEY,
+ };
+ protected static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
+ protected static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
+
+ protected static final String[] CONTACT_LOOKUP_PROJECTION = new String[] {
+ Contacts._ID,
+ };
+ protected static final int CONTACT_LOOKUP_ID_COLUMN_INDEX = 0;
+
+ private static final int TOKEN_CONTACT_INFO = 0;
+ private static final int TOKEN_PHONE_LOOKUP = 1;
+ private static final int TOKEN_EMAIL_LOOKUP = 2;
+ private static final int TOKEN_PHOTO_QUERY = 3;
+
+ public ContactDetailHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public ContactDetailHeaderView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ContactDetailHeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mContentResolver = mContext.getContentResolver();
+
+ final LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.contact_detail_header_view, this);
+
+ mDisplayNameView = (TextView) findViewById(R.id.name);
+ mAggregateBadge = findViewById(R.id.aggregate_badge);
+
+ mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
+
+ mStarredView = (CheckBox)findViewById(R.id.star);
+ mStarredView.setOnClickListener(this);
+
+ mEditButton = (ImageButton) findViewById(R.id.edit);
+ mEditButton.setOnClickListener(this);
+
+ mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
+
+ mPresenceView = (ImageView) findViewById(R.id.presence);
+
+ mStatusView = (TextView)findViewById(R.id.status);
+ mStatusAttributionView = (TextView)findViewById(R.id.status_date);
+
+ // 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;
+ }
+
+ resetAsyncQueryHandler();
+ }
+
+ public void enableClickListeners() {
+ mDisplayNameView.setOnClickListener(this);
+ mPhotoView.setOnClickListener(this);
+ }
+
+ /**
+ * Set the given {@link Listener} to handle header events.
+ */
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ private void performPhotoClick() {
+ if (mListener != null) {
+ mListener.onPhotoClick(mPhotoView);
+ }
+ }
+
+ private void performEditClick() {
+ if (mListener != null) {
+ mListener.onEditClicked();
+ }
+ }
+
+ private void performDisplayNameClick() {
+ if (mListener != null) {
+ mListener.onDisplayNameClick(mDisplayNameView);
+ }
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ try{
+ if (this != mQueryHandler) {
+ Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!");
+ return;
+ }
+
+ switch (token) {
+ case TOKEN_PHOTO_QUERY: {
+ //Set the photo
+ Bitmap photoBitmap = null;
+ if (cursor != null && cursor.moveToFirst()
+ && !cursor.isNull(PhotoQuery.PHOTO)) {
+ byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO);
+ photoBitmap = BitmapFactory.decodeByteArray(photoData, 0,
+ photoData.length, null);
+ }
+
+ if (photoBitmap == null) {
+ photoBitmap = loadPlaceholderPhoto(null);
+ }
+ setPhoto(photoBitmap);
+ if (cookie != null && cookie instanceof Uri) {
+ mPhotoView.assignContactUri((Uri) cookie);
+ }
+ invalidate();
+ break;
+ }
+ case TOKEN_CONTACT_INFO: {
+ if (cursor != null && cursor.moveToFirst()) {
+ bindContactInfo(cursor);
+ final Uri lookupUri = Contacts.getLookupUri(
+ cursor.getLong(ContactQuery._ID),
+ cursor.getString(ContactQuery.LOOKUP_KEY));
+
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+
+ setPhotoId(photoId, lookupUri);
+ } else {
+ // shouldn't really happen
+ setDisplayName(null, null);
+ setSocialSnippet(null);
+ setPhoto(loadPlaceholderPhoto(null));
+ }
+ break;
+ }
+ case TOKEN_PHONE_LOOKUP: {
+ if (cursor != null && cursor.moveToFirst()) {
+ long contactId = cursor.getLong(PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(
+ PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+ bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
+ false /* don't reset query handler */);
+ } else {
+ String phoneNumber = (String) cookie;
+ setDisplayName(phoneNumber, null);
+ setSocialSnippet(null);
+ setPhoto(loadPlaceholderPhoto(null));
+ mPhotoView.assignContactFromPhone(phoneNumber, true);
+ }
+ break;
+ }
+ case TOKEN_EMAIL_LOOKUP: {
+ if (cursor != null && cursor.moveToFirst()) {
+ long contactId = cursor.getLong(EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(
+ EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+ bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
+ false /* don't reset query handler */);
+ } else {
+ String emailAddress = (String) cookie;
+ setDisplayName(emailAddress, null);
+ setSocialSnippet(null);
+ setPhoto(loadPlaceholderPhoto(null));
+ mPhotoView.assignContactFromEmail(emailAddress, true);
+ }
+ break;
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Turn on/off showing of the aggregate badge element.
+ */
+ public void showAggregateBadge(boolean showBagde) {
+ mAggregateBadge.setVisibility(showBagde ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Turn on/off showing of the star element.
+ */
+ public void showStar(boolean showStar) {
+ mStarredView.setVisibility(showStar ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Manually set the starred state of this header widget. This doesn't change
+ * the underlying {@link Contacts} value, only the UI state.
+ */
+ public void setStared(boolean starred) {
+ mStarredView.setChecked(starred);
+ }
+
+ /**
+ * Manually set the presence.
+ */
+ public void setPresence(int presence) {
+ mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence));
+ }
+
+ /**
+ * Manually set the presence. If presence is null, it is hidden.
+ * This doesn't change the underlying {@link Contacts} value, only the UI state.
+ * @hide
+ */
+ public void setPresence(Integer presence) {
+ if (presence == null) {
+ showPresence(false);
+ } else {
+ showPresence(true);
+ setPresence(presence.intValue());
+ }
+ }
+
+ /**
+ * Turn on/off showing the presence.
+ * @hide this is here for consistency with setStared/showStar and should be public
+ */
+ public void showPresence(boolean showPresence) {
+ mPresenceView.setVisibility(showPresence ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Manually set the contact uri without loading any data
+ */
+ public void setContactUri(Uri uri) {
+ setContactUri(uri, true);
+ }
+
+ /**
+ * Manually set the contact uri without loading any data
+ */
+ public void setContactUri(Uri uri, boolean sendToQuickContact) {
+ mContactUri = uri;
+ if (sendToQuickContact) {
+ mPhotoView.assignContactUri(uri);
+ }
+ }
+
+ /**
+ * Manually set the photo to display in the header. This doesn't change the
+ * underlying {@link Contacts}, only the UI state.
+ */
+ public void setPhoto(Bitmap bitmap) {
+ mPhotoView.setImageBitmap(bitmap);
+ }
+
+ /**
+ * Manually set the photo given its id. If the id is 0, a placeholder picture will
+ * be loaded. For any other Id, an async query is started
+ * @hide
+ */
+ public void setPhotoId(final long photoId, final Uri lookupUri) {
+ if (photoId == 0) {
+ setPhoto(loadPlaceholderPhoto(null));
+ mPhotoView.assignContactUri(lookupUri);
+ invalidate();
+ } else {
+ startPhotoQuery(photoId, lookupUri,
+ false /* don't reset query handler */);
+ }
+ }
+
+ /**
+ * Manually set the display name and phonetic name to show in the header.
+ * This doesn't change the underlying {@link Contacts}, only the UI state.
+ */
+ public void setDisplayName(CharSequence displayName, CharSequence phoneticName) {
+ mDisplayNameView.setText(displayName);
+ if (!TextUtils.isEmpty(phoneticName)) {
+ mPhoneticNameView.setText(phoneticName);
+ mPhoneticNameView.setVisibility(View.VISIBLE);
+ } else {
+ mPhoneticNameView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Manually set the social snippet text to display in the header. This doesn't change the
+ * underlying {@link Contacts}, only the UI state.
+ */
+ public void setSocialSnippet(CharSequence snippet) {
+ if (snippet == null) {
+ mStatusView.setVisibility(View.GONE);
+ mStatusAttributionView.setVisibility(View.GONE);
+ } else {
+ mStatusView.setText(snippet);
+ mStatusView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Manually set the status attribution text to display in the header.
+ * This doesn't change the underlying {@link Contacts}, only the UI state.
+ * @hide
+ */
+ public void setStatusAttribution(CharSequence attribution) {
+ if (attribution != null) {
+ mStatusAttributionView.setText(attribution);
+ mStatusAttributionView.setVisibility(View.VISIBLE);
+ } else {
+ mStatusAttributionView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Set a list of specific MIME-types to exclude and not display. For
+ * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
+ * profile icon.
+ */
+ public void setExcludeMimes(String[] excludeMimes) {
+ mExcludeMimes = excludeMimes;
+ mPhotoView.setExcludeMimes(excludeMimes);
+ }
+
+ /**
+ * Manually set all the status values to display in the header.
+ * This doesn't change the underlying {@link Contacts}, only the UI state.
+ * @hide
+ * @param status The status of the contact. If this is either null or empty,
+ * the status is cleared and the other parameters are ignored.
+ * @param statusTimestamp The timestamp (retrieved via a call to
+ * {@link System#currentTimeMillis()}) of the last status update.
+ * This value can be null if it is not known.
+ * @param statusLabel The id of a resource string that specifies the current
+ * status. This value can be null if no Label should be used.
+ * @param statusResPackage The name of the resource package containing the resource string
+ * referenced in the parameter statusLabel.
+ */
+ public void setStatus(final String status, final Long statusTimestamp,
+ final Integer statusLabel, final String statusResPackage) {
+ if (TextUtils.isEmpty(status)) {
+ setSocialSnippet(null);
+ return;
+ }
+
+ setSocialSnippet(status);
+
+ final CharSequence timestampDisplayValue;
+
+ if (statusTimestamp != null) {
+ // Set the date/time field by mixing relative and absolute
+ // times.
+ int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
+
+ timestampDisplayValue = DateUtils.getRelativeTimeSpanString(
+ statusTimestamp.longValue(), System.currentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS, flags);
+ } else {
+ timestampDisplayValue = null;
+ }
+
+
+ String labelDisplayValue = null;
+
+ if (statusLabel != null) {
+ Resources resources;
+ if (TextUtils.isEmpty(statusResPackage)) {
+ resources = getResources();
+ } else {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ resources = pm.getResourcesForApplication(statusResPackage);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Contact status update resource package not found: "
+ + statusResPackage);
+ resources = null;
+ }
+ }
+
+ if (resources != null) {
+ try {
+ labelDisplayValue = resources.getString(statusLabel.intValue());
+ } catch (NotFoundException e) {
+ Log.w(TAG, "Contact status update resource not found: " + statusResPackage + "@"
+ + statusLabel.intValue());
+ }
+ }
+ }
+
+ final CharSequence attribution;
+ if (timestampDisplayValue != null && labelDisplayValue != null) {
+ attribution = getContext().getString(
+ R.string.contact_status_update_attribution_with_date,
+ timestampDisplayValue, labelDisplayValue);
+ } else if (timestampDisplayValue == null && labelDisplayValue != null) {
+ attribution = getContext().getString(
+ R.string.contact_status_update_attribution,
+ labelDisplayValue);
+ } else if (timestampDisplayValue != null) {
+ attribution = timestampDisplayValue;
+ } else {
+ attribution = null;
+ }
+ setStatusAttribution(attribution);
+ }
+
+ /**
+ * Convenience method for binding all available data from an existing
+ * contact.
+ *
+ * @param contactUri a {Contacts.CONTENT_URI} style URI.
+ * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
+ */
+ private void bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler) {
+ mContactUri = contactUri;
+ startContactQuery(contactUri, resetQueryHandler);
+ }
+
+ /**
+ * Convenience method for binding all available data from an existing
+ * contact.
+ *
+ * @param emailAddress The email address used to do a reverse lookup in
+ * the contacts database. If more than one contact contains this email
+ * address, one of them will be chosen to bind to.
+ */
+ public void bindFromEmail(String emailAddress) {
+ resetAsyncQueryHandler();
+
+ mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, emailAddress,
+ Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
+ EMAIL_LOOKUP_PROJECTION, null, null, null);
+ }
+
+ /**
+ * Convenience method for binding all available data from an existing
+ * contact.
+ *
+ * @param number The phone number used to do a reverse lookup in
+ * the contacts database. If more than one contact contains this phone
+ * number, one of them will be chosen to bind to.
+ */
+ public void bindFromPhoneNumber(String number) {
+ resetAsyncQueryHandler();
+
+ mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, number,
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+ PHONE_LOOKUP_PROJECTION, null, null, null);
+ }
+
+ /**
+ * startContactQuery
+ *
+ * internal method to query contact by Uri.
+ *
+ * @param contactUri the contact uri
+ * @param resetQueryHandler whether to use a new AsyncQueryHandler or not
+ */
+ private void startContactQuery(Uri contactUri, boolean resetQueryHandler) {
+ if (resetQueryHandler) {
+ resetAsyncQueryHandler();
+ }
+
+ mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
+ null, null, null);
+ }
+
+ /**
+ * startPhotoQuery
+ *
+ * internal method to query contact photo by photo id and uri.
+ *
+ * @param photoId the photo id.
+ * @param lookupKey the lookup uri.
+ * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
+ */
+ protected void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) {
+ if (resetQueryHandler) {
+ resetAsyncQueryHandler();
+ }
+
+ mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
+ ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS,
+ null, null, null);
+ }
+
+ /**
+ * Method to force this widget to forget everything it knows about the contact.
+ * We need to stop any existing async queries for phone, email, contact, and photos.
+ */
+ public void wipeClean() {
+ resetAsyncQueryHandler();
+
+ setDisplayName(null, null);
+ setPhoto(loadPlaceholderPhoto(null));
+ setSocialSnippet(null);
+ setPresence(0);
+ mContactUri = null;
+ mExcludeMimes = null;
+ }
+
+
+ private void resetAsyncQueryHandler() {
+ // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really
+ // need the old async queries to be cancelled, let's do it the hard way.
+ mQueryHandler = new QueryHandler(mContentResolver);
+ }
+
+ /**
+ * Bind the contact details provided by the given {@link Cursor}.
+ */
+ protected void bindContactInfo(Cursor c) {
+ final String displayName = c.getString(ContactQuery.DISPLAY_NAME);
+ final String phoneticName = c.getString(ContactQuery.PHONETIC_NAME);
+ this.setDisplayName(displayName, phoneticName);
+
+ final boolean starred = c.getInt(ContactQuery.STARRED) != 0;
+ setStared(starred);
+
+ //Set the presence status
+ if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) {
+ int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS);
+ setPresence(presence);
+ showPresence(true);
+ } else {
+ showPresence(false);
+ }
+
+ //Set the status update
+ final String status = c.getString(ContactQuery.CONTACT_STATUS);
+ final Long statusTimestamp = c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
+ ? null
+ : c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
+ final Integer statusLabel = c.isNull(ContactQuery.CONTACT_STATUS_LABEL)
+ ? null
+ : c.getInt(ContactQuery.CONTACT_STATUS_LABEL);
+ final String statusResPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE);
+
+ setStatus(status, statusTimestamp, statusLabel, statusResPackage);
+ }
+
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.edit: {
+ performEditClick();
+ break;
+ }
+ case R.id.star: {
+ // Toggle "starred" state
+ // Make sure there is a contact
+ if (mContactUri != null) {
+ final ContentValues values = new ContentValues(1);
+ values.put(Contacts.STARRED, mStarredView.isChecked());
+ mContentResolver.update(mContactUri, values, null, null);
+ }
+ break;
+ }
+ case R.id.photo: {
+ performPhotoClick();
+ break;
+ }
+ case R.id.name: {
+ performDisplayNameClick();
+ break;
+ }
+ }
+ }
+
+ private Bitmap loadPlaceholderPhoto(BitmapFactory.Options options) {
+ if (mNoPhotoResource == 0) {
+ return null;
+ }
+ return BitmapFactory.decodeResource(mContext.getResources(),
+ mNoPhotoResource, options);
+ }
+}
diff --git a/src/com/android/contacts/views/edit/ContactEditFragment.java b/src/com/android/contacts/views/edit/ContactEditFragment.java
deleted file mode 100644
index 488a1a5..0000000
--- a/src/com/android/contacts/views/edit/ContactEditFragment.java
+++ /dev/null
@@ -1,1324 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * 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.views.edit;
-
-import com.android.contacts.JoinContactActivity;
-import com.android.contacts.R;
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.Editor;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
-import com.android.contacts.model.EntitySet;
-import com.android.contacts.model.GoogleSource;
-import com.android.contacts.model.Sources;
-import com.android.contacts.model.ContactsSource.EditType;
-import com.android.contacts.model.Editor.EditorListener;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.ui.ViewIdGenerator;
-import com.android.contacts.ui.widget.BaseContactEditorView;
-import com.android.contacts.ui.widget.PhotoEditorView;
-import com.android.contacts.util.EmptyService;
-import com.android.contacts.util.WeakAsyncTask;
-
-import android.accounts.Account;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.LoaderManagingFragment;
-import android.app.ProgressDialog;
-import android.content.ActivityNotFoundException;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.Loader;
-import android.content.OperationApplicationException;
-import android.content.ContentProviderOperation.Builder;
-import android.content.DialogInterface.OnDismissListener;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.media.MediaScannerConnection;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.MediaStore;
-import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.View.OnClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.LinearLayout;
-import android.widget.ListAdapter;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.io.File;
-import java.lang.ref.WeakReference;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-
-//Here are the open TODOs for the Fragment transition
-//TODO How to save data? Service?
-//TODO Do account-list lookup always in a thread
-//TODO Cleanup state handling (orientation changes etc).
-//TODO Cleanup the load function. It can currenlty also do insert, which is awkward
-//TODO Watch for background changes...How?
-
-public class ContactEditFragment extends LoaderManagingFragment<ContactEditLoader.Result> {
-
- private static final String TAG = "ContactEditFragment";
-
- private static final int LOADER_DATA = 1;
-
- private static final String KEY_EDIT_STATE = "state";
- private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
- private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
- private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
- private static final String KEY_QUERY_SELECTION = "queryselection";
- private static final String KEY_QUERY_SELECTION_ARGS = "queryselectionargs";
- private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
-
- public static final int SAVE_MODE_DEFAULT = 0;
- public static final int SAVE_MODE_SPLIT = 1;
- public static final int SAVE_MODE_JOIN = 2;
-
- private long mRawContactIdRequestingPhoto = -1;
-
- private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
-
- private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
-
- private static final int ICON_SIZE = 96;
-
- private static final File PHOTO_DIR = new File(
- Environment.getExternalStorageDirectory() + "/DCIM/Camera");
-
- private File mCurrentPhotoFile;
-
- private Context mContext;
- private String mAction;
- private Uri mUri;
- private String mMimeType;
- private Bundle mIntentExtras;
- private Callbacks mCallbacks;
-
- private String mQuerySelection;
- private String[] mQuerySelectionArgs;
-
- private long mContactIdForJoin;
-
- private LinearLayout mContent;
- private EntitySet mState;
-
- private ViewIdGenerator mViewIdGenerator;
-
- public ContactEditFragment() {
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- mContext = activity;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
- final View view = inflater.inflate(R.layout.contact_edit, container, false);
-
- mContent = (LinearLayout) view.findViewById(R.id.editors);
-
- view.findViewById(R.id.btn_done).setOnClickListener(new OnClickListener() {
- public void onClick(View v) {
- doSaveAction(SAVE_MODE_DEFAULT);
- }
- });
- view.findViewById(R.id.btn_discard).setOnClickListener(new OnClickListener() {
- public void onClick(View v) {
- doRevertAction();
- }
- });
-
- return view;
- }
-
- // TODO: Think about splitting this. Doing INSERT via load is kinda weird
- public void load(String action, Uri uri, String mimeType, Bundle intentExtras) {
- mAction = action;
- mUri = uri;
- mMimeType = mimeType;
- mIntentExtras = intentExtras;
-
- if (Intent.ACTION_EDIT.equals(mAction)) {
- // Read initial state from database
- if (mCallbacks != null) mCallbacks.setTitleTo(R.string.editContact_title_edit);
- startLoading(LOADER_DATA, null);
- } else if (Intent.ACTION_INSERT.equals(mAction)) {
- if (mCallbacks != null) mCallbacks.setTitleTo(R.string.editContact_title_insert);
-
- doAddAction();
- } else throw new IllegalArgumentException("Unknown Action String " + mAction +
- ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
- }
-
- public void setCallbacks(Callbacks callbacks) {
- mCallbacks = callbacks;
- }
-
- @Override
- public void onCreate(Bundle savedState) {
- // TODO: Currently savedState is always null (framework issue). Test once this is fixed
- super.onCreate(savedState);
-
- if (savedState == null) {
- // If savedState is non-null, onRestoreInstanceState() will restore the generator.
- mViewIdGenerator = new ViewIdGenerator();
- } else {
- // Read modifications from instance
- mState = savedState.<EntitySet> getParcelable(KEY_EDIT_STATE);
- mRawContactIdRequestingPhoto = savedState.getLong(
- KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
- mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
- String fileName = savedState.getString(KEY_CURRENT_PHOTO_FILE);
- if (fileName != null) {
- mCurrentPhotoFile = new File(fileName);
- }
- mQuerySelection = savedState.getString(KEY_QUERY_SELECTION);
- mQuerySelectionArgs = savedState.getStringArray(KEY_QUERY_SELECTION_ARGS);
- mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
- }
- }
-
- @Override
- protected Loader<ContactEditLoader.Result> onCreateLoader(int id, Bundle args) {
- return new ContactEditLoader(mContext, mUri, mMimeType, mIntentExtras);
- }
-
- @Override
- protected void onInitializeLoaders() {
- }
-
- @Override
- protected void onLoadFinished(Loader<ContactEditLoader.Result> loader,
- ContactEditLoader.Result data) {
- if (data == ContactEditLoader.Result.NOT_FOUND) {
- // Item has been deleted
- Log.i(TAG, "No contact found. Closing fragment");
- if (mCallbacks != null) mCallbacks.closeBecauseContactNotFound();
- return;
- }
- setData(data);
- }
-
- public void setData(ContactEditLoader.Result data) {
- mState = data.getEntitySet();
- bindEditors();
- }
-
- public void selectAccountAndCreateContact(ArrayList<Account> accounts, boolean isNewContact) {
- // No Accounts available. Create a phone-local contact.
- if (accounts.isEmpty()) {
- createContact(null, isNewContact);
- return; // Don't show a dialog.
- }
-
- // In the common case of a single account being writable, auto-select
- // it without showing a dialog.
- if (accounts.size() == 1) {
- createContact(accounts.get(0), isNewContact);
- return; // Don't show a dialog.
- }
-
- Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(BUNDLE_SELECT_ACCOUNT_LIST, accounts);
- getActivity().showDialog(R.id.edit_dialog_select_account, bundle);
- }
-
- /**
- * @param account may be null to signal a device-local contact should
- * be created.
- * @param prefillFromIntent If this is set, the intent extras will be used to prefill the fields
- */
- private void createContact(Account account, boolean prefillFromIntent) {
- final Sources sources = Sources.getInstance(mContext);
- final ContentValues values = new ContentValues();
- if (account != null) {
- values.put(RawContacts.ACCOUNT_NAME, account.name);
- values.put(RawContacts.ACCOUNT_TYPE, account.type);
- } else {
- values.putNull(RawContacts.ACCOUNT_NAME);
- values.putNull(RawContacts.ACCOUNT_TYPE);
- }
-
- // Parse any values from incoming intent
- EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
- final ContactsSource source = sources.getInflatedSource(
- account != null ? account.type : null,
- ContactsSource.LEVEL_CONSTRAINTS);
- EntityModifier.parseExtras(mContext, source, insert,
- prefillFromIntent ? mIntentExtras : null);
-
- // Ensure we have some default fields
- EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
- EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
-
- if (mState == null) {
- // Create state if none exists yet
- mState = EntitySet.fromSingle(insert);
- } else {
- // Add contact onto end of existing state
- mState.add(insert);
- }
-
- bindEditors();
- }
-
- private void bindEditors() {
- // Sort the editors
- Collections.sort(mState, mComparator);
-
- // Remove any existing editors and rebuild any visible
- mContent.removeAllViews();
-
- final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
- final Sources sources = Sources.getInstance(mContext);
- int size = mState.size();
- for (int i = 0; i < size; i++) {
- // TODO ensure proper ordering of entities in the list
- final EntityDelta entity = mState.get(i);
- final ValuesDelta values = entity.getValues();
- if (!values.isVisible()) continue;
-
- final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource source = sources.getInflatedSource(accountType,
- ContactsSource.LEVEL_CONSTRAINTS);
- final long rawContactId = values.getAsLong(RawContacts._ID);
-
- final BaseContactEditorView editor;
- if (!source.readOnly) {
- editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor,
- mContent, false);
- } else {
- editor = (BaseContactEditorView) inflater.inflate(
- R.layout.item_read_only_contact_editor, mContent, false);
- }
- final PhotoEditorView photoEditor = editor.getPhotoEditor();
- photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly,
- photoEditor));
-
- mContent.addView(editor);
- editor.setState(entity, source, mViewIdGenerator);
- }
-
- // Show editor now that we've loaded state
- mContent.setVisibility(View.VISIBLE);
- }
-
- public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.edit, menu);
-
- return true;
- }
-
- public boolean onPrepareOptionsMenu(Menu menu) {
- menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1);
- return true;
- }
-
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_done:
- return doSaveAction(SAVE_MODE_DEFAULT);
- case R.id.menu_discard:
- return doRevertAction();
- case R.id.menu_add:
- return doAddAction();
- case R.id.menu_delete:
- return doDeleteAction();
- case R.id.menu_split:
- return doSplitContactAction();
- case R.id.menu_join:
- return doJoinContactAction();
- }
- return false;
- }
-
- private boolean doAddAction() {
- // Load Accounts async so that we can present them
- AsyncTask<Void, Void, ArrayList<Account>> loadAccountsTask =
- new AsyncTask<Void, Void, ArrayList<Account>>() {
- @Override
- protected ArrayList<Account> doInBackground(Void... params) {
- return Sources.getInstance(mContext).getAccounts(true);
- }
- @Override
- protected void onPostExecute(ArrayList<Account> result) {
- selectAccountAndCreateContact(result, true);
- }
- };
- loadAccountsTask.execute();
-
- return true;
- }
-
- /**
- * Delete the entire contact currently being edited, which usually asks for
- * user confirmation before continuing.
- */
- private boolean doDeleteAction() {
- if (!hasValidState())
- return false;
- int readOnlySourcesCnt = 0;
- int writableSourcesCnt = 0;
- // TODO: This shouldn't be called from the UI thread
- final Sources sources = Sources.getInstance(mContext);
- for (EntityDelta delta : mState) {
- final String accountType = delta.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource contactsSource = sources.getInflatedSource(accountType,
- ContactsSource.LEVEL_CONSTRAINTS);
- if (contactsSource != null && contactsSource.readOnly) {
- readOnlySourcesCnt += 1;
- } else {
- writableSourcesCnt += 1;
- }
- }
-
- if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) {
- getActivity().showDialog(R.id.edit_dialog_confirm_readonly_delete);
- } else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) {
- getActivity().showDialog(R.id.edit_dialog_confirm_readonly_hide);
- } else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) {
- getActivity().showDialog(R.id.edit_dialog_confirm_multiple_delete);
- } else {
- getActivity().showDialog(R.id.edit_dialog_confirm_delete);
- }
- return true;
- }
-
- /**
- * Pick a specific photo to be added under the currently selected tab.
- */
- /* package */ boolean doPickPhotoAction(long rawContactId) {
- if (!hasValidState()) return false;
-
- mRawContactIdRequestingPhoto = rawContactId;
-
- getActivity().showDialog(R.id.edit_dialog_pick_photo);
- return false;
- }
-
- public Dialog onCreateDialog(int id, Bundle bundle) {
- switch (id) {
- case R.id.edit_dialog_confirm_delete:
- return new AlertDialog.Builder(mContext)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.deleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener())
- .setCancelable(false)
- .create();
- case R.id.edit_dialog_confirm_readonly_delete:
- return new AlertDialog.Builder(mContext)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.readOnlyContactDeleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener())
- .setCancelable(false)
- .create();
- case R.id.edit_dialog_confirm_multiple_delete:
- return new AlertDialog.Builder(mContext)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.multipleContactDeleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener())
- .setCancelable(false)
- .create();
- case R.id.edit_dialog_confirm_readonly_hide:
- return new AlertDialog.Builder(mContext)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.readOnlyContactWarning)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener())
- .setCancelable(false)
- .create();
- case R.id.edit_dialog_pick_photo:
- return createPickPhotoDialog();
- case R.id.edit_dialog_split:
- return createSplitDialog();
- case R.id.edit_dialog_select_account:
- return createSelectAccountDialog(bundle);
- default:
- return null;
- }
- }
-
- private Dialog createSelectAccountDialog(Bundle bundle) {
- final ArrayList<Account> accounts = bundle.getParcelableArrayList(
- BUNDLE_SELECT_ACCOUNT_LIST);
- // Wrap our context to inflate list items using correct theme
- final Context dialogContext = new ContextThemeWrapper(mContext, android.R.style.Theme_Light);
- final LayoutInflater dialogInflater =
- (LayoutInflater)dialogContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- final Sources sources = Sources.getInstance(mContext);
-
- final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(mContext,
- android.R.layout.simple_list_item_2, accounts) {
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
- parent, false);
- }
-
- // TODO: show icon along with title
- final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
- final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
-
- final Account account = this.getItem(position);
- final ContactsSource source = sources.getInflatedSource(account.type,
- ContactsSource.LEVEL_SUMMARY);
-
- text1.setText(account.name);
- text2.setText(source.getDisplayLabel(mContext));
-
- return convertView;
- }
- };
-
- final DialogInterface.OnClickListener clickListener =
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
-
- // Create new contact based on selected source
- final Account account = accountAdapter.getItem(which);
- createContact(account, false);
- }
- };
-
- final DialogInterface.OnCancelListener cancelListener =
- new DialogInterface.OnCancelListener() {
- public void onCancel(DialogInterface dialog) {
- // If nothing remains, close activity
- if (!hasValidState()) {
- mCallbacks.closeBecauseAccountSelectorAborted();
- }
- }
- };
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- builder.setTitle(R.string.dialog_new_contact_account);
- builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
- builder.setOnCancelListener(cancelListener);
- final Dialog result = builder.create();
- result.setOnDismissListener(new OnDismissListener() {
- public void onDismiss(DialogInterface dialog) {
- // TODO: Check if we even need this...seems useless
- //removeDialog(DIALOG_SELECT_ACCOUNT);
- }
- });
- return result;
- }
-
- private boolean doSplitContactAction() {
- if (!hasValidState()) return false;
-
- getActivity().showDialog(R.id.edit_dialog_split);
- return true;
- }
-
- private Dialog createSplitDialog() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- builder.setTitle(R.string.splitConfirmation_title);
- builder.setIcon(android.R.drawable.ic_dialog_alert);
- builder.setMessage(R.string.splitConfirmation);
- builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- // Split the contacts
- mState.splitRawContacts();
- doSaveAction(SAVE_MODE_SPLIT);
- }
- });
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.setCancelable(false);
- return builder.create();
- }
-
- private boolean doJoinContactAction() {
- return doSaveAction(SAVE_MODE_JOIN);
- }
-
- /**
- * Creates a dialog offering two options: take a photo or pick a photo from the gallery.
- */
- private Dialog createPickPhotoDialog() {
- // Wrap our context to inflate list items using correct theme
- final Context dialogContext = new ContextThemeWrapper(mContext,
- android.R.style.Theme_Light);
-
- String[] choices = new String[2];
- choices[0] = mContext.getString(R.string.take_photo);
- choices[1] = mContext.getString(R.string.pick_photo);
- final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
- android.R.layout.simple_list_item_1, choices);
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
- builder.setTitle(R.string.attachToContact);
- builder.setSingleChoiceItems(adapter, -1, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- switch(which) {
- case 0:
- doTakePhoto();
- break;
- case 1:
- doPickPhotoFromGallery();
- break;
- }
- }
- });
- return builder.create();
- }
-
-
- /**
- * Launches Gallery to pick a photo.
- */
- protected void doPickPhotoFromGallery() {
- try {
- // Launch picker to choose photo for selected contact
- final Intent intent = getPhotoPickIntent();
- getActivity().startActivityForResult(intent,
- R.id.edit_request_code_photo_picked_with_data);
- } catch (ActivityNotFoundException e) {
- Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
- */
- public static Intent getPhotoPickIntent() {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
- intent.setType("image/*");
- intent.putExtra("crop", "true");
- intent.putExtra("aspectX", 1);
- intent.putExtra("aspectY", 1);
- intent.putExtra("outputX", ICON_SIZE);
- intent.putExtra("outputY", ICON_SIZE);
- intent.putExtra("return-data", true);
- return intent;
- }
-
- /**
- * Check if our internal {@link #mState} is valid, usually checked before
- * performing user actions.
- */
- private boolean hasValidState() {
- return mState != null && mState.size() > 0;
- }
-
- /**
- * Create a file name for the icon photo using current time.
- */
- private String getPhotoFileName() {
- Date date = new Date(System.currentTimeMillis());
- SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
- return dateFormat.format(date) + ".jpg";
- }
-
- /**
- * Launches Camera to take a picture and store it in a file.
- */
- protected void doTakePhoto() {
- try {
- // Launch camera to take photo for selected contact
- PHOTO_DIR.mkdirs();
- mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName());
- final Intent intent = getTakePickIntent(mCurrentPhotoFile);
-
- getActivity().startActivityForResult(intent, R.id.edit_request_code_camera_with_data);
- } catch (ActivityNotFoundException e) {
- Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Constructs an intent for capturing a photo and storing it in a temporary file.
- */
- public static Intent getTakePickIntent(File f) {
- Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
- intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
- return intent;
- }
-
- /**
- * Sends a newly acquired photo to Gallery for cropping
- */
- protected void doCropPhoto(File f) {
- try {
- // Add the image to the media store
- MediaScannerConnection.scanFile(
- mContext,
- new String[] { f.getAbsolutePath() },
- new String[] { null },
- null);
-
- // Launch gallery to crop the photo
- final Intent intent = getCropImageIntent(Uri.fromFile(f));
- getActivity().startActivityForResult(intent,
- R.id.edit_request_code_photo_picked_with_data);
- } catch (Exception e) {
- Log.e(TAG, "Cannot crop image", e);
- Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Constructs an intent for image cropping.
- */
- public static Intent getCropImageIntent(Uri photoUri) {
- Intent intent = new Intent("com.android.camera.action.CROP");
- intent.setDataAndType(photoUri, "image/*");
- intent.putExtra("crop", "true");
- intent.putExtra("aspectX", 1);
- intent.putExtra("aspectY", 1);
- intent.putExtra("outputX", ICON_SIZE);
- intent.putExtra("outputY", ICON_SIZE);
- intent.putExtra("return-data", true);
- return intent;
- }
-
- /**
- * Saves or creates the contact based on the mode, and if successful
- * finishes the activity.
- */
- private boolean doSaveAction(int saveMode) {
- if (!hasValidState()) {
- return false;
- }
-
- // TODO: Status still needed?
- //mStatus = STATUS_SAVING;
- final PersistTask task = new PersistTask(this, saveMode);
- task.execute(mState);
-
- return true;
- }
-
- private boolean doRevertAction() {
- if (mCallbacks != null) mCallbacks.closeAfterRevert();
-
- return true;
- }
-
- private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
- switch (saveMode) {
- case SAVE_MODE_DEFAULT:
- final Intent resultIntent;
- final int resultCode;
- if (success && contactLookupUri != null) {
- final String requestAuthority = mUri == null ? null : mUri.getAuthority();
-
- final String legacyAuthority = "contacts";
-
- resultIntent = new Intent();
- if (legacyAuthority.equals(requestAuthority)) {
- // Build legacy Uri when requested by caller
- final long contactId = ContentUris.parseId(Contacts.lookupContact(
- mContext.getContentResolver(), contactLookupUri));
- final Uri legacyContentUri = Uri.parse("content://contacts/people");
- final Uri legacyUri = ContentUris.withAppendedId(
- legacyContentUri, contactId);
- resultIntent.setData(legacyUri);
- } else {
- // Otherwise pass back a lookup-style Uri
- resultIntent.setData(contactLookupUri);
- }
-
- resultCode = Activity.RESULT_OK;
- } else {
- resultCode = Activity.RESULT_CANCELED;
- resultIntent = null;
- }
- if (mCallbacks != null) mCallbacks.closeAfterSaving(resultCode, resultIntent);
- break;
- case SAVE_MODE_SPLIT:
- if (mCallbacks != null) mCallbacks.closeAfterSplit();
- break;
-
- case SAVE_MODE_JOIN:
- //mStatus = STATUS_EDITING;
- if (success) {
- showJoinAggregateActivity(contactLookupUri);
- }
- break;
- }
- }
-
- /**
- * Shows a list of aggregates that can be joined into the currently viewed aggregate.
- *
- * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
- */
- private void showJoinAggregateActivity(Uri contactLookupUri) {
- if (contactLookupUri == null) {
- return;
- }
-
- mContactIdForJoin = ContentUris.parseId(contactLookupUri);
- final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
- intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
- getActivity().startActivityForResult(intent, R.id.edit_request_code_join);
- }
-
- private interface JoinContactQuery {
- String[] PROJECTION = {
- RawContacts._ID,
- RawContacts.CONTACT_ID,
- RawContacts.NAME_VERIFIED,
- };
-
- String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
-
- int _ID = 0;
- int CONTACT_ID = 1;
- int NAME_VERIFIED = 2;
- }
-
- /**
- * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
- */
- private void joinAggregate(final long contactId) {
- final ContentResolver resolver = mContext.getContentResolver();
-
- // Load raw contact IDs for all raw contacts involved - currently edited and selected
- // in the join UIs
- Cursor c = resolver.query(RawContacts.CONTENT_URI,
- JoinContactQuery.PROJECTION,
- JoinContactQuery.SELECTION,
- new String[]{String.valueOf(contactId), String.valueOf(mContactIdForJoin)}, null);
-
- long rawContactIds[];
- long verifiedNameRawContactId = -1;
- try {
- rawContactIds = new long[c.getCount()];
- for (int i = 0; i < rawContactIds.length; i++) {
- c.moveToNext();
- long rawContactId = c.getLong(JoinContactQuery._ID);
- rawContactIds[i] = rawContactId;
- if (c.getLong(JoinContactQuery.CONTACT_ID) == mContactIdForJoin) {
- if (verifiedNameRawContactId == -1
- || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0) {
- verifiedNameRawContactId = rawContactId;
- }
- }
- }
- } finally {
- c.close();
- }
-
- // For each pair of raw contacts, insert an aggregation exception
- ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
- for (int i = 0; i < rawContactIds.length; i++) {
- for (int j = 0; j < rawContactIds.length; j++) {
- if (i != j) {
- buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
- }
- }
- }
-
- // Mark the original contact as "name verified" to make sure that the contact
- // display name does not change as a result of the join
- Builder builder = ContentProviderOperation.newUpdate(
- ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
- builder.withValue(RawContacts.NAME_VERIFIED, 1);
- operations.add(builder.build());
-
- // Apply all aggregation exceptions as one batch
- try {
- resolver.applyBatch(ContactsContract.AUTHORITY, operations);
-
- // We can use any of the constituent raw contacts to refresh the UI - why not the first
- final Intent intent = new Intent();
- intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
-
- // Reload the new state from database
- // TODO: Reload necessary or do we have a listener?
- //new QueryEntitiesTask(this).execute(intent);
-
- Toast.makeText(mContext, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to apply aggregation exception batch", e);
- Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
- } catch (OperationApplicationException e) {
- Log.e(TAG, "Failed to apply aggregation exception batch", e);
- Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
- */
- private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
- long rawContactId1, long rawContactId2) {
- Builder builder =
- ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
- builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
- builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
- builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
- operations.add(builder.build());
- }
-
- public static interface Callbacks {
- /**
- * Contact was not found, so somehow close this fragment.
- */
- void closeBecauseContactNotFound();
-
- /**
- * Contact was split, so we can close now
- */
- void closeAfterSplit();
-
- /**
- * User was presented with an account selection and couldn't decide.
- */
- void closeBecauseAccountSelectorAborted();
-
- /**
- * User has tapped Revert, close the fragment now.
- */
- void closeAfterRevert();
-
- /**
- * User has removed the contact, close the fragment now.
- */
- void closeAfterDelete();
-
- /**
- * Set the Title (e.g. of the Activity)
- */
- void setTitleTo(int resourceId);
-
- /**
- * Contact was
- * @param resultCode
- * @param resultIntent
- */
- void closeAfterSaving(int resultCode, Intent resultIntent);
- }
-
- private class EntityDeltaComparator implements Comparator<EntityDelta> {
- /**
- * Compare EntityDeltas for sorting the stack of editors.
- */
- public int compare(EntityDelta one, EntityDelta two) {
- // Check direct equality
- if (one.equals(two)) {
- return 0;
- }
-
- final Sources sources = Sources.getInstance(mContext);
- String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource oneSource = sources.getInflatedSource(accountType,
- ContactsSource.LEVEL_SUMMARY);
- accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource twoSource = sources.getInflatedSource(accountType,
- ContactsSource.LEVEL_SUMMARY);
-
- // Check read-only
- if (oneSource.readOnly && !twoSource.readOnly) {
- return 1;
- } else if (twoSource.readOnly && !oneSource.readOnly) {
- return -1;
- }
-
- // Check account type
- boolean skipAccountTypeCheck = false;
- boolean oneIsGoogle = oneSource instanceof GoogleSource;
- boolean twoIsGoogle = twoSource instanceof GoogleSource;
- if (oneIsGoogle && !twoIsGoogle) {
- return -1;
- } else if (twoIsGoogle && !oneIsGoogle) {
- return 1;
- } else if (oneIsGoogle && twoIsGoogle){
- skipAccountTypeCheck = true;
- }
-
- int value;
- if (!skipAccountTypeCheck) {
- value = oneSource.accountType.compareTo(twoSource.accountType);
- if (value != 0) {
- return value;
- }
- }
-
- // Check account name
- ValuesDelta oneValues = one.getValues();
- String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
- if (oneAccount == null) oneAccount = "";
- ValuesDelta twoValues = two.getValues();
- String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
- if (twoAccount == null) twoAccount = "";
- value = oneAccount.compareTo(twoAccount);
- if (value != 0) {
- return value;
- }
-
- // Both are in the same account, fall back to contact ID
- Long oneId = oneValues.getAsLong(RawContacts._ID);
- Long twoId = twoValues.getAsLong(RawContacts._ID);
- if (oneId == null) {
- return -1;
- } else if (twoId == null) {
- return 1;
- }
-
- return (int)(oneId - twoId);
- }
- }
-
- /**
- * Class that listens to requests coming from photo editors
- */
- private class PhotoListener implements EditorListener, DialogInterface.OnClickListener {
- private long mRawContactId;
- private boolean mReadOnly;
- private PhotoEditorView mEditor;
-
- public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) {
- mRawContactId = rawContactId;
- mReadOnly = readOnly;
- mEditor = editor;
- }
-
- public void onDeleted(Editor editor) {
- // Do nothing
- }
-
- public void onRequest(int request) {
- if (!hasValidState()) return;
-
- if (request == EditorListener.REQUEST_PICK_PHOTO) {
- if (mEditor.hasSetPhoto()) {
- // There is an existing photo, offer to remove, replace, or promoto to primary
- createPhotoDialog().show();
- } else if (!mReadOnly) {
- // No photo set and not read-only, try to set the photo
- doPickPhotoAction(mRawContactId);
- }
- }
- }
-
- /**
- * Prepare dialog for picking a new {@link EditType} or entering a
- * custom label. This dialog is limited to the valid types as determined
- * by {@link EntityModifier}.
- */
- public Dialog createPhotoDialog() {
- // Wrap our context to inflate list items using correct theme
- final Context dialogContext = new ContextThemeWrapper(mContext,
- android.R.style.Theme_Light);
-
- String[] choices;
- if (mReadOnly) {
- choices = new String[1];
- choices[0] = mContext.getString(R.string.use_photo_as_primary);
- } else {
- choices = new String[3];
- choices[0] = mContext.getString(R.string.use_photo_as_primary);
- choices[1] = mContext.getString(R.string.removePicture);
- choices[2] = mContext.getString(R.string.changePicture);
- }
- final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
- android.R.layout.simple_list_item_1, choices);
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
- builder.setTitle(R.string.attachToContact);
- builder.setSingleChoiceItems(adapter, -1, this);
- return builder.create();
- }
-
- /**
- * Called when something in the dialog is clicked
- */
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
-
- switch (which) {
- case 0:
- // Set the photo as super primary
- mEditor.setSuperPrimary(true);
-
- // And set all other photos as not super primary
- int count = mContent.getChildCount();
- for (int i = 0; i < count; i++) {
- View childView = mContent.getChildAt(i);
- if (childView instanceof BaseContactEditorView) {
- BaseContactEditorView editor = (BaseContactEditorView) childView;
- PhotoEditorView photoEditor = editor.getPhotoEditor();
- if (!photoEditor.equals(mEditor)) {
- photoEditor.setSuperPrimary(false);
- }
- }
- }
- break;
-
- case 1:
- // Remove the photo
- mEditor.setPhotoBitmap(null);
- break;
-
- case 2:
- // Pick a new photo for the contact
- doPickPhotoAction(mRawContactId);
- break;
- }
- }
- }
-
-
- private class DeleteClickListener implements DialogInterface.OnClickListener {
- public void onClick(DialogInterface dialog, int which) {
- // TODO: Don't do this from the UI thread
- final Sources sources = Sources.getInstance(mContext);
- // Mark all raw contacts for deletion
- for (EntityDelta delta : mState) {
- delta.markDeleted();
- }
- // Save the deletes
- doSaveAction(SAVE_MODE_DEFAULT);
- mCallbacks.closeAfterDelete();
- }
- }
-
- // TODO: There has to be a nicer way than this WeakAsyncTask...? Maybe call a service?
- /**
- * Background task for persisting edited contact data, using the changes
- * defined by a set of {@link EntityDelta}. This task starts
- * {@link EmptyService} to make sure the background thread can finish
- * persisting in cases where the system wants to reclaim our process.
- */
- public static class PersistTask extends
- WeakAsyncTask<EntitySet, Void, Integer, ContactEditFragment> {
- private static final int PERSIST_TRIES = 3;
-
- private static final int RESULT_UNCHANGED = 0;
- private static final int RESULT_SUCCESS = 1;
- private static final int RESULT_FAILURE = 2;
-
- private WeakReference<ProgressDialog> mProgress;
- private final Context mContext;
-
- private int mSaveMode;
- private Uri mContactLookupUri = null;
-
- public PersistTask(ContactEditFragment target, int saveMode) {
- super(target);
- mSaveMode = saveMode;
- mContext = target.mContext;
- }
-
- /** {@inheritDoc} */
- @Override
- protected void onPreExecute(ContactEditFragment target) {
- mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(mContext, null,
- mContext.getText(R.string.savingContact)));
-
- // Before starting this task, start an empty service to protect our
- // process from being reclaimed by the system.
- mContext.startService(new Intent(mContext, EmptyService.class));
- }
-
- /** {@inheritDoc} */
- @Override
- protected Integer doInBackground(ContactEditFragment target, EntitySet... params) {
- final ContentResolver resolver = mContext.getContentResolver();
-
- EntitySet state = params[0];
-
- // Trim any empty fields, and RawContacts, before persisting
- final Sources sources = Sources.getInstance(mContext);
- EntityModifier.trimEmpty(state, sources);
-
- // Attempt to persist changes
- int tries = 0;
- Integer result = RESULT_FAILURE;
- while (tries++ < PERSIST_TRIES) {
- try {
- // Build operations and try applying
- final ArrayList<ContentProviderOperation> diff = state.buildDiff();
- ContentProviderResult[] results = null;
- if (!diff.isEmpty()) {
- results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
- }
-
- final long rawContactId = getRawContactId(state, diff, results);
- if (rawContactId != -1) {
- final Uri rawContactUri = ContentUris.withAppendedId(
- RawContacts.CONTENT_URI, rawContactId);
-
- // convert the raw contact URI to a contact URI
- mContactLookupUri = RawContacts.getContactLookupUri(resolver,
- rawContactUri);
- }
- result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
- break;
-
- } catch (RemoteException e) {
- // Something went wrong, bail without success
- Log.e(TAG, "Problem persisting user edits", e);
- break;
-
- } catch (OperationApplicationException e) {
- // Version consistency failed, re-parent change and try again
- Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
- final EntitySet newState = EntitySet.fromQuery(resolver,
- target.mQuerySelection, target.mQuerySelectionArgs, null);
- state = EntitySet.mergeAfter(newState, state);
- }
- }
-
- return result;
- }
-
- private long getRawContactId(EntitySet state,
- final ArrayList<ContentProviderOperation> diff,
- final ContentProviderResult[] results) {
- long rawContactId = state.findRawContactId();
- if (rawContactId != -1) {
- return rawContactId;
- }
-
- // we gotta do some searching for the id
- final int diffSize = diff.size();
- for (int i = 0; i < diffSize; i++) {
- ContentProviderOperation operation = diff.get(i);
- if (operation.getType() == ContentProviderOperation.TYPE_INSERT
- && operation.getUri().getEncodedPath().contains(
- RawContacts.CONTENT_URI.getEncodedPath())) {
- return ContentUris.parseId(results[i].uri);
- }
- }
- return -1;
- }
-
- /** {@inheritDoc} */
- @Override
- protected void onPostExecute(ContactEditFragment target, Integer result) {
- final ProgressDialog progress = mProgress.get();
-
- if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) {
- Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
- } else if (result == RESULT_FAILURE) {
- Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
- }
-
- if (progress != null) progress.dismiss();
-
- // Stop the service that was protecting us
- mContext.stopService(new Intent(mContext, EmptyService.class));
-
- target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- if (hasValidState()) {
- // Store entities with modifications
- outState.putParcelable(KEY_EDIT_STATE, mState);
- }
-
- outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
- outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
- if (mCurrentPhotoFile != null) {
- outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
- }
- outState.putString(KEY_QUERY_SELECTION, mQuerySelection);
- outState.putStringArray(KEY_QUERY_SELECTION_ARGS, mQuerySelectionArgs);
- outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
- super.onSaveInstanceState(outState);
- }
-
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- // Ignore failed requests
- if (resultCode != Activity.RESULT_OK) return;
- switch (requestCode) {
- case R.id.edit_request_code_photo_picked_with_data: {
- BaseContactEditorView requestingEditor = null;
- for (int i = 0; i < mContent.getChildCount(); i++) {
- View childView = mContent.getChildAt(i);
- if (childView instanceof BaseContactEditorView) {
- BaseContactEditorView editor = (BaseContactEditorView) childView;
- if (editor.getRawContactId() == mRawContactIdRequestingPhoto) {
- requestingEditor = editor;
- break;
- }
- }
- }
-
- if (requestingEditor != null) {
- final Bitmap photo = data.getParcelableExtra("data");
- requestingEditor.setPhotoBitmap(photo);
- mRawContactIdRequestingPhoto = -1;
- } else {
- // The contact that requested the photo is no longer present.
- // TODO: Show error message
- }
-
- break;
- }
-
- case R.id.edit_request_code_camera_with_data: {
- doCropPhoto(mCurrentPhotoFile);
- break;
- }
- case R.id.edit_request_code_join: {
- if (data != null) {
- final long contactId = ContentUris.parseId(data.getData());
- joinAggregate(contactId);
- }
- }
- }
- }
-}
diff --git a/src/com/android/contacts/views/edit/ContactEditLoader.java b/src/com/android/contacts/views/edit/ContactEditLoader.java
deleted file mode 100644
index b89273c..0000000
--- a/src/com/android/contacts/views/edit/ContactEditLoader.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package com.android.contacts.views.edit;
-
-import com.android.contacts.ContactsUtils;
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
-import com.android.contacts.model.EntitySet;
-import com.android.contacts.model.Sources;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Loader;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.RawContacts;
-
-public class ContactEditLoader extends Loader<ContactEditLoader.Result> {
- private static final String TAG = "ContactEditLoader";
-
- private final Uri mLookupUri;
- private final String mMimeType;
- private Result mContact;
- private boolean mDestroyed;
- private ForceLoadContentObserver mObserver;
- private final Bundle mIntentExtras;
-
- public ContactEditLoader(Context context, Uri lookupUri, String mimeType,
- Bundle intentExtras) {
- super(context);
- mLookupUri = lookupUri;
- mMimeType = mimeType;
- mIntentExtras = intentExtras;
- }
-
- /**
- * The result of a load operation. Contains all data necessary to display the contact for
- * editing.
- */
- public static class Result {
- /**
- * Singleton instance that represents "No Contact Found"
- */
- public static final Result NOT_FOUND = new Result(null);
-
- private final EntitySet mEntitySet;
-
- private Result(EntitySet entitySet) {
- mEntitySet = entitySet;
- }
-
- public EntitySet getEntitySet() {
- return mEntitySet;
- }
- }
-
- private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
- @Override
- protected Result doInBackground(Void... params) {
- final ContentResolver resolver = getContext().getContentResolver();
- final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
-
- // Handle both legacy and new authorities
-
- final long contactId;
- final String selection = "0";
- if (Contacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
- // Handle selected aggregate
- contactId = ContentUris.parseId(uriCurrentFormat);
- } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
- // Get id of corresponding aggregate
- final long rawContactId = ContentUris.parseId(uriCurrentFormat);
- contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
- } else throw new IllegalStateException();
-
- return new Result(EntitySet.fromQuery(resolver, RawContacts.CONTACT_ID + "=?",
- new String[] { String.valueOf(contactId) }, null));
- }
-
- /**
- * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
- * For legacy contacts, a raw-contact lookup is performed.
- */
- private Uri convertLegacyIfNecessary(Uri uri) {
- if (uri == null) throw new IllegalArgumentException("uri must not be null");
-
- final String authority = uri.getAuthority();
-
- // Current Style Uri? Just return it
- if (ContactsContract.AUTHORITY.equals(authority)) {
- return uri;
- }
-
- // Legacy Style? Convert to RawContact
- final String OBSOLETE_AUTHORITY = "contacts";
- if (OBSOLETE_AUTHORITY.equals(authority)) {
- // Legacy Format. Convert to RawContact-Uri and then lookup the contact
- final long rawContactId = ContentUris.parseId(uri);
- return RawContacts.getContactLookupUri(getContext().getContentResolver(),
- ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
- }
-
- throw new IllegalArgumentException("uri format is unknown");
- }
-
- @Override
- protected void onPostExecute(Result result) {
- super.onPostExecute(result);
-
- // TODO: This merging of extras is probably wrong on subsequent loads
-
- // Load edit details in background
- final Sources sources = Sources.getInstance(getContext());
-
- // Handle any incoming values that should be inserted
- final boolean hasExtras = mIntentExtras != null && mIntentExtras.size() > 0;
- final boolean hasState = result.getEntitySet().size() > 0;
- if (hasExtras && hasState) {
- // Find source defining the first RawContact found
- final EntityDelta state = result.getEntitySet().get(0);
- final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource source = sources.getInflatedSource(accountType,
- ContactsSource.LEVEL_CONSTRAINTS);
- EntityModifier.parseExtras(getContext(), source, state, mIntentExtras);
- }
-
- // The creator isn't interested in any further updates
- if (mDestroyed) {
- return;
- }
-
- mContact = result;
- if (result != null) {
- if (mObserver == null) {
- mObserver = new ForceLoadContentObserver();
- }
- // TODO: Do we want a content observer here?
-// Log.i(TAG, "Registering content observer for " + mLookupUri);
-// getContext().getContentResolver().registerContentObserver(mLookupUri, true,
-// mObserver);
- deliverResult(result);
- }
- }
- }
-
- @Override
- public void startLoading() {
- if (mContact != null) {
- deliverResult(mContact);
- } else {
- forceLoad();
- }
- }
-
- @Override
- public void forceLoad() {
- LoadContactTask task = new LoadContactTask();
- task.execute((Void[])null);
- }
-
- @Override
- public void stopLoading() {
- mContact = null;
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- }
- }
-
- @Override
- public void destroy() {
- mContact = null;
- mDestroyed = true;
- }
-}
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
new file mode 100644
index 0000000..8626939
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -0,0 +1,1041 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor;
+
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.R;
+import com.android.contacts.TypePrecedence;
+import com.android.contacts.activities.ContactFieldEditorActivity;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.ui.EditContactActivity;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.ContactLoader;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.LoaderManagingFragment;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Entity.NamedContentValues;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+import java.util.zip.Inflater;
+
+public class ContactEditorFragment extends LoaderManagingFragment<ContactLoader.Result>
+ implements OnCreateContextMenuListener, OnItemClickListener {
+ private static final String TAG = "ContactEditorFragment";
+
+ private static final String BUNDLE_RAW_CONTACT_ID = "rawContactId";
+
+ private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+ private static final int LOADER_DETAILS = 1;
+
+ private Context mContext;
+ private Uri mLookupUri;
+ private Listener mListener;
+
+ private ContactLoader.Result mContactData;
+ private ContactEditorHeaderView mHeaderView;
+ private ListView mListView;
+ private ViewAdapter mAdapter;
+
+ private int mReadOnlySourcesCnt;
+ private int mWritableSourcesCnt;
+ private boolean mAllRestricted;
+
+ /**
+ * A list of RawContacts included in this Contact.
+ */
+ private ArrayList<RawContact> mRawContacts = new ArrayList<RawContact>();
+
+ private LayoutInflater mInflater;
+
+ public ContactEditorFragment() {
+ // Explicit constructor for inflation
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mContext = activity;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
+
+ mInflater = inflater;
+
+ mHeaderView =
+ (ContactEditorHeaderView) view.findViewById(R.id.contact_header_widget);
+
+ mListView = (ListView) view.findViewById(android.R.id.list);
+ mListView.setOnCreateContextMenuListener(this);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ mListView.setOnItemClickListener(this);
+
+ return view;
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ public void loadUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ }
+
+ @Override
+ protected Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactLoader(mContext, mLookupUri);
+ }
+ default: {
+ Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<ContactLoader.Result> loader,
+ ContactLoader.Result data) {
+ final int id = loader.getId();
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ mListener.onContactNotFound();
+ return;
+ }
+ if (data == ContactLoader.Result.ERROR) {
+ // Item has been deleted
+ Log.i(TAG, "Error fetching contact. Closing activity");
+ mListener.onError();
+ return;
+ }
+ mContactData = data;
+ bindData();
+ break;
+ default: {
+ Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+ }
+ }
+ }
+
+ private void bindData() {
+ // Build up the contact entries
+ buildEntries();
+
+ mHeaderView.setMergeInfo(mRawContacts.size());
+
+ if (mAdapter == null) {
+ mAdapter = new ViewAdapter();
+ mListView.setAdapter(mAdapter);
+ } else {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Build up the entries to display on the screen.
+ */
+ private final void buildEntries() {
+ // Clear out the old entries
+ mRawContacts.clear();
+
+ mReadOnlySourcesCnt = 0;
+ mWritableSourcesCnt = 0;
+ mAllRestricted = true;
+
+ // TODO: This should be done in the background thread
+ final Sources sources = Sources.getInstance(mContext);
+
+ // Build up method entries
+ if (mContactData == null) {
+ return;
+ }
+
+ for (Entity entity: mContactData.getEntities()) {
+ final ContentValues entValues = entity.getEntityValues();
+ final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
+ final String accountName = entValues.getAsString(RawContacts.ACCOUNT_NAME);
+ final long rawContactId = entValues.getAsLong(RawContacts._ID);
+ final String rawContactUriString = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactId).toString();
+
+ // Mark when this contact has any unrestricted components
+ final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+ if (!isRestricted) mAllRestricted = false;
+
+ final ContactsSource contactsSource = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_SUMMARY);
+ final boolean writable = contactsSource == null || !contactsSource.readOnly;
+ if (writable) {
+ mWritableSourcesCnt += 1;
+ } else {
+ mReadOnlySourcesCnt += 1;
+ }
+
+ final RawContact rawContact =
+ new RawContact(contactsSource, accountName, rawContactId, writable);
+ mRawContacts.add(rawContact);
+
+ for (NamedContentValues subValue : entity.getSubValues()) {
+ final ContentValues entryValues = subValue.values;
+ entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+ final long dataId = entryValues.getAsLong(Data._ID);
+ final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+ if (mimeType == null) continue;
+
+ final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
+ ContactsSource.LEVEL_MIMETYPES);
+ if (kind == null) continue;
+
+ final DataViewEntry entry = new DataViewEntry(mContext, mimeType, kind,
+ rawContact, dataId, entryValues);
+
+ final boolean hasData = !TextUtils.isEmpty(entry.data);
+ final boolean isSuperPrimary = entryValues.getAsInteger(
+ Data.IS_SUPER_PRIMARY) != 0;
+
+ final Intent itemEditIntent = new Intent(Intent.ACTION_EDIT, entry.uri);
+ itemEditIntent.putExtra(ContactFieldEditorActivity.BUNDLE_RAW_CONTACT_URI,
+ rawContactUriString);
+ entry.intent = itemEditIntent;
+ entry.actionIcon = R.drawable.edit;
+
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ rawContact.getFields().add(entry);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ rawContact.getFields().add(entry);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Remember super-primary phone
+ entry.isPrimary = isSuperPrimary;
+ rawContact.getFields().add(entry);
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build email entries
+ entry.isPrimary = isSuperPrimary;
+ rawContact.getFields().add(entry);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build postal entries
+ rawContact.getFields().add(entry);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build IM entries
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = mContext.getString(R.string.chat).toLowerCase();
+ }
+ rawContact.getFields().add(entry);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
+ (hasData || !TextUtils.isEmpty(entry.label))) {
+ entry.uri = null;
+
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = entry.data;
+ entry.data = "";
+ }
+
+ rawContact.getFields().add(entry);
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ entry.uri = null;
+ rawContact.getFields().add(entry);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ entry.uri = null;
+ entry.maxLines = 100;
+ rawContact.getFields().add(entry);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ entry.uri = null;
+ entry.maxLines = 10;
+ rawContact.getFields().add(entry);
+ } else {
+ // Use social summary when requested by external source
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ final boolean hasSocial = kind.actionBodySocial && status != null;
+
+ if (hasSocial || hasData) {
+ rawContact.getFields().add(entry);
+ }
+ }
+ }
+ }
+ }
+
+ private static String buildActionString(DataKind kind, ContentValues values,
+ boolean lowerCase, Context context) {
+ if (kind.actionHeader == null) {
+ return null;
+ }
+ CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+ if (actionHeader == null) {
+ return null;
+ }
+ return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+ }
+
+ private static String buildDataString(DataKind kind, ContentValues values,
+ Context context) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ private abstract static class BaseViewEntry {
+ private final RawContact mRawContact;
+
+ public BaseViewEntry(RawContact rawContact) {
+ mRawContact = rawContact;
+ }
+
+ public RawContact getRawContact() {
+ return mRawContact;
+ }
+
+ public abstract int getEntryType();
+ public abstract View getView(View convertView, ViewGroup parent);
+ }
+
+ private class HeaderViewEntry extends BaseViewEntry {
+ private boolean mCollapsed;
+
+ public HeaderViewEntry(RawContact rawContact) {
+ super(rawContact);
+ }
+
+ public boolean isCollapsed() {
+ return mCollapsed;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ mCollapsed = collapsed;
+ }
+
+ @Override
+ public int getEntryType() {
+ return ItemTypes.RAW_CONTACT_HEADER;
+ }
+
+ @Override
+ public View getView(View convertView, ViewGroup parent) {
+ final View result;
+ final HeaderItemViewCache viewCache;
+ if (convertView != null) {
+ result = convertView;
+ viewCache = (HeaderItemViewCache) result.getTag();
+ } else {
+ result = mInflater.inflate(R.layout.list_edit_item_header, parent, false);
+ viewCache = new HeaderItemViewCache();
+ result.setTag(viewCache);
+ viewCache.logo = (ImageView) result.findViewById(R.id.logo);
+ viewCache.caption = (TextView) result.findViewById(R.id.caption);
+ }
+
+ CharSequence accountType = getRawContact().getSource().getDisplayLabel(mContext);
+ if (TextUtils.isEmpty(accountType)) {
+ accountType = mContext.getString(R.string.account_phone);
+ }
+ final String accountName = getRawContact().getAccountName();
+
+ final String accountTypeDisplay;
+ if (TextUtils.isEmpty(accountName)) {
+ accountTypeDisplay = mContext.getString(R.string.account_type_format,
+ accountType);
+ } else {
+ accountTypeDisplay = mContext.getString(R.string.account_type_and_name,
+ accountType, accountName);
+ }
+
+ viewCache.caption.setText(accountTypeDisplay);
+ viewCache.logo.setImageDrawable(getRawContact().getSource().getDisplayIcon(mContext));
+
+ return result;
+ }
+ }
+
+ private class FooterViewEntry extends BaseViewEntry {
+ public FooterViewEntry(RawContact rawContact) {
+ super(rawContact);
+ }
+
+ @Override
+ public int getEntryType() {
+ return ItemTypes.RAW_CONTACT_FOOTER;
+ }
+
+ @Override
+ public View getView(View convertView, ViewGroup parent) {
+ final View result;
+ final FooterItemViewCache viewCache;
+ if (convertView != null) {
+ result = convertView;
+ viewCache = (FooterItemViewCache) result.getTag();
+ } else {
+ result = mInflater.inflate(R.layout.list_edit_item_footer, parent, false);
+ viewCache = new FooterItemViewCache();
+ result.setTag(viewCache);
+ viewCache.addInformationButton =
+ (Button) result.findViewById(R.id.add_information);
+ viewCache.separateButton =
+ (Button) result.findViewById(R.id.separate);
+ viewCache.deleteButton =
+ (Button) result.findViewById(R.id.deleteButton);
+ viewCache.addInformationButton.setOnClickListener(
+ mAddInformationButtonClickListener);
+ }
+
+ viewCache.viewEntry = this;
+ return result;
+ }
+ }
+
+ private class DataViewEntry extends BaseViewEntry {
+ public String label;
+ public String data;
+ public Uri uri;
+ public long id = 0;
+ public int maxLines = 1;
+ public String mimetype;
+
+ public int actionIcon = -1;
+ public boolean isPrimary = false;
+ public Intent intent;
+ public Intent secondaryIntent = null;
+ public int maxLabelLines = 1;
+ public byte[] binaryData = null;
+
+ /**
+ * Build new {@link DataViewEntry} and populate from the given values.
+ */
+ public DataViewEntry(Context context, String mimeType, DataKind kind,
+ RawContact rawContact, long dataId, ContentValues values) {
+ super(rawContact);
+ id = dataId;
+ uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
+ mimetype = mimeType;
+ label = buildActionString(kind, values, false, context);
+ data = buildDataString(kind, values, context);
+ binaryData = values.getAsByteArray(Data.DATA15);
+ }
+
+ @Override
+ public int getEntryType() {
+ return Photo.CONTENT_ITEM_TYPE.equals(mimetype) ? ItemTypes.PHOTO : ItemTypes.DATA;
+ }
+
+ @Override
+ public View getView(View convertView, ViewGroup parent) {
+ final View result;
+ if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ final PhotoItemViewCache viewCache;
+ if (convertView != null) {
+ result = convertView;
+ viewCache = (PhotoItemViewCache) result.getTag();
+ } else {
+ // Create a new view if needed
+ result = mInflater.inflate(R.layout.list_edit_item_photo, parent, false);
+
+ // Cache the children
+ viewCache = new PhotoItemViewCache();
+ viewCache.photo = (ImageView) result.findViewById(R.id.photo);
+ viewCache.galleryActionButton =
+ (ImageView) result.findViewById(R.id.action_icon);
+ viewCache.takePhotoActionButton =
+ (ImageView) result.findViewById(R.id.secondary_action_button);
+ result.setTag(viewCache);
+ }
+ final Bitmap bitmap = binaryData != null
+ ? BitmapFactory.decodeByteArray(binaryData, 0, binaryData.length)
+ : null;
+ viewCache.photo.setImageBitmap(bitmap);
+ } else {
+ final DataItemViewCache viewCache;
+ if (convertView != null) {
+ result = convertView;
+ viewCache = (DataItemViewCache) result.getTag();
+ } else {
+ // Create a new view if needed
+ result = mInflater.inflate(R.layout.list_edit_item_text_icons, parent,
+ false);
+
+ // Cache the children
+ viewCache = new DataItemViewCache();
+ viewCache.label = (TextView) result.findViewById(android.R.id.text1);
+ viewCache.data = (TextView) result.findViewById(android.R.id.text2);
+ viewCache.actionIcon = (ImageView) result.findViewById(R.id.action_icon);
+ viewCache.primaryIcon = (ImageView) result.findViewById(R.id.primary_icon);
+ viewCache.secondaryActionButton = (ImageView) result.findViewById(
+ R.id.secondary_action_button);
+ viewCache.secondaryActionDivider = result.findViewById(R.id.divider);
+ result.setTag(viewCache);
+ }
+ final Resources resources = mContext.getResources();
+
+ // Set the label
+ setMaxLines(viewCache.label, maxLabelLines);
+ viewCache.label.setText(label);
+
+ if (data != null) {
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
+ || Constants.MIME_SMS_ADDRESS.equals(mimetype)) {
+ viewCache.data.setText(PhoneNumberUtils.formatNumber(data));
+ } else {
+ viewCache.data.setText(data);
+ }
+ setMaxLines(viewCache.data, maxLines);
+ }
+
+ // Set the primary icon
+ viewCache.primaryIcon.setVisibility(isPrimary ? View.VISIBLE : View.GONE);
+
+ // Set the action icon
+ final ImageView action = viewCache.actionIcon;
+ if (intent != null) {
+ action.setImageDrawable(resources.getDrawable(actionIcon));
+ action.setVisibility(View.VISIBLE);
+ } else {
+ action.setVisibility(View.INVISIBLE);
+ }
+
+ // Set the secondary action button
+ final ImageView secondaryActionView = viewCache.secondaryActionButton;
+ secondaryActionView.setVisibility(View.GONE);
+ viewCache.secondaryActionDivider.setVisibility(View.GONE);
+ }
+ return result;
+ }
+
+ private void setMaxLines(TextView textView, int maxLines) {
+ if (maxLines == 1) {
+ textView.setSingleLine(true);
+ textView.setEllipsize(TextUtils.TruncateAt.END);
+ } else {
+ textView.setSingleLine(false);
+ textView.setMaxLines(maxLines);
+ textView.setEllipsize(null);
+ }
+ }
+ }
+
+ /** Cache of the header of a raw contact */
+ private static class HeaderItemViewCache {
+ public ImageView logo;
+ public TextView caption;
+ }
+
+ /** Cache of the footer of a raw contact */
+ private static class FooterItemViewCache {
+ public Button addInformationButton;
+ public Button separateButton;
+ public Button deleteButton;
+
+ public FooterViewEntry viewEntry;
+ }
+
+ /** Cache of the children views of a row */
+ private static class DataItemViewCache {
+ public TextView label;
+ public TextView data;
+ public ImageView actionIcon;
+ public ImageView primaryIcon;
+ public ImageView secondaryActionButton;
+ public View secondaryActionDivider;
+ }
+
+ /** Cache of the children views of a row */
+ private static class PhotoItemViewCache {
+ public ImageView photo;
+ public ImageView takePhotoActionButton;
+ public ImageView galleryActionButton;
+ }
+
+ /** Possible Item Types */
+ private interface ItemTypes {
+ public static final int DATA = 0;
+ public static final int PHOTO = 1;
+ public static final int RAW_CONTACT_HEADER = 2;
+ public static final int RAW_CONTACT_FOOTER = 3;
+ public static final int _COUNT = 4;
+ }
+
+ private final class ViewAdapter extends BaseAdapter {
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View result;
+
+ final BaseViewEntry viewEntry = getEntry(position);
+ return viewEntry.getView(convertView, parent);
+ }
+
+ public Object getItem(int position) {
+ return getEntry(position);
+ }
+
+ public long getItemId(int position) {
+ // TODO Get a unique Id. Use negative numbers for Headers/Footers
+ return position;
+ }
+
+ private BaseViewEntry getEntry(int position) {
+ for (int i = 0; i < mRawContacts.size(); i++) {
+ final RawContact rawContact = mRawContacts.get(i);
+ if (position == 0) return rawContact.getHeader();
+
+ // Collapsed header? Count one item and continue
+ if (rawContact.getHeader().isCollapsed()) {
+ position--;
+ continue;
+ }
+
+ final ArrayList<DataViewEntry> fields = rawContact.getFields();
+ // +1 for header, +1 for footer
+ final int fieldCount = fields.size() + 2;
+ if (position == fieldCount - 1) {
+ return rawContact.getFooter();
+ }
+ if (position < fieldCount) {
+ // -1 for header
+ return fields.get(position - 1);
+ }
+ position -= fieldCount;
+ }
+ return null;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return ItemTypes._COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return getEntry(position).getEntryType();
+ }
+
+ public int getCount() {
+ int result = 0;
+ for (int i = 0; i < mRawContacts.size(); i++) {
+ final RawContact rawContact = mRawContacts.get(i);
+ if (rawContact.getHeader().isCollapsed()) {
+ // just one header item
+ result++;
+ } else {
+ // +1 for header, +1 for footer
+ result += rawContact.getFields().size() + 2;
+ }
+ }
+ return result;
+ }
+ }
+
+ public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.view, menu);
+ return true;
+ }
+
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // TODO: Prepare options
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_edit: {
+ // TODO: This is temporary code to invoke the old editor. We can get rid of this
+ // later
+ final Intent intent = new Intent();
+ intent.setClass(mContext, EditContactActivity.class);
+ final long rawContactId = mRawContacts.get(0).mId;
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactId);
+ intent.setAction(Intent.ACTION_EDIT);
+ intent.setData(rawContactUri);
+ startActivity(intent);
+ return true;
+ }
+ case R.id.menu_delete: {
+ showDeleteConfirmationDialog();
+ return true;
+ }
+ case R.id.menu_options: {
+ final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
+ intent.setData(mContactData.getLookupUri());
+ mContext.startActivity(intent);
+ return true;
+ }
+ case R.id.menu_share: {
+ if (mAllRestricted) return false;
+
+ final String lookupKey = mContactData.getLookupKey();
+ final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(Contacts.CONTENT_VCARD_TYPE);
+ intent.putExtra(Intent.EXTRA_STREAM, shareUri);
+
+ // Launch chooser to share contact via
+ final CharSequence chooseTitle = mContext.getText(R.string.share_via);
+ final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+ try {
+ mContext.startActivity(chooseIntent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void showDeleteConfirmationDialog() {
+ final int dialogId;
+ if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+ dialogId = R.id.detail_dialog_confirm_readonly_delete;
+ } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+ dialogId = R.id.detail_dialog_confirm_readonly_hide;
+ } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+ dialogId = R.id.detail_dialog_confirm_multiple_delete;
+ } else {
+ dialogId = R.id.detail_dialog_confirm_delete;
+ }
+ if (mListener != null) mListener.onDialogRequested(dialogId, null);
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return;
+ }
+
+ // This can be null sometimes, don't crash...
+ if (info == null) {
+ Log.e(TAG, "bad menuInfo");
+ return;
+ }
+
+ final BaseViewEntry baseEntry = mAdapter.getEntry(info.position);
+ if (baseEntry instanceof DataViewEntry) {
+ final DataViewEntry entry = (DataViewEntry) baseEntry;
+ menu.setHeaderTitle(R.string.contactOptionsTitle);
+ if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
+ menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
+ }
+ }
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (mListener == null) return;
+ final BaseViewEntry baseEntry = mAdapter.getEntry(position);
+ if (baseEntry == null) return;
+
+ if (baseEntry instanceof HeaderViewEntry) {
+ // Toggle rawcontact visibility
+ final HeaderViewEntry entry = (HeaderViewEntry) baseEntry;
+ entry.setCollapsed(!entry.isCollapsed());
+ mAdapter.notifyDataSetChanged();
+ } else if (baseEntry instanceof DataViewEntry) {
+ final DataViewEntry entry = (DataViewEntry) baseEntry;
+ final Intent intent = entry.intent;
+ if (intent == null) return;
+ mListener.onItemClicked(intent);
+ }
+ }
+
+ private final DialogInterface.OnClickListener mDeleteListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+ }
+ };
+
+ public Dialog onCreateDialog(int id, Bundle bundle) {
+ // TODO The delete dialogs mirror the functionality from the Contact-Detail-Fragment.
+ // Consider whether we can extract common logic here
+ // TODO The actual removal is not in a worker thread currently
+ switch (id) {
+ case R.id.detail_dialog_confirm_delete:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.deleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case R.id.detail_dialog_confirm_readonly_delete:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case R.id.detail_dialog_confirm_multiple_delete:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.multipleContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case R.id.detail_dialog_confirm_readonly_hide: {
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactWarning)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .create();
+ }
+ case R.id.edit_dialog_add_information: {
+ final long rawContactId = bundle.getLong(BUNDLE_RAW_CONTACT_ID);
+ final RawContact rawContact = findRawContactById(rawContactId);
+ if (rawContact == null) return null;
+ final ContactsSource source = rawContact.getSource();
+
+ final ArrayList<DataKind> sortedDataKinds = source.getSortedDataKinds();
+ final ArrayList<String> items = new ArrayList<String>(sortedDataKinds.size());
+ for (DataKind dataKind : sortedDataKinds) {
+ // TODO: Filter out fields that do not make sense in the current Context
+ // (Name, Photo, Notes etc)
+ if (dataKind.titleRes == -1) continue;
+ if (!dataKind.editable) continue;
+ final String title = mContext.getString(dataKind.titleRes);
+ items.add(title);
+ }
+ final DialogInterface.OnClickListener itemClickListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // TODO: Launch Intent to show Dialog
+// final KindSectionView view = (KindSectionView) mFields.getChildAt(which);
+// view.addItem();
+ }
+ };
+ return new AlertDialog.Builder(mContext)
+ .setItems(items.toArray(new String[0]), itemClickListener)
+ .create();
+ }
+ default:
+ return null;
+ }
+ }
+
+ private RawContact findRawContactById(long rawContactId) {
+ for (RawContact rawContact : mRawContacts) {
+ if (rawContact.getId() == rawContactId) return rawContact;
+ }
+ return null;
+ }
+
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_ITEM_MAKE_DEFAULT: {
+ if (makeItemDefault(item)) {
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean makeItemDefault(MenuItem item) {
+ final BaseViewEntry baseEntry = getViewEntryForMenuItem(item);
+ if (baseEntry == null || !(baseEntry instanceof DataViewEntry)) {
+ return false;
+ }
+ final DataViewEntry entry = (DataViewEntry) baseEntry;
+
+ // Update the primary values in the data record.
+ ContentValues values = new ContentValues(1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+
+ mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
+ values, null, null);
+ return true;
+ }
+
+ private BaseViewEntry getViewEntryForMenuItem(MenuItem item) {
+ final AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return null;
+ }
+
+ return mAdapter.getEntry(info.position);
+ }
+
+ public OnClickListener mAddInformationButtonClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ // The parent of the Button allows identifying the section
+ final View parentView = (View) v.getParent();
+ final FooterItemViewCache viewCache = (FooterItemViewCache) parentView.getTag();
+ final FooterViewEntry entry = viewCache.viewEntry;
+ final RawContact rawContact = entry.getRawContact();
+
+ // Create a bundle to show the Dialog
+ final Bundle bundle = new Bundle();
+ bundle.putLong(BUNDLE_RAW_CONTACT_ID, rawContact.getId());
+ if (mListener != null) mListener.onDialogRequested(R.id.edit_dialog_add_information,
+ bundle);
+ }
+ };
+
+ private class RawContact {
+ private final ContactsSource mSource;
+ private String mAccountName;
+ private final long mId;
+ private boolean mWritable;
+ private final HeaderViewEntry mHeader = new HeaderViewEntry(this);
+ private final FooterViewEntry mFooter = new FooterViewEntry(this);
+ private final ArrayList<DataViewEntry> mFields = new ArrayList<DataViewEntry>();
+
+ public RawContact(ContactsSource source, String accountName, long id, boolean writable) {
+ mSource = source;
+ mAccountName = accountName;
+ mId = id;
+ mWritable = writable;
+ }
+
+ public ContactsSource getSource() {
+ return mSource;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public boolean isWritable() {
+ return mWritable;
+ }
+
+ public ArrayList<DataViewEntry> getFields() {
+ return mFields;
+ }
+
+ public HeaderViewEntry getHeader() {
+ return mHeader;
+ }
+
+ public FooterViewEntry getFooter() {
+ return mFooter;
+ }
+ }
+
+ public static interface Listener {
+ /**
+ * Contact was not found, so somehow close this fragment.
+ */
+ public void onContactNotFound();
+
+ /**
+ * There was an error loading the contact
+ */
+ public void onError();
+
+ /**
+ * User clicked a single item (e.g. mail)
+ */
+ public void onItemClicked(Intent intent);
+
+ /**
+ * Show a dialog using the globally unique id
+ */
+ public void onDialogRequested(int id, Bundle bundle);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/ContactEditorHeaderView.java b/src/com/android/contacts/views/editor/ContactEditorHeaderView.java
new file mode 100644
index 0000000..18cbf72
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactEditorHeaderView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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.views.editor;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * Header for displaying the title bar in the contact editor
+ */
+public class ContactEditorHeaderView extends FrameLayout {
+ private static final String TAG = "ContactEditorHeaderView";
+
+ private TextView mMergeInfo;
+
+ public ContactEditorHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public ContactEditorHeaderView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ContactEditorHeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.contact_editor_header_view, this);
+
+ mMergeInfo = (TextView) findViewById(R.id.merge_info);
+
+ // Start with unmerged
+ setMergeInfo(1);
+ }
+
+ public void setMergeInfo(int count) {
+ if (count <= 1) {
+ mMergeInfo.setVisibility(GONE);
+ } else {
+ mMergeInfo.setVisibility(VISIBLE);
+ mMergeInfo.setText(
+ getResources().getQuantityString(R.plurals.merge_info, count, count));
+ }
+ }
+}
diff --git a/src/com/android/contacts/views/editor/ContactFieldEditorBaseFragment.java b/src/com/android/contacts/views/editor/ContactFieldEditorBaseFragment.java
new file mode 100644
index 0000000..11b9481
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactFieldEditorBaseFragment.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor;
+
+import com.android.contacts.R;
+import com.android.contacts.views.ContactLoader;
+
+import android.app.Activity;
+import android.app.LoaderManagingFragment;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.content.Loader;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public abstract class ContactFieldEditorBaseFragment
+ extends LoaderManagingFragment<ContactLoader.Result> {
+ private static final String TAG = "ContactFieldEditorBaseFragment";
+
+ private static final int LOADER_DETAILS = 1;
+
+ private long mRawContactId;
+ private Uri mRawContactUri;
+ private long mDataId;
+ private Uri mDataUri;
+ private Context mContext;
+ private Listener mListener;
+ private ContactLoader.Result mContactData;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mContext = activity;
+ }
+
+ public ContactLoader.Result getContactData() {
+ return mContactData;
+ }
+
+ /**
+ * Sets the Uris for the Fragment. If the dataUri is null, this is interpreted as an Insert
+ * to an existing rawContact. rawContactUri must always be given
+ */
+ public void setupUris(Uri rawContactUri, Uri dataUri) {
+ mRawContactUri = rawContactUri;
+ mRawContactId = Long.parseLong(rawContactUri.getLastPathSegment());
+ mDataUri = dataUri;
+ mDataId = dataUri == null ? 0 : Long.parseLong(dataUri.getLastPathSegment());
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactLoader(mContext,
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, mRawContactId));
+ }
+ default: {
+ Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<ContactLoader.Result> loader,
+ ContactLoader.Result data) {
+ final int id = loader.getId();
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ if (mListener != null) mListener.onContactNotFound();
+ return;
+ }
+ if (data == ContactLoader.Result.ERROR) {
+ throw new IllegalStateException("Error during data loading");
+ }
+ mContactData = data;
+
+ // Find the correct RawContact
+ boolean valueFound = false;
+ for (Entity entity : mContactData.getEntities()) {
+ final ContentValues entValues = entity.getEntityValues();
+ final long rawContactId = entValues.getAsLong(RawContacts._ID);
+ if (rawContactId == mRawContactId) {
+ for (NamedContentValues subValue : entity.getSubValues()) {
+ final long dataId = subValue.values.getAsLong(Data._ID);
+ if (dataId == mDataId) {
+ loadData(subValue);
+ valueFound = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!valueFound) {
+ // Item has been deleted
+ Log.i(TAG, "Data item not found. Closing activity");
+ if (mListener != null) mListener.onDataNotFound();
+ return;
+ }
+ break;
+ default: {
+ Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+ }
+ }
+ }
+
+ protected void saveData() {
+ final ContentResolver resolver = getActivity().getContentResolver();
+
+ final ArrayList<ContentProviderOperation> operations =
+ new ArrayList<ContentProviderOperation>();
+
+ final Builder builder;
+ if (getDataUri() == null) {
+ // INSERT
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValue(Data.RAW_CONTACT_ID, getRawContactId());
+ saveData(builder);
+ } else {
+ // UPDATE
+ builder = ContentProviderOperation.newUpdate(getDataUri());
+ saveData(builder);
+ }
+ operations.add(builder.build());
+
+ // TODO: Do in Background thread/service
+ try {
+ resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+ } catch (RemoteException e) {
+ Toast.makeText(getActivity(), R.string.edit_error_saving, Toast.LENGTH_LONG).show();
+ } catch (OperationApplicationException e) {
+ Toast.makeText(getActivity(), R.string.edit_error_saving, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ protected abstract void saveData(final Builder builder);
+
+ protected abstract void loadData(NamedContentValues contentValues);
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public Listener getListener() {
+ return mListener;
+ }
+
+ public Uri getRawContactUri() {
+ return mRawContactUri;
+ }
+
+ public long getRawContactId() {
+ return mRawContactId;
+ }
+
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ public long getDataId() {
+ return mDataId;
+ }
+
+ public static interface Listener {
+ void onContactNotFound();
+ void onDataNotFound();
+ void onCancel();
+ void onSaved();
+ }
+}
diff --git a/src/com/android/contacts/views/editor/ContactFieldEditorEmailFragment.java b/src/com/android/contacts/views/editor/ContactFieldEditorEmailFragment.java
new file mode 100644
index 0000000..d838ea8
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactFieldEditorEmailFragment.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor;
+
+import com.android.contacts.R;
+import com.android.contacts.util.ViewGroupAnimator;
+
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+
+public class ContactFieldEditorEmailFragment extends ContactFieldEditorBaseFragment {
+ private EditText mEditText;
+ private View mCustomTypeContainer;
+ private EditText mCustomTypeTextView;
+ private SparseArray<Button> mTypeButtons = new SparseArray<Button>();
+ private int mSelectedType;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.contact_field_editor_email_fragment, container,
+ false);
+
+ mEditText = (EditText) view.findViewById(R.id.email_edit_text);
+ mTypeButtons.append(Email.TYPE_HOME, (Button) view.findViewById(R.id.email_type_home));
+ mTypeButtons.append(Email.TYPE_WORK, (Button) view.findViewById(R.id.email_type_work));
+ mTypeButtons.append(Email.TYPE_OTHER, (Button) view.findViewById(R.id.email_type_other));
+ mTypeButtons.append(Email.TYPE_CUSTOM, (Button) view.findViewById(R.id.email_type_custom));
+
+ mCustomTypeContainer = view.findViewById(R.id.email_edit_custom_type);
+ mCustomTypeTextView = (EditText) view.findViewById(R.id.email_edit_type_text);
+
+ for (int i = 0; i < mTypeButtons.size(); i++) {
+ final int type = mTypeButtons.keyAt(i);
+ final Button button = mTypeButtons.get(type);
+ button.setText(Email.getTypeLabelResource(type));
+ button.setOnClickListener(mTypeButtonOnClickListener);
+ }
+
+ final Button okButton = (Button) view.findViewById(R.id.btn_ok);
+ final Button cancelButton = (Button) view.findViewById(R.id.btn_cancel);
+ okButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ saveData();
+
+ if (getListener() != null) getListener().onSaved();
+ }
+ });
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (getListener() != null) getListener().onCancel();
+ }
+ });
+ return view;
+ }
+
+ private void pushTypeButton(int newType, boolean animate) {
+ boolean doAnimation =
+ animate && ((mSelectedType == Email.TYPE_CUSTOM) ^ (newType == Email.TYPE_CUSTOM));
+
+ final ViewGroupAnimator animator;
+ if (doAnimation) {
+ animator = ViewGroupAnimator.captureView(getView().getRootView());
+ } else {
+ animator = null;
+ }
+ for (int i = 0; i < mTypeButtons.size(); i++) {
+ final int type = mTypeButtons.keyAt(i);
+ final Button button = mTypeButtons.get(type);
+ button.setTextColor(newType == type ? Color.BLACK : Color.GRAY);
+ }
+
+ mCustomTypeContainer.setVisibility(newType == Email.TYPE_CUSTOM ? View.VISIBLE : View.GONE);
+
+ if (doAnimation) animator.animate();
+
+ mSelectedType = newType;
+ }
+
+ private OnClickListener mTypeButtonOnClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ final Button b = (Button) v;
+ final int index = mTypeButtons.indexOfValue(b);
+ final int type = mTypeButtons.keyAt(index);
+ pushTypeButton(type, true);
+ }
+ };
+
+ @Override
+ protected void loadData(NamedContentValues contentValues) {
+ mEditText.setText(contentValues.values.getAsString(Email.ADDRESS));
+ pushTypeButton(contentValues.values.getAsInteger(Email.TYPE).intValue(), false);
+ mCustomTypeTextView.setText(contentValues.values.getAsString(Email.LABEL));
+ }
+
+ @Override
+ protected void saveData(Builder builder) {
+ builder.withValue(Email.ADDRESS, mEditText.getText().toString());
+ builder.withValue(Email.TYPE, mSelectedType);
+ builder.withValue(Email.LABEL, mCustomTypeTextView.getText().toString());
+ }
+}