blob: fb781a930859afd50af9cbe0823e1e8cf71d4803 [file] [log] [blame]
Daniel Lehmann4cd94412010-04-08 16:44:36 -07001/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.contacts.views.detail;
18
19import com.android.contacts.Collapser;
20import com.android.contacts.ContactEntryAdapter;
21import com.android.contacts.ContactOptionsActivity;
22import com.android.contacts.ContactPresenceIconUtil;
23import com.android.contacts.ContactsUtils;
24import com.android.contacts.R;
25import com.android.contacts.TypePrecedence;
26import com.android.contacts.Collapser.Collapsible;
27import com.android.contacts.model.ContactsSource;
28import com.android.contacts.model.Sources;
29import com.android.contacts.model.ContactsSource.DataKind;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070030import com.android.contacts.util.Constants;
31import com.android.contacts.util.DataStatus;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070032import com.android.internal.telephony.ITelephony;
33import com.android.internal.widget.ContactHeaderWidget;
34
35import android.app.AlertDialog;
36import android.app.Dialog;
Daniel Lehmann18f104f2010-05-07 15:41:11 -070037import android.app.patterns.Loader;
38import android.app.patterns.LoaderManagingFragment;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070039import android.content.ActivityNotFoundException;
40import android.content.ContentUris;
41import android.content.ContentValues;
42import android.content.Context;
43import android.content.DialogInterface;
44import android.content.Entity;
45import android.content.Intent;
46import android.content.Entity.NamedContentValues;
47import android.content.res.Resources;
48import android.graphics.drawable.Drawable;
49import android.net.ParseException;
50import android.net.Uri;
51import android.net.WebAddress;
52import android.os.Bundle;
53import android.os.RemoteException;
54import android.os.ServiceManager;
55import android.provider.ContactsContract.CommonDataKinds;
56import android.provider.ContactsContract.Contacts;
57import android.provider.ContactsContract.Data;
58import android.provider.ContactsContract.DisplayNameSources;
59import android.provider.ContactsContract.RawContacts;
60import android.provider.ContactsContract.StatusUpdates;
61import android.provider.ContactsContract.CommonDataKinds.Email;
62import android.provider.ContactsContract.CommonDataKinds.Im;
63import android.provider.ContactsContract.CommonDataKinds.Nickname;
64import android.provider.ContactsContract.CommonDataKinds.Note;
65import android.provider.ContactsContract.CommonDataKinds.Organization;
66import android.provider.ContactsContract.CommonDataKinds.Phone;
67import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
68import android.provider.ContactsContract.CommonDataKinds.Website;
69import android.telephony.PhoneNumberUtils;
70import android.text.TextUtils;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070071import android.util.Log;
72import android.view.ContextMenu;
73import android.view.KeyEvent;
74import android.view.LayoutInflater;
75import android.view.Menu;
76import android.view.MenuInflater;
77import android.view.MenuItem;
78import android.view.View;
79import android.view.ViewGroup;
80import android.view.ContextMenu.ContextMenuInfo;
Daniel Lehmannc2687c32010-04-19 18:20:44 -070081import android.view.View.OnClickListener;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070082import android.view.View.OnCreateContextMenuListener;
83import android.widget.AdapterView;
84import android.widget.ImageView;
Daniel Lehmann4cd94412010-04-08 16:44:36 -070085import android.widget.ListView;
86import android.widget.TextView;
87import android.widget.Toast;
88import android.widget.AdapterView.OnItemClickListener;
89
90import java.util.ArrayList;
91
Daniel Lehmann18f104f2010-05-07 15:41:11 -070092public class ContactDetailFragment extends LoaderManagingFragment<ContactDetailLoader.Result>
93 implements OnCreateContextMenuListener, OnItemClickListener {
Daniel Lehmann4cd94412010-04-08 16:44:36 -070094 private static final String TAG = "ContactDetailsView";
95 private static final boolean SHOW_SEPARATORS = false;
96
Daniel Lehmann4cd94412010-04-08 16:44:36 -070097 private static final int MENU_ITEM_MAKE_DEFAULT = 3;
98
Daniel Lehmann18f104f2010-05-07 15:41:11 -070099 private static final int LOADER_DETAILS = 1;
100
Daniel Lehmannc2687c32010-04-19 18:20:44 -0700101 private final Context mContext;
102 private final View mView;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700103 private final LayoutInflater mInflater;
104 private final Uri mLookupUri;
105 private Callbacks mCallbacks;
Daniel Lehmannc2687c32010-04-19 18:20:44 -0700106
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700107 private ContactDetailLoader.Result mContactData;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700108 private ContactHeaderWidget mContactHeaderWidget;
109 private ListView mListView;
110 private boolean mShowSmsLinksForAllPhones;
111 private ViewAdapter mAdapter;
112 private Uri mPrimaryPhoneUri = null;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700113
114 private int mReadOnlySourcesCnt;
115 private int mWritableSourcesCnt;
116 private boolean mAllRestricted;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700117 private final ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700118 private int mNumPhoneNumbers = 0;
119
120 /**
121 * The view shown if the detail list is empty.
122 * We set this to the list view when first bind the adapter, so that it won't be shown while
123 * we're loading data.
124 */
125 private View mEmptyView;
126
127 /**
128 * A list of distinct contact IDs included in the current contact.
129 */
130 private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
131 private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
132 private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
133 private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
134 private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
135 private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
136 private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
137 private ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
138 private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
139 private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
140 private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
141
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700142 public ContactDetailFragment(Context context, View view, Callbacks callbacks, Uri lookupUri) {
143 super();
144 if (callbacks == null) throw new IllegalArgumentException("callbacks must be provided");
Daniel Lehmannc2687c32010-04-19 18:20:44 -0700145 mContext = context;
146 mView = view;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700147 mCallbacks = callbacks;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700148
Daniel Lehmannc2687c32010-04-19 18:20:44 -0700149 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
150 mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
151 mContactHeaderWidget.showStar(true);
152 mContactHeaderWidget.setExcludeMimes(new String[] {
153 Contacts.CONTENT_ITEM_TYPE
154 });
155
156 mListView = (ListView) view.findViewById(android.R.id.list);
157 mListView.setOnCreateContextMenuListener(this);
158 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
159 mListView.setOnItemClickListener(this);
160 // Don't set it to mListView yet. We do so later when we bind the adapter.
161 mEmptyView = view.findViewById(android.R.id.empty);
162
163 // Build the list of sections. The order they're added to mSections dictates the
164 // order they are displayed in the list.
165 mSections.add(mPhoneEntries);
166 mSections.add(mSmsEntries);
167 mSections.add(mEmailEntries);
168 mSections.add(mImEntries);
169 mSections.add(mPostalEntries);
170 mSections.add(mNicknameEntries);
171 mSections.add(mOrganizationEntries);
172 mSections.add(mGroupEntries);
173 mSections.add(mOtherEntries);
174
175 //TODO Read this value from a preference
176 mShowSmsLinksForAllPhones = true;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700177
178 mLookupUri = lookupUri;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700179 }
180
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700181 @Override
182 protected void onInitializeLoaders() {
183 startLoading(LOADER_DETAILS, null);
184 }
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700185
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700186 @Override
187 protected Loader<ContactDetailLoader.Result> onCreateLoader(int id, Bundle args) {
188 switch (id) {
189 case LOADER_DETAILS: {
190 return new ContactDetailLoader(mContext, mLookupUri);
191 }
192 default: {
193 Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
194 }
195 }
196 return null;
197 }
198
199 @Override
200 protected void onLoadFinished(Loader<ContactDetailLoader.Result> loader,
201 ContactDetailLoader.Result data) {
202 final int id = loader.getId();
203 switch (id) {
204 case LOADER_DETAILS:
205 if (data == ContactDetailLoader.Result.NOT_FOUND) {
206 // Item has been deleted
207 Log.i(TAG, "No contact found. Closing activity");
208 mCallbacks.closeBecauseContactNotFound();
209 return;
210 }
211 mContactData = data;
212 bindData();
213 break;
214 default: {
215 Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
216 }
217 }
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700218 }
219
220 private void bindData() {
Daniel Lehmannd3e0cdb2010-04-19 13:45:53 -0700221 // Set the header
222 mContactHeaderWidget.setContactUri(mContactData.getLookupUri());
223 mContactHeaderWidget.setDisplayName(mContactData.getDisplayName(),
224 mContactData.getPhoneticName());
225 mContactHeaderWidget.setPhotoId(mContactData.getPhotoId(), mContactData.getLookupUri());
226 mContactHeaderWidget.setStared(mContactData.getStarred());
227 mContactHeaderWidget.setPresence(mContactData.getPresence());
228 mContactHeaderWidget.setStatus(
229 mContactData.getStatus(), mContactData.getStatusTimestamp(),
230 mContactData.getStatusLabel(), mContactData.getStatusResPackage());
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700231
232 // Build up the contact entries
233 buildEntries();
234
235 // Collapse similar data items in select sections.
236 Collapser.collapseList(mPhoneEntries);
237 Collapser.collapseList(mSmsEntries);
238 Collapser.collapseList(mEmailEntries);
239 Collapser.collapseList(mPostalEntries);
240 Collapser.collapseList(mImEntries);
241
242 if (mAdapter == null) {
243 mAdapter = new ViewAdapter(mContext, mSections);
244 mListView.setAdapter(mAdapter);
245 } else {
246 mAdapter.setSections(mSections, SHOW_SEPARATORS);
247 }
248 mListView.setEmptyView(mEmptyView);
249 }
250
251 /**
252 * Build up the entries to display on the screen.
253 */
254 private final void buildEntries() {
255 // Clear out the old entries
256 final int numSections = mSections.size();
257 for (int i = 0; i < numSections; i++) {
258 mSections.get(i).clear();
259 }
260
261 mRawContactIds.clear();
262
263 mReadOnlySourcesCnt = 0;
264 mWritableSourcesCnt = 0;
265 mAllRestricted = true;
266 mPrimaryPhoneUri = null;
267 mNumPhoneNumbers = 0;
268
269 mWritableRawContactIds.clear();
270
271 final Sources sources = Sources.getInstance(mContext);
272
273 // Build up method entries
274 if (mContactData == null) {
275 return;
276 }
277
278 for (Entity entity: mContactData.getEntities()) {
279 final ContentValues entValues = entity.getEntityValues();
280 final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
281 final long rawContactId = entValues.getAsLong(RawContacts._ID);
282
283 // Mark when this contact has any unrestricted components
284 final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
285 if (!isRestricted) mAllRestricted = false;
286
287 if (!mRawContactIds.contains(rawContactId)) {
288 mRawContactIds.add(rawContactId);
289 }
290 ContactsSource contactsSource = sources.getInflatedSource(accountType,
291 ContactsSource.LEVEL_SUMMARY);
292 if (contactsSource != null && contactsSource.readOnly) {
293 mReadOnlySourcesCnt += 1;
294 } else {
295 mWritableSourcesCnt += 1;
296 mWritableRawContactIds.add(rawContactId);
297 }
298
299
300 for (NamedContentValues subValue : entity.getSubValues()) {
301 final ContentValues entryValues = subValue.values;
302 entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
303
304 final long dataId = entryValues.getAsLong(Data._ID);
305 final String mimeType = entryValues.getAsString(Data.MIMETYPE);
306 if (mimeType == null) continue;
307
308 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
309 ContactsSource.LEVEL_MIMETYPES);
310 if (kind == null) continue;
311
312 final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind,
313 rawContactId, dataId, entryValues);
314
315 final boolean hasData = !TextUtils.isEmpty(entry.data);
316 final boolean isSuperPrimary = entryValues.getAsInteger(
317 Data.IS_SUPER_PRIMARY) != 0;
318
319 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
320 // Build phone entries
321 mNumPhoneNumbers++;
322
323 entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
324 Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
325 entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
326 Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
327
328 // Remember super-primary phone
329 if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
330
331 entry.isPrimary = isSuperPrimary;
332 mPhoneEntries.add(entry);
333
334 if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
335 || mShowSmsLinksForAllPhones) {
336 // Add an SMS entry
337 if (kind.iconAltRes > 0) {
338 entry.secondaryActionIcon = kind.iconAltRes;
339 }
340 }
341 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
342 // Build email entries
343 entry.intent = new Intent(Intent.ACTION_SENDTO,
344 Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
345 entry.isPrimary = isSuperPrimary;
346 mEmailEntries.add(entry);
347
348 // When Email rows have status, create additional Im row
349 final DataStatus status = mContactData.getStatuses().get(entry.id);
350 if (status != null) {
351 final String imMime = Im.CONTENT_ITEM_TYPE;
352 final DataKind imKind = sources.getKindOrFallback(accountType,
353 imMime, mContext, ContactsSource.LEVEL_MIMETYPES);
354 final ViewEntry imEntry = ViewEntry.fromValues(mContext,
355 imMime, imKind, rawContactId, dataId, entryValues);
356 imEntry.intent = ContactsUtils.buildImIntent(entryValues);
357 imEntry.applyStatus(status, false);
358 mImEntries.add(imEntry);
359 }
360 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
361 // Build postal entries
362 entry.maxLines = 4;
363 entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
364 mPostalEntries.add(entry);
365 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
366 // Build IM entries
367 entry.intent = ContactsUtils.buildImIntent(entryValues);
368 if (TextUtils.isEmpty(entry.label)) {
369 entry.label = mContext.getString(R.string.chat).toLowerCase();
370 }
371
372 // Apply presence and status details when available
373 final DataStatus status = mContactData.getStatuses().get(entry.id);
374 if (status != null) {
375 entry.applyStatus(status, false);
376 }
377 mImEntries.add(entry);
378 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
379 (hasData || !TextUtils.isEmpty(entry.label))) {
380 // Build organization entries
381 final boolean isNameRawContact =
382 (mContactData.getNameRawContactId() == rawContactId);
383
384 final boolean duplicatesTitle =
385 isNameRawContact
386 && mContactData.getDisplayNameSource()
387 == DisplayNameSources.ORGANIZATION
388 && (!hasData || TextUtils.isEmpty(entry.label));
389
390 if (!duplicatesTitle) {
391 entry.uri = null;
392
393 if (TextUtils.isEmpty(entry.label)) {
394 entry.label = entry.data;
395 entry.data = "";
396 }
397
398 mOrganizationEntries.add(entry);
399 }
400 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
401 // Build nickname entries
402 final boolean isNameRawContact =
403 (mContactData.getNameRawContactId() == rawContactId);
404
405 final boolean duplicatesTitle =
406 isNameRawContact
407 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
408
409 if (!duplicatesTitle) {
410 entry.uri = null;
411 mNicknameEntries.add(entry);
412 }
413 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
414 // Build note entries
415 entry.uri = null;
416 entry.maxLines = 100;
417 mOtherEntries.add(entry);
418 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
419 // Build note entries
420 entry.uri = null;
421 entry.maxLines = 10;
422 try {
423 WebAddress webAddress = new WebAddress(entry.data);
424 entry.intent = new Intent(Intent.ACTION_VIEW,
425 Uri.parse(webAddress.toString()));
426 } catch (ParseException e) {
427 Log.e(TAG, "Couldn't parse website: " + entry.data);
428 }
429 mOtherEntries.add(entry);
430 } else {
431 // Handle showing custom rows
432 entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
433
434 // Use social summary when requested by external source
435 final DataStatus status = mContactData.getStatuses().get(entry.id);
436 final boolean hasSocial = kind.actionBodySocial && status != null;
437 if (hasSocial) {
438 entry.applyStatus(status, true);
439 }
440
441 if (hasSocial || hasData) {
442 mOtherEntries.add(entry);
443 }
444 }
445 }
446 }
447 }
448
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700449 /* package */ static String buildActionString(DataKind kind, ContentValues values,
450 boolean lowerCase, Context context) {
451 if (kind.actionHeader == null) {
452 return null;
453 }
454 CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
455 if (actionHeader == null) {
456 return null;
457 }
458 return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
459 }
460
461 /* package */ static String buildDataString(DataKind kind, ContentValues values,
462 Context context) {
463 if (kind.actionBody == null) {
464 return null;
465 }
466 CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
467 return actionBody == null ? null : actionBody.toString();
468 }
469
470 /**
471 * A basic structure with the data for a contact entry in the list.
472 */
473 static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
474 public Context context = null;
475 public String resPackageName = null;
476 public int actionIcon = -1;
477 public boolean isPrimary = false;
478 public int secondaryActionIcon = -1;
479 public Intent intent;
480 public Intent secondaryIntent = null;
481 public int maxLabelLines = 1;
482 public ArrayList<Long> ids = new ArrayList<Long>();
483 public int collapseCount = 0;
484
485 public int presence = -1;
486
487 public CharSequence footerLine = null;
488
489 private ViewEntry() {
490 }
491
492 /**
493 * Build new {@link ViewEntry} and populate from the given values.
494 */
495 public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
496 long rawContactId, long dataId, ContentValues values) {
497 final ViewEntry entry = new ViewEntry();
498 entry.context = context;
499 entry.contactId = rawContactId;
500 entry.id = dataId;
501 entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
502 entry.mimetype = mimeType;
503 entry.label = buildActionString(kind, values, false, context);
504 entry.data = buildDataString(kind, values, context);
505
506 if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
507 entry.type = values.getAsInteger(kind.typeColumn);
508 }
509 if (kind.iconRes > 0) {
510 entry.resPackageName = kind.resPackageName;
511 entry.actionIcon = kind.iconRes;
512 }
513
514 return entry;
515 }
516
517 /**
518 * Apply given {@link DataStatus} values over this {@link ViewEntry}
519 *
520 * @param fillData When true, the given status replaces {@link #data}
521 * and {@link #footerLine}. Otherwise only {@link #presence}
522 * is updated.
523 */
524 public ViewEntry applyStatus(DataStatus status, boolean fillData) {
525 presence = status.getPresence();
526 if (fillData && status.isValid()) {
527 this.data = status.getStatus().toString();
528 this.footerLine = status.getTimestampLabel(context);
529 }
530
531 return this;
532 }
533
534 public boolean collapseWith(ViewEntry entry) {
535 // assert equal collapse keys
536 if (!shouldCollapseWith(entry)) {
537 return false;
538 }
539
540 // Choose the label associated with the highest type precedence.
541 if (TypePrecedence.getTypePrecedence(mimetype, type)
542 > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
543 type = entry.type;
544 label = entry.label;
545 }
546
547 // Choose the max of the maxLines and maxLabelLines values.
548 maxLines = Math.max(maxLines, entry.maxLines);
549 maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
550
551 // Choose the presence with the highest precedence.
552 if (StatusUpdates.getPresencePrecedence(presence)
553 < StatusUpdates.getPresencePrecedence(entry.presence)) {
554 presence = entry.presence;
555 }
556
557 // If any of the collapsed entries are primary make the whole thing primary.
558 isPrimary = entry.isPrimary ? true : isPrimary;
559
560 // uri, and contactdId, shouldn't make a difference. Just keep the original.
561
562 // Keep track of all the ids that have been collapsed with this one.
563 ids.add(entry.id);
564 collapseCount++;
565 return true;
566 }
567
568 public boolean shouldCollapseWith(ViewEntry entry) {
569 if (entry == null) {
570 return false;
571 }
572
573 if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
574 entry.data)) {
575 return false;
576 }
577
578 if (!TextUtils.equals(mimetype, entry.mimetype)
579 || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
580 || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
581 || actionIcon != entry.actionIcon) {
582 return false;
583 }
584
585 return true;
586 }
587 }
588
589 /** Cache of the children views of a row */
590 private static class ViewCache {
591 public TextView label;
592 public TextView data;
593 public TextView footer;
594 public ImageView actionIcon;
595 public ImageView presenceIcon;
596 public ImageView primaryIcon;
597 public ImageView secondaryActionButton;
598 public View secondaryActionDivider;
599
600 // Need to keep track of this too
601 public ViewEntry entry;
602 }
603
604 final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700605 ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
606 super(context, sections, SHOW_SEPARATORS);
607 }
608
609 @Override
610 public View getView(int position, View convertView, ViewGroup parent) {
611 final ViewEntry entry = getEntry(mSections, position, false);
612 final View v;
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700613 final ViewCache viewCache;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700614
615 // Check to see if we can reuse convertView
616 if (convertView != null) {
617 v = convertView;
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700618 viewCache = (ViewCache) v.getTag();
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700619 } else {
620 // Create a new view if needed
621 v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
622
623 // Cache the children
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700624 viewCache = new ViewCache();
625 viewCache.label = (TextView) v.findViewById(android.R.id.text1);
626 viewCache.data = (TextView) v.findViewById(android.R.id.text2);
627 viewCache.footer = (TextView) v.findViewById(R.id.footer);
628 viewCache.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
629 viewCache.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
630 viewCache.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
631 viewCache.secondaryActionButton = (ImageView) v.findViewById(
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700632 R.id.secondary_action_button);
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700633 viewCache.secondaryActionButton.setOnClickListener(this);
634 viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
635 v.setTag(viewCache);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700636 }
637
638 // Update the entry in the view cache
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700639 viewCache.entry = entry;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700640
641 // Bind the data to the view
642 bindView(v, entry);
643 return v;
644 }
645
646 @Override
647 protected View newView(int position, ViewGroup parent) {
648 // getView() handles this
649 throw new UnsupportedOperationException();
650 }
651
652 @Override
653 protected void bindView(View view, ViewEntry entry) {
654 final Resources resources = mContext.getResources();
655 ViewCache views = (ViewCache) view.getTag();
656
657 // Set the label
658 TextView label = views.label;
659 setMaxLines(label, entry.maxLabelLines);
660 label.setText(entry.label);
661
662 // Set the data
663 TextView data = views.data;
664 if (data != null) {
665 if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
666 || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
667 data.setText(PhoneNumberUtils.formatNumber(entry.data));
668 } else {
669 data.setText(entry.data);
670 }
671 setMaxLines(data, entry.maxLines);
672 }
673
674 // Set the footer
675 if (!TextUtils.isEmpty(entry.footerLine)) {
676 views.footer.setText(entry.footerLine);
677 views.footer.setVisibility(View.VISIBLE);
678 } else {
679 views.footer.setVisibility(View.GONE);
680 }
681
682 // Set the primary icon
683 views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
684
685 // Set the action icon
686 ImageView action = views.actionIcon;
687 if (entry.actionIcon != -1) {
688 Drawable actionIcon;
689 if (entry.resPackageName != null) {
690 // Load external resources through PackageManager
691 actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
692 entry.actionIcon, null);
693 } else {
694 actionIcon = resources.getDrawable(entry.actionIcon);
695 }
696 action.setImageDrawable(actionIcon);
697 action.setVisibility(View.VISIBLE);
698 } else {
699 // Things should still line up as if there was an icon, so make it invisible
700 action.setVisibility(View.INVISIBLE);
701 }
702
703 // Set the presence icon
704 Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
705 mContext, entry.presence);
706 ImageView presenceIconView = views.presenceIcon;
707 if (presenceIcon != null) {
708 presenceIconView.setImageDrawable(presenceIcon);
709 presenceIconView.setVisibility(View.VISIBLE);
710 } else {
711 presenceIconView.setVisibility(View.GONE);
712 }
713
714 // Set the secondary action button
715 ImageView secondaryActionView = views.secondaryActionButton;
716 Drawable secondaryActionIcon = null;
717 if (entry.secondaryActionIcon != -1) {
718 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
719 }
720 if (entry.secondaryIntent != null && secondaryActionIcon != null) {
721 secondaryActionView.setImageDrawable(secondaryActionIcon);
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700722 secondaryActionView.setTag(entry);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700723 secondaryActionView.setVisibility(View.VISIBLE);
724 views.secondaryActionDivider.setVisibility(View.VISIBLE);
725 } else {
726 secondaryActionView.setVisibility(View.GONE);
727 views.secondaryActionDivider.setVisibility(View.GONE);
728 }
729 }
730
731 private void setMaxLines(TextView textView, int maxLines) {
732 if (maxLines == 1) {
733 textView.setSingleLine(true);
734 textView.setEllipsize(TextUtils.TruncateAt.END);
735 } else {
736 textView.setSingleLine(false);
737 textView.setMaxLines(maxLines);
738 textView.setEllipsize(null);
739 }
740 }
741
742 public void onClick(View v) {
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700743 if (mCallbacks == null) return;
744 if (v == null) return;
745 final ViewEntry entry = (ViewEntry) v.getTag();
746 if (entry == null) return;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700747 final Intent intent = entry.secondaryIntent;
748 if (intent == null) return;
749 mCallbacks.itemClicked(intent);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700750 }
751 }
752
753 public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
754 inflater.inflate(R.menu.view, menu);
755 return true;
756 }
757
758 public boolean onPrepareOptionsMenu(Menu menu) {
759 // Only allow edit when we have at least one raw_contact id
760 final boolean hasRawContact = (mRawContactIds.size() > 0);
761 menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
762
763 // Only allow share when unrestricted contacts available
764 menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
765
766 return true;
767 }
768
769 public boolean onOptionsItemSelected(MenuItem item) {
770 switch (item.getItemId()) {
771 case R.id.menu_edit: {
772 if (mRawContactIds.size() > 0) {
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700773 final long rawContactIdToEdit = mRawContactIds.get(0);
774 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700775 rawContactIdToEdit);
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700776 mCallbacks.editContact(rawContactUri);
777 //mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700778 return true;
779 } else {
780 // There is no rawContact to edit.
781 return false;
782 }
783 }
784 case R.id.menu_delete: {
785 showDeleteConfirmationDialog();
786 return true;
787 }
788 case R.id.menu_options: {
789 final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
790 intent.setData(mContactData.getLookupUri());
791 mContext.startActivity(intent);
792 return true;
793 }
794 case R.id.menu_share: {
795 if (mAllRestricted) return false;
796
797 final String lookupKey = mContactData.getLookupKey();
798 final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
799
800 final Intent intent = new Intent(Intent.ACTION_SEND);
801 intent.setType(Contacts.CONTENT_VCARD_TYPE);
802 intent.putExtra(Intent.EXTRA_STREAM, shareUri);
803
804 // Launch chooser to share contact via
805 final CharSequence chooseTitle = mContext.getText(R.string.share_via);
806 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
807
808 try {
809 mContext.startActivity(chooseIntent);
810 } catch (ActivityNotFoundException ex) {
811 Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
812 }
813 return true;
814 }
815 }
816 return false;
817 }
818
819 private void showDeleteConfirmationDialog() {
820 if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700821 getActivity().showDialog(R.id.detail_dialog_confirm_readonly_delete);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700822 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700823 getActivity().showDialog(R.id.detail_dialog_confirm_readonly_hide);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700824 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700825 getActivity().showDialog(R.id.detail_dialog_confirm_multiple_delete);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700826 } else {
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700827 getActivity().showDialog(R.id.detail_dialog_confirm_delete);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700828 }
829 }
830
831 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
832 AdapterView.AdapterContextMenuInfo info;
833 try {
834 info = (AdapterView.AdapterContextMenuInfo) menuInfo;
835 } catch (ClassCastException e) {
836 Log.e(TAG, "bad menuInfo", e);
837 return;
838 }
839
840 // This can be null sometimes, don't crash...
841 if (info == null) {
842 Log.e(TAG, "bad menuInfo");
843 return;
844 }
845
846 ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
847 menu.setHeaderTitle(R.string.contactOptionsTitle);
848 if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
849 menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
850 menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
851 if (!entry.isPrimary) {
852 menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
853 }
854 } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
855 menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
856 if (!entry.isPrimary) {
857 menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
858 }
859 } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
860 menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
861 }
862 }
863
864 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Daniel Lehmannc3a00082010-04-13 13:53:54 -0700865 if (mCallbacks == null) return;
866 final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
867 if (entry == null) return;
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700868 final Intent intent = entry.intent;
869 if (intent == null) return;
870 mCallbacks.itemClicked(intent);
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700871 }
872
873 private final DialogInterface.OnClickListener mDeleteListener =
874 new DialogInterface.OnClickListener() {
875 public void onClick(DialogInterface dialog, int which) {
876 mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
877 }
878 };
879
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700880 public Dialog onCreateDialog(int id, Bundle bundle) {
881 switch (id) {
882 case R.id.detail_dialog_confirm_delete:
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700883 return new AlertDialog.Builder(mContext)
884 .setTitle(R.string.deleteConfirmation_title)
885 .setIcon(android.R.drawable.ic_dialog_alert)
886 .setMessage(R.string.deleteConfirmation)
887 .setNegativeButton(android.R.string.cancel, null)
888 .setPositiveButton(android.R.string.ok, mDeleteListener)
889 .setCancelable(false)
890 .create();
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700891 case R.id.detail_dialog_confirm_readonly_delete:
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700892 return new AlertDialog.Builder(mContext)
893 .setTitle(R.string.deleteConfirmation_title)
894 .setIcon(android.R.drawable.ic_dialog_alert)
895 .setMessage(R.string.readOnlyContactDeleteConfirmation)
896 .setNegativeButton(android.R.string.cancel, null)
897 .setPositiveButton(android.R.string.ok, mDeleteListener)
898 .setCancelable(false)
899 .create();
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700900 case R.id.detail_dialog_confirm_multiple_delete:
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700901 return new AlertDialog.Builder(mContext)
902 .setTitle(R.string.deleteConfirmation_title)
903 .setIcon(android.R.drawable.ic_dialog_alert)
904 .setMessage(R.string.multipleContactDeleteConfirmation)
905 .setNegativeButton(android.R.string.cancel, null)
906 .setPositiveButton(android.R.string.ok, mDeleteListener)
907 .setCancelable(false)
908 .create();
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700909 case R.id.detail_dialog_confirm_readonly_hide: {
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700910 return new AlertDialog.Builder(mContext)
911 .setTitle(R.string.deleteConfirmation_title)
912 .setIcon(android.R.drawable.ic_dialog_alert)
913 .setMessage(R.string.readOnlyContactWarning)
914 .setPositiveButton(android.R.string.ok, mDeleteListener)
915 .create();
916 }
917 default:
Daniel Lehmann18f104f2010-05-07 15:41:11 -0700918 return null;
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700919 }
920 }
921
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700922 public boolean onContextItemSelected(MenuItem item) {
923 switch (item.getItemId()) {
924 case MENU_ITEM_MAKE_DEFAULT: {
925 if (makeItemDefault(item)) {
926 return true;
927 }
928 break;
929 }
930 }
931
932 return false;
933 }
934
935 private boolean makeItemDefault(MenuItem item) {
936 ViewEntry entry = getViewEntryForMenuItem(item);
937 if (entry == null) {
938 return false;
939 }
940
941 // Update the primary values in the data record.
942 ContentValues values = new ContentValues(1);
943 values.put(Data.IS_SUPER_PRIMARY, 1);
944
945 mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
946 values, null, null);
947 return true;
948 }
949
950 private ViewEntry getViewEntryForMenuItem(MenuItem item) {
951 AdapterView.AdapterContextMenuInfo info;
952 try {
953 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
954 } catch (ClassCastException e) {
955 Log.e(TAG, "bad menuInfo", e);
956 return null;
957 }
958
959 return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
960 }
961
Daniel Lehmann4cd94412010-04-08 16:44:36 -0700962 public boolean onKeyDown(int keyCode, KeyEvent event) {
963 switch (keyCode) {
964 case KeyEvent.KEYCODE_CALL: {
965 try {
966 ITelephony phone = ITelephony.Stub.asInterface(
967 ServiceManager.checkService("phone"));
968 if (phone != null && !phone.isIdle()) {
969 // Skip out and let the key be handled at a higher level
970 break;
971 }
972 } catch (RemoteException re) {
973 // Fall through and try to call the contact
974 }
975
976 int index = mListView.getSelectedItemPosition();
977 if (index != -1) {
978 final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
979 if (entry != null &&
980 entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
981 mContext.startActivity(entry.intent);
982 return true;
983 }
984 } else if (mPrimaryPhoneUri != null) {
985 // There isn't anything selected, call the default number
986 final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
987 mPrimaryPhoneUri);
988 mContext.startActivity(intent);
989 return true;
990 }
991 return false;
992 }
993
994 case KeyEvent.KEYCODE_DEL: {
995 showDeleteConfirmationDialog();
996 return true;
997 }
998 }
999
Daniel Lehmannc2687c32010-04-19 18:20:44 -07001000 return false;
Daniel Lehmann4cd94412010-04-08 16:44:36 -07001001 }
Daniel Lehmann18f104f2010-05-07 15:41:11 -07001002
1003 public static interface Callbacks {
1004 /**
1005 * Contact was not found, so somehow close this fragment.
1006 */
1007 public void closeBecauseContactNotFound();
1008
1009 /**
1010 * User decided to go to Edit-Mode
1011 */
1012 public void editContact(Uri rawContactUri);
1013
1014 /**
1015 * User clicked a single item (e.g. mail)
1016 */
1017 public void itemClicked(Intent intent);
1018 }
Daniel Lehmann4cd94412010-04-08 16:44:36 -07001019}