Merge "Making DefaultContactBrowserListFragment auto-configured"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 05797f3..10a0645 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -386,7 +386,7 @@
<!-- Edit or insert details for a contact -->
<activity
- android:name=".ui.EditContactActivity"
+ android:name=".activities.ContactEditActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter android:label="@string/editContactDescription">
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
index b4f8101..10d3e48 100644
--- a/res/layout/contact_detail.xml
+++ b/res/layout/contact_detail.xml
@@ -15,7 +15,7 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/contact_details"
+ android:id="@+id/contact_detail"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
diff --git a/res/layout/contact_edit.xml b/res/layout/contact_edit.xml
new file mode 100644
index 0000000..25dff3f
--- /dev/null
+++ b/res/layout/contact_edit.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_edit"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:layout_weight="1"
+ android:fillViewport="true"
+ >
+
+ <LinearLayout android:id="@+id/editors"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ />
+
+ </ScrollView>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ style="@android:style/ButtonBar"
+ >
+
+ <Button android:id="@+id/btn_done"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_done"
+ />
+
+ <Button android:id="@+id/btn_discard"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_doNotSave"
+ />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 692c413..83a0cf3 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -14,12 +14,29 @@
limitations under the License.
-->
<resources>
- <!-- The EditText for entries in the EditContactActivity -->
+ <!-- Dialogs in ContactDetailFragment -->
+ <item type="id" name="detail_dialog_confirm_delete" />
+ <item type="id" name="detail_dialog_confirm_readonly_delete" />
+ <item type="id" name="detail_dialog_confirm_multiple_delete" />
+ <item type="id" name="detail_dialog_confirm_readonly_hide" />
+
+ <!-- The EditText for entries in the ContactEditFragment -->
<item type="id" name="data"/>
<item type="id" name="header_phones"/>
<item type="id" name="dialog_sync_add"/>
<item type="id" name="dialog_import_export"/>
+ <!-- Dialogs in ContactEditFragment -->
+ <item type="id" name="edit_dialog_confirm_delete"/>
+ <item type="id" name="edit_dialog_confirm_readonly_delete"/>
+ <item type="id" name="edit_dialog_confirm_multiple_delete"/>
+ <item type="id" name="edit_dialog_confirm_readonly_hide"/>
+ <item type="id" name="edit_dialog_pick_photo"/>
+ <item type="id" name="edit_dialog_split"/>
+ <item type="id" name="edit_dialog_select_account"/>
+ <item type="id" name="edit_dialog_view_dialogs_id1"/>
+ <item type="id" name="edit_dialog_view_dialogs_id2"/>
+
<!-- For ImportVCardActivity -->
<item type="id" name="dialog_searching_vcard"/>
<item type="id" name="dialog_sdcard_not_found"/>
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index d413a25..a843141 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -18,13 +18,11 @@
import com.android.contacts.ContactsSearchManager;
import com.android.contacts.R;
-import com.android.contacts.util.DialogManager;
-import com.android.contacts.views.detail.ContactPresenter;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailFragment;
+import android.app.Activity;
import android.app.Dialog;
-import android.app.patterns.Loader;
-import android.app.patterns.LoaderActivity;
+import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
@@ -32,16 +30,12 @@
import android.view.Menu;
import android.view.MenuItem;
-public class ContactDetailActivity extends LoaderActivity<ContactLoader.Result> implements
- DialogManager.DialogShowingViewActivity {
- private static final int LOADER_DETAILS = 1;
- private ContactPresenter mCoupler;
- private DialogManager mDialogManager;
+public class ContactDetailActivity extends Activity {
+ private ContactDetailFragment mFragment;
private static final String TAG = "ContactDetailActivity";
- private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
- private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+ private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
@Override
public void onCreate(Bundle savedState) {
@@ -49,53 +43,18 @@
setContentView(R.layout.contact_detail);
- mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+ mFragment = new ContactDetailFragment(this, findViewById(R.id.contact_detail),
+ mCallbackHandler, getIntent().getData());
- mCoupler = new ContactPresenter(this, findViewById(R.id.contact_details));
- mCoupler.setController(new ContactPresenter.DefaultController(this));
- }
-
- @Override
- public void onInitializeLoaders() {
- startLoading(LOADER_DETAILS, null);
- }
-
- @Override
- protected ContactLoader onCreateLoader(int id, Bundle args) {
- switch (id) {
- case LOADER_DETAILS: {
- return new ContactLoader(this, getIntent().getData());
- }
- default: {
- Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
- }
- }
- return null;
- }
-
- @Override
- public void onLoadFinished(Loader 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");
- finish();
- return;
- }
- mCoupler.setData(data);
- break;
- default: {
- Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
- }
- }
+ openFragmentTransaction()
+ .add(mFragment, R.id.contact_detail)
+ .commit();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// TODO: This is too hardwired.
- if (mCoupler.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+ if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
return super.onCreateOptionsMenu(menu);
}
@@ -103,7 +62,7 @@
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// TODO: This is too hardwired.
- if (mCoupler.onPrepareOptionsMenu(menu)) return true;
+ if (mFragment.onPrepareOptionsMenu(menu)) return true;
return super.onPrepareOptionsMenu(menu);
}
@@ -111,24 +70,26 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// TODO: This is too hardwired.
- if (mCoupler.onOptionsItemSelected(item)) return true;
+ if (mFragment.onOptionsItemSelected(item)) return true;
return super.onOptionsItemSelected(item);
}
- public DialogManager getDialogManager() {
- return mDialogManager;
- }
-
@Override
protected Dialog onCreateDialog(int id, Bundle args) {
- return mDialogManager.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
public boolean onContextItemSelected(MenuItem item) {
// TODO: This is too hardwired.
- if (mCoupler.onContextItemSelected(item)) return true;
+ if (mFragment.onContextItemSelected(item)) return true;
return super.onContextItemSelected(item);
}
@@ -146,8 +107,22 @@
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO: This is too hardwired.
- if (mCoupler.onKeyDown(keyCode, event)) return true;
+ if (mFragment.onKeyDown(keyCode, event)) return true;
return super.onKeyDown(keyCode, event);
}
+
+ private class FragmentCallbackHandler implements ContactDetailFragment.Callbacks {
+ public void closeBecauseContactNotFound() {
+ finish();
+ }
+
+ public void editContact(Uri rawContactUri) {
+ startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+ }
+
+ public void itemClicked(Intent intent) {
+ startActivity(intent);
+ }
+ }
}
diff --git a/src/com/android/contacts/activities/ContactEditActivity.java b/src/com/android/contacts/activities/ContactEditActivity.java
new file mode 100644
index 0000000..cf1fbac
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditActivity.java
@@ -0,0 +1,105 @@
+/*
+ * 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.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;
+
+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);
+
+ 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 = new ContactEditFragment(
+ this, findViewById(R.id.contact_edit),
+ action, uri, mimeType, intentExtras,
+ mCallbackHandler);
+
+ openFragmentTransaction()
+ .add(mFragment, R.id.contact_edit)
+ .commit();
+ }
+
+ private class FragmentCallbackHandler implements ContactEditFragment.Callbacks {
+ public void closeAfterRevert() {
+ finish();
+ }
+
+ public void closeBecauseContactNotFound() {
+ 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;
+ }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
index c9d4635..617c855 100644
--- a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
+++ b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
@@ -56,7 +56,8 @@
}
ContactListAdapter adapter = mContactListFragment.getAdapter();
- adapter.moveToPosition(info.position);
+ int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+ adapter.moveToPosition(info.position - headerViewsCount);
// Setup the menu header
menu.setHeaderTitle(adapter.getContactDisplayName());
@@ -93,7 +94,8 @@
}
ContactListAdapter adapter = mContactListFragment.getAdapter();
- adapter.moveToPosition(info.position);
+ int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+ adapter.moveToPosition(info.position - headerViewsCount);
final Uri contactUri = adapter.getContactUri();
switch (item.getItemId()) {
diff --git a/src/com/android/contacts/views/detail/ContactPresenter.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
similarity index 90%
rename from src/com/android/contacts/views/detail/ContactPresenter.java
rename to src/com/android/contacts/views/detail/ContactDetailFragment.java
index d2dfda1..fb781a9 100644
--- a/src/com/android/contacts/views/detail/ContactPresenter.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -27,15 +27,15 @@
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Sources;
import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.util.DialogManager;
import com.android.contacts.util.Constants;
import com.android.contacts.util.DataStatus;
-import com.android.contacts.views.detail.ContactLoader.Result;
import com.android.internal.telephony.ITelephony;
import com.android.internal.widget.ContactHeaderWidget;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -89,36 +89,32 @@
import java.util.ArrayList;
-public class ContactPresenter implements OnCreateContextMenuListener,
- OnItemClickListener, DialogManager.DialogShowingView {
+public class ContactDetailFragment extends LoaderManagingFragment<ContactDetailLoader.Result>
+ implements OnCreateContextMenuListener, OnItemClickListener {
private static final String TAG = "ContactDetailsView";
private static final boolean SHOW_SEPARATORS = false;
- private static final String DIALOG_ID_KEY = "dialog_id";
- private static final int DIALOG_CONFIRM_DELETE = 1;
- private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
- private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
- private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
-
private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+ private static final int LOADER_DETAILS = 1;
+
private final Context mContext;
private final View mView;
+ private final LayoutInflater mInflater;
+ private final Uri mLookupUri;
+ private Callbacks mCallbacks;
- private Result mContactData;
- private Controller mCallbacks;
- private LayoutInflater mInflater;
+ private ContactDetailLoader.Result mContactData;
private ContactHeaderWidget mContactHeaderWidget;
private ListView mListView;
private boolean mShowSmsLinksForAllPhones;
private ViewAdapter mAdapter;
private Uri mPrimaryPhoneUri = null;
- private DialogManager mDialogManager = null;
private int mReadOnlySourcesCnt;
private int mWritableSourcesCnt;
private boolean mAllRestricted;
- private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+ private final ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mNumPhoneNumbers = 0;
/**
@@ -143,9 +139,12 @@
private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
- public ContactPresenter(Context context, View view) {
+ public ContactDetailFragment(Context context, View view, Callbacks callbacks, Uri lookupUri) {
+ super();
+ if (callbacks == null) throw new IllegalArgumentException("callbacks must be provided");
mContext = context;
mView = view;
+ mCallbacks = callbacks;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
@@ -175,12 +174,47 @@
//TODO Read this value from a preference
mShowSmsLinksForAllPhones = true;
+
+ mLookupUri = lookupUri;
}
- public void setData(Result data) {
- mContactData = data;
+ @Override
+ protected void onInitializeLoaders() {
+ startLoading(LOADER_DETAILS, null);
+ }
- bindData();
+ @Override
+ protected Loader<ContactDetailLoader.Result> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactDetailLoader(mContext, mLookupUri);
+ }
+ default: {
+ Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<ContactDetailLoader.Result> loader,
+ ContactDetailLoader.Result data) {
+ final int id = loader.getId();
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactDetailLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ mCallbacks.closeBecauseContactNotFound();
+ return;
+ }
+ mContactData = data;
+ bindData();
+ break;
+ default: {
+ Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+ }
+ }
}
private void bindData() {
@@ -412,45 +446,6 @@
}
}
- public interface Controller {
- public void onPrimaryClick(ViewEntry entry);
- public void onSecondaryClick(ViewEntry entry);
- }
-
- public static final class DefaultController implements Controller {
- private Context mContext;
-
- public DefaultController(Context context) {
- mContext = context;
- }
-
- public void onPrimaryClick(ViewEntry entry) {
- Intent intent = entry.intent;
- if (intent != null) {
- try {
- mContext.startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "No activity found for intent: " + intent);
- }
- }
- }
-
- public void onSecondaryClick(ViewEntry entry) {
- Intent intent = entry.secondaryIntent;
- if (intent != null) {
- try {
- mContext.startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "No activity found for intent: " + intent);
- }
- }
- }
- }
-
- public void setController(Controller callbacks) {
- mCallbacks = callbacks;
- }
-
/* package */ static String buildActionString(DataKind kind, ContentValues values,
boolean lowerCase, Context context) {
if (kind.actionHeader == null) {
@@ -607,7 +602,6 @@
}
final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
-
ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
super(context, sections, SHOW_SEPARATORS);
}
@@ -750,7 +744,9 @@
if (v == null) return;
final ViewEntry entry = (ViewEntry) v.getTag();
if (entry == null) return;
- mCallbacks.onSecondaryClick(entry);
+ final Intent intent = entry.secondaryIntent;
+ if (intent == null) return;
+ mCallbacks.itemClicked(intent);
}
}
@@ -774,10 +770,11 @@
switch (item.getItemId()) {
case R.id.menu_edit: {
if (mRawContactIds.size() > 0) {
- long rawContactIdToEdit = mRawContactIds.get(0);
- Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ final long rawContactIdToEdit = mRawContactIds.get(0);
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
rawContactIdToEdit);
- mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+ mCallbacks.editContact(rawContactUri);
+ //mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
return true;
} else {
// There is no rawContact to edit.
@@ -821,13 +818,13 @@
private void showDeleteConfirmationDialog() {
if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
- showDialog(DIALOG_CONFIRM_READONLY_DELETE);
+ getActivity().showDialog(R.id.detail_dialog_confirm_readonly_delete);
} else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
- showDialog(DIALOG_CONFIRM_READONLY_HIDE);
+ getActivity().showDialog(R.id.detail_dialog_confirm_readonly_hide);
} else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
- showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
+ getActivity().showDialog(R.id.detail_dialog_confirm_multiple_delete);
} else {
- showDialog(DIALOG_CONFIRM_DELETE);
+ getActivity().showDialog(R.id.detail_dialog_confirm_delete);
}
}
@@ -868,7 +865,9 @@
if (mCallbacks == null) return;
final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
if (entry == null) return;
- mCallbacks.onPrimaryClick(entry);
+ final Intent intent = entry.intent;
+ if (intent == null) return;
+ mCallbacks.itemClicked(intent);
}
private final DialogInterface.OnClickListener mDeleteListener =
@@ -878,11 +877,9 @@
}
};
- public Dialog createDialog(Bundle bundle) {
- if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
- int dialogId = bundle.getInt(DIALOG_ID_KEY);
- switch (dialogId) {
- case DIALOG_CONFIRM_DELETE:
+ public Dialog onCreateDialog(int id, Bundle bundle) {
+ 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)
@@ -891,7 +888,7 @@
.setPositiveButton(android.R.string.ok, mDeleteListener)
.setCancelable(false)
.create();
- case DIALOG_CONFIRM_READONLY_DELETE:
+ 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)
@@ -900,7 +897,7 @@
.setPositiveButton(android.R.string.ok, mDeleteListener)
.setCancelable(false)
.create();
- case DIALOG_CONFIRM_MULTIPLE_DELETE:
+ 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)
@@ -909,7 +906,7 @@
.setPositiveButton(android.R.string.ok, mDeleteListener)
.setCancelable(false)
.create();
- case DIALOG_CONFIRM_READONLY_HIDE: {
+ 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)
@@ -918,28 +915,10 @@
.create();
}
default:
- throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+ return null;
}
}
- /* package */ void showDialog(int bundleDialogId) {
- Bundle bundle = new Bundle();
- bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
- getDialogManager().showDialogInView(mView, bundle);
- }
-
- private DialogManager getDialogManager() {
- if (mDialogManager == null) {
- if (!(mContext instanceof DialogManager.DialogShowingViewActivity)) {
- throw new IllegalStateException(
- "View must be hosted in an Activity that implements " +
- "DialogManager.DialogShowingViewActivity");
- }
- mDialogManager = ((DialogManager.DialogShowingViewActivity)mContext).getDialogManager();
- }
- return mDialogManager;
- }
-
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_ITEM_MAKE_DEFAULT: {
@@ -1020,4 +999,21 @@
return false;
}
+
+ public static interface Callbacks {
+ /**
+ * Contact was not found, so somehow close this fragment.
+ */
+ public void closeBecauseContactNotFound();
+
+ /**
+ * User decided to go to Edit-Mode
+ */
+ public void editContact(Uri rawContactUri);
+
+ /**
+ * User clicked a single item (e.g. mail)
+ */
+ public void itemClicked(Intent intent);
+ }
}
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactDetailLoader.java
similarity index 88%
rename from src/com/android/contacts/views/detail/ContactLoader.java
rename to src/com/android/contacts/views/detail/ContactDetailLoader.java
index bc885df..147ec3c 100644
--- a/src/com/android/contacts/views/detail/ContactLoader.java
+++ b/src/com/android/contacts/views/detail/ContactDetailLoader.java
@@ -43,15 +43,14 @@
/**
* Loads a single Contact and all it constituent RawContacts.
*/
-public class ContactLoader extends Loader<ContactLoader.Result> {
- private boolean mIsSynchronous;
+public class ContactDetailLoader extends Loader<ContactDetailLoader.Result> {
+ private static final String TAG = "ContactLoader";
+
private Uri mLookupUri;
private Result mContact;
private ForceLoadContentObserver mObserver;
private boolean mDestroyed;
- private static final String TAG = "ContactLoader";
-
public interface Callbacks {
public void onContactLoaded(Result contact);
}
@@ -65,6 +64,13 @@
*/
public static final Result NOT_FOUND = new Result();
+ /**
+ * Singleton instance that represents an error, e.g. because of an invalid Uri
+ * TODO: We should come up with something nicer here. Maybe use an Either type so
+ * that we can capture the Exception?
+ */
+ public static final Result ERROR = new Result();
+
private final Uri mLookupUri;
private final String mLookupKey;
private final Uri mUri;
@@ -225,32 +231,35 @@
final static int CONTACT_STATUS_LABEL = 11;
}
-
- public final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+ private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
@Override
protected Result doInBackground(Void... args) {
- final ContentResolver resolver = getContext().getContentResolver();
- Uri uriCurrentFormat = convertLegacyIfNecessary(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.
- // We might have went through a sync where Ids changed
- final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
- result = loadContactHeaderData(resolver, freshLookupUri);
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+ Result result = loadContactHeaderData(resolver, uriCurrentFormat);
if (result == Result.NOT_FOUND) {
- // Still not found. We now believe this contact really does not exist
- Log.e(TAG, "invalid contact uri: " + mLookupUri);
- return Result.NOT_FOUND;
+ // No record found. Try to lookup up a new record with the same lookupKey.
+ // We might have went through a sync where Ids changed
+ final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
+ result = loadContactHeaderData(resolver, freshLookupUri);
+ if (result == Result.NOT_FOUND) {
+ // Still not found. We now believe this contact really does not exist
+ Log.e(TAG, "invalid contact uri: " + mLookupUri);
+ return Result.NOT_FOUND;
+ }
}
+
+ // These queries could be run in parallel (we did this until froyo). But unless
+ // we actually have two database connections there is no performance gain
+ loadSocial(resolver, result);
+ loadRawContacts(resolver, result);
+
+ return result;
+ } catch (Exception e) {
+ return Result.ERROR;
}
-
- // These queries could be run in parallel (we did this until froyo). But unless
- // we actually have two database connections there is no performance gain
- loadSocial(resolver, result);
- loadRawContacts(resolver, result);
-
- return result;
}
/**
@@ -436,14 +445,17 @@
mObserver = new ForceLoadContentObserver();
}
Log.i(TAG, "Registering content observer for " + mLookupUri);
- getContext().getContentResolver().registerContentObserver(
- mLookupUri, true, mObserver);
+
+ if (result != Result.ERROR && result != Result.NOT_FOUND) {
+ getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+ mObserver);
+ }
deliverResult(result);
}
}
}
- public ContactLoader(Context context, Uri lookupUri) {
+ public ContactDetailLoader(Context context, Uri lookupUri) {
super(context);
mLookupUri = lookupUri;
}
@@ -459,11 +471,7 @@
@Override
public void forceLoad() {
- LoadContactTask task = new LoadContactTask();
- if (mIsSynchronous) {
- task.onPostExecute(task.doInBackground((Void[])null));
- return;
- }
+ final LoadContactTask task = new LoadContactTask();
task.execute((Void[])null);
}
@@ -480,9 +488,4 @@
mContact = null;
mDestroyed = true;
}
-
-
- public void setSynchronous(boolean value) {
- mIsSynchronous = value;
- }
}
diff --git a/src/com/android/contacts/views/edit/ContactEditFragment.java b/src/com/android/contacts/views/edit/ContactEditFragment.java
new file mode 100644
index 0000000..4666c44
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditFragment.java
@@ -0,0 +1,1160 @@
+/*
+ * 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.ProgressDialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
+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.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.DialogInterface.OnDismissListener;
+import android.database.Cursor;
+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.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;
+
+public class ContactEditFragment
+ extends LoaderManagingFragment<ContactEditLoader.Result> {
+ private static final String TAG = "ContactEditFragment";
+
+ private static final int LOADER_DATA = 1;
+
+ /** The launch code when picking a photo and the raw data is returned */
+ private static final int PHOTO_PICKED_WITH_DATA = 3021;
+
+ /** The launch code when a contact to join with is returned */
+ private static final int REQUEST_JOIN_CONTACT = 3022;
+
+ /** The launch code when taking a picture */
+ private static final int CAMERA_WITH_DATA = 3023;
+
+ 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";
+
+ private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
+
+ 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 static final int ICON_SIZE = 96;
+
+ private static final File PHOTO_DIR = new File(
+ Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+ private final Context mContext;
+ private final View mView;
+ private final LayoutInflater mInflater;
+ private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
+ private final String mAction;
+ private final Uri mUri;
+ private final String mMimeType;
+ private final Bundle mIntentExtras;
+ private final Callbacks mCallbacks;
+
+ private File mCurrentPhotoFile;
+
+ private String mQuerySelection;
+ private String[] mQuerySelectionArgs;
+
+ private long mContactIdForJoin;
+
+ private LinearLayout mContent;
+ private EntitySet mState;
+
+ private ViewIdGenerator mViewIdGenerator;
+
+ // TODO: We might not need to pass Context and View manually here as the framework should
+ // take care of this...?
+ public ContactEditFragment(Context context, View view, String action, Uri uri,
+ String mimeType, Bundle intentExtras, Callbacks callbacks) {
+ super();
+
+ if (callbacks == null) throw new IllegalArgumentException("callbacks must be given");
+
+ mContext = context;
+ mView = view;
+ mAction = action;
+ mUri = uri;
+ mMimeType = mimeType;
+ mIntentExtras = intentExtras;
+ mCallbacks = callbacks;
+ mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ // Build editor and listen for photo requests
+ 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) {
+ mCallbacks.closeAfterRevert();
+ }
+ });
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ // TODO: Currently savedState is always null (framework issue). Test once this is fixed
+ super.onCreate(savedState);
+
+ // Handle initial actions only when existing state missing
+ final boolean hasIncomingState =
+ savedState != null && savedState.containsKey(KEY_EDIT_STATE);
+
+ if (!hasIncomingState) {
+ if (Intent.ACTION_EDIT.equals(mAction)) {
+ // Read initial state from database
+ mCallbacks.setTitleTo(R.string.editContact_title_edit);
+ startLoading(LOADER_DATA, null);
+ } else if (Intent.ACTION_INSERT.equals(mAction)) {
+ mCallbacks.setTitleTo(R.string.editContact_title_insert);
+
+ // 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();
+ } else throw new IllegalArgumentException("Unknown Action String " + mAction +
+ ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
+ }
+
+ if (savedState == null) {
+ // If savedState is non-null, onRestoreInstanceState() will restore the generator.
+ mViewIdGenerator = new ViewIdGenerator();
+ }
+ }
+
+ @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");
+ 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 void onClick(View v) {
+ // TODO forward to controller?
+ }
+
+ /**
+ * 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()) {
+ // TODO: pass this back to Activity
+ // finish();
+ }
+ }
+ };
+
+ 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 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();
+ }
+
+ /**
+ * 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();
+ // TODO: Do this again (Controller?)
+ //startActivityForResult(intent, 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);
+
+ // TODO: Start camera
+ //startActivityForResult(intent, 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;
+ }
+
+ /**
+ * 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 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;
+ }
+ mCallbacks.closeAfterSaving(resultCode, resultIntent);
+ break;
+// TODO: Other save modes
+
+// case SAVE_MODE_SPLIT:
+// if (success) {
+// Intent intent = new Intent();
+// intent.setData(contactLookupUri);
+// setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent);
+// }
+// finish();
+// 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)
+ */
+ public void showJoinAggregateActivity(Uri contactLookupUri) {
+ if (contactLookupUri == null) {
+ return;
+ }
+
+ mContactIdForJoin = ContentUris.parseId(contactLookupUri);
+ Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+ intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
+ getActivity().startActivityForResult(intent, REQUEST_JOIN_CONTACT);
+ }
+
+ 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();
+
+ /**
+ * User has tapped Revert, close the fragment now.
+ */
+ void closeAfterRevert();
+
+ /**
+ * 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) {
+ // TODO: Still needed?
+// 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: Delete
+// Sources sources = Sources.getInstance(mContext);
+// // Mark all raw contacts for deletion
+// for (EntityDelta delta : mState) {
+// delta.markDeleted();
+// }
+// // Save the deletes
+// doSaveAction(SAVE_MODE_DEFAULT);
+// finish();
+ }
+ }
+
+ // 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();
+ }
+
+ // TODO: How do we get rid of the dialog
+// dismissDialog(progress);
+
+ // 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);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ // Read modifications from instance
+ mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
+ mRawContactIdRequestingPhoto = savedInstanceState.getLong(
+ KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
+ mViewIdGenerator = savedInstanceState.getParcelable(KEY_VIEW_ID_GENERATOR);
+ String fileName = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+ if (fileName != null) {
+ mCurrentPhotoFile = new File(fileName);
+ }
+ mQuerySelection = savedInstanceState.getString(KEY_QUERY_SELECTION);
+ mQuerySelectionArgs = savedInstanceState.getStringArray(KEY_QUERY_SELECTION_ARGS);
+ mContactIdForJoin = savedInstanceState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+
+ bindEditors();
+
+ super.onRestoreInstanceState(savedInstanceState);
+ }
+}
diff --git a/src/com/android/contacts/views/edit/ContactEditLoader.java b/src/com/android/contacts/views/edit/ContactEditLoader.java
new file mode 100644
index 0000000..3506c59
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditLoader.java
@@ -0,0 +1,177 @@
+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.app.patterns.Loader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+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/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
index 6676a2b..77cfb1f 100644
--- a/tests/src/com/android/contacts/ContactDetailLoaderTest.java
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -18,7 +18,7 @@
import com.android.contacts.tests.mocks.ContactsMockContext;
import com.android.contacts.tests.mocks.MockContentProvider;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailLoader;
import android.app.patterns.Loader;
import android.app.patterns.Loader.OnLoadCompleteListener;
@@ -59,7 +59,7 @@
protected void onPostExecute(Void result) {}
};
}
-
+
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -144,42 +144,24 @@
return result;
}
- private ContactLoader.Result assertLoadContact(Uri uri) {
- final ContactLoader loader = new ContactLoader(mMockContext, uri);
+ private ContactDetailLoader.Result assertLoadContact(Uri uri) {
+ final ContactDetailLoader loader = new ContactDetailLoader(mMockContext, uri);
return getLoaderResultSynchronously(loader);
}
- @Suppress // The code under test is incorrect
public void testNullUri() {
- IllegalArgumentException e =
- assertThrows(IllegalArgumentException.class, new Runnable() {
- public void run() {
- assertLoadContact(null);
- }
- });
- assertEquals(e.getMessage(), "uri must not be null");
+ ContactDetailLoader.Result result = assertLoadContact(null);
+ assertEquals(ContactDetailLoader.Result.ERROR, result);
}
- @Suppress // The code under test is incorrect
public void testEmptyUri() {
- IllegalArgumentException e =
- assertThrows(IllegalArgumentException.class, new Runnable() {
- public void run() {
- assertLoadContact(Uri.EMPTY);
- }
- });
- assertEquals(e.getMessage(), "uri format is unknown");
+ ContactDetailLoader.Result result = assertLoadContact(Uri.EMPTY);
+ assertEquals(ContactDetailLoader.Result.ERROR, result);
}
- @Suppress // The code under test is incorrect
public void testInvalidUri() {
- IllegalArgumentException e =
- assertThrows(IllegalArgumentException.class, new Runnable() {
- public void run() {
- assertLoadContact(Uri.parse("content://wtf"));
- }
- });
- assertEquals(e.getMessage(), "uri format is unknown");
+ ContactDetailLoader.Result result = assertLoadContact(Uri.parse("content://wtf"));
+ assertEquals(ContactDetailLoader.Result.ERROR, result);
}
public void testLoadContactWithContactIdUri() {
@@ -201,7 +183,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(baseUri);
+ ContactDetailLoader.Result contact = assertLoadContact(baseUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -235,7 +217,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(legacyUri);
+ ContactDetailLoader.Result contact = assertLoadContact(legacyUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -266,7 +248,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(lookupNoIdUri);
+ ContactDetailLoader.Result contact = assertLoadContact(lookupNoIdUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -296,7 +278,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(lookupUri);
+ ContactDetailLoader.Result contact = assertLoadContact(lookupUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -338,7 +320,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+ ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -381,7 +363,7 @@
queries.fetchSocial(dataUri, contactId);
queries.fetchRawContacts(contactId, dataId, rawContactId);
- ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+ ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
assertEquals(contactId, contact.getId());
assertEquals(rawContactId, contact.getNameRawContactId());
@@ -422,9 +404,9 @@
queries.fetchHeaderDataNoResult(wrongBaseUri);
queries.fetchLookupAndIdNoResult(lookupWithWrongIdUri);
- ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+ ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
- assertEquals(ContactLoader.Result.NOT_FOUND, contact);
+ assertEquals(ContactDetailLoader.Result.NOT_FOUND, contact);
mContactsProvider.verify();
}
diff --git a/tests/src/com/android/contacts/ContactDetailTest.java b/tests/src/com/android/contacts/ContactDetailTest.java
index 926a353..6e93bd9 100644
--- a/tests/src/com/android/contacts/ContactDetailTest.java
+++ b/tests/src/com/android/contacts/ContactDetailTest.java
@@ -3,7 +3,7 @@
import com.android.contacts.activities.ContactDetailActivity;
import com.android.contacts.tests.mocks.ContactsMockContext;
import com.android.contacts.tests.mocks.MockContentProvider;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailLoader;
import android.content.ContentUris;
import android.content.Intent;