// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Contacts;
import android.util.Log;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.HoneycombMR1Util;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.SdkLevel;
/**
* Component enabling a user to select a contact.
*
* @author sharon@google.com (Sharon Perl)
* @author markf@google.com (Mark Friedman)
* @author: Yifan(Evan) Li (for contact Uri)
*/
@DesignerComponent(version = YaVersion.CONTACTPICKER_COMPONENT_VERSION,
description = "A button that, when clicked on, displays a list of " +
"the contacts to choose among. After the user has made a " +
"selection, the following properties will be set to information about " +
"the chosen contact: <ul>\n" +
"<li> <code>ContactName</code>: the contact's name </li>\n " +
"<li> <code>EmailAddress</code>: the contact's primary email address </li>\n " +
"<li> <code>ContactUri</code>: the contact's URI on the device </li>\n"+
"<li> <code>EmailAddressList</code>: a list of the contact's email addresses </li>\n " +
"<li> <code>PhoneNumber</code>: the contact's primary phone number (on Later Android Verisons)</li>\n " +
"<li> <code>PhoneNumberList</code>: a list of the contact's phone numbers (on Later Android Versions)</li>\n " +
"<li> <code>Picture</code>: the name of the file containing the contact's " +
"image, which can be used as a <code>Picture</code> property value for " +
"the <code>Image</code> or <code>ImageSprite</code> component.</li></ul>\n" +
"</p><p>Other properties affect the appearance of the button " +
"(<code>TextAlignment</code>, <code>BackgroundColor</code>, etc.) and " +
"whether it can be clicked on (<code>Enabled</code>).\n</p>" +
"<p>The ContactPicker component might not work on all phones. For " +
"example, on Android systems before system 3.0, it cannot pick phone " +
"numbers, and the list of email addresses will contain only one email.",
category = ComponentCategory.SOCIAL)
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.READ_CONTACTS")
public class ContactPicker extends Picker implements ActivityResultListener {
private static String[] CONTACT_PROJECTION;
private static String[] DATA_PROJECTION;
private static final String[] PROJECTION = {
Contacts.PeopleColumns.NAME,
Contacts.People.PRIMARY_EMAIL_ID,
};
private static final int NAME_INDEX = 0;
private static final int EMAIL_INDEX = 1;
private static final int PHONE_INDEX = 2;
protected final Activity activityContext;
private final Uri intentUri;
protected String contactName;
protected String emailAddress;
protected String contactUri;
protected String contactPictureUri;
protected String phoneNumber;
protected List emailAddressList;
protected List phoneNumberList;
/**
* Create a new ContactPicker component.
*
* @param container the parent container.
*/
public ContactPicker(ComponentContainer container) {
this(container, Contacts.People.CONTENT_URI);
}
protected ContactPicker(ComponentContainer container, Uri intentUri) {
super(container);
activityContext = container.$context();
if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1 && intentUri.equals(Contacts.People.CONTENT_URI)) {
this.intentUri = HoneycombMR1Util.getContentUri();
} else if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1 && intentUri.equals(Contacts.Phones.CONTENT_URI)) {
this.intentUri = HoneycombMR1Util.getPhoneContentUri();
} else {
this.intentUri = intentUri;
}
}
/**
* Picture URI for this contact, which can be
* used to retrieve the contact's photo and other fields.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public String Picture() {
return ensureNotNull(contactPictureUri);
}
/**
* Name property getter method.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public String ContactName() {
return ensureNotNull(contactName);
}
/**
* EmailAddress property getter method.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public String EmailAddress() {
// Note(halabelson): I am commenting out this test. Android provider.Contacts was
// deprecated in Donut, but email picking still seems to work on newer versions of the SDK.
// If there's a phone where it does not work, we'll get the error at PuntContactSelection
// Note that there is still a general problem with contact picking on Motoblur.
// if (SdkLevel.getLevel() > SdkLevel.LEVEL_DONUT) {
// container.$form().dispatchErrorOccurredEvent(this, "EmailAddress",
// ErrorMessages.ERROR_FUNCTIONALITY_NOT_SUPPORTED_CONTACT_EMAIL);
// }
return ensureNotNull(emailAddress);
}
/**
* "URI that specifies the location of the contact on the device.",
*/
@SimpleProperty(description = "URI that specifies the location of the contact on the device.",
category = PropertyCategory.BEHAVIOR)
public String ContactUri() {
return ensureNotNull(contactUri);
}
/**
* EmailAddressList property getter method.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public List EmailAddressList() {
return ensureNotNull(emailAddressList);
}
/**
* PhoneNumber property getter method.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public String PhoneNumber() {
return ensureNotNull(phoneNumber);
}
/**
* PhoneNumberList property getter method.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR)
public List PhoneNumberList() {
return ensureNotNull(phoneNumberList);
}
/**
* return nothing, just call another activity which is view contact
*/
@SimpleFunction(description = "view a contact via its URI")
public void ViewContact(String uri) {
if(contactUri != null){
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse(uri));
if (intent.resolveActivity(this.activityContext.getPackageManager()) != null) {
this.activityContext.startActivity(intent);
}
}
}
@Override
protected Intent getIntent() {
return new Intent(Intent.ACTION_PICK, intentUri);
}
/**
* Callback method to get the result returned by the contact picker activity
*
* @param requestCode a code identifying the request.
* @param resultCode a code specifying success or failure of the activity
* @param data the returned data, in this case an Intent whose data field
* contains the contact's content provider Uri.
*/
@Override
public void resultReturned(int requestCode, int resultCode, Intent data) {
if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK) {
Log.i("ContactPicker", "received intent is " + data);
Uri receivedContactUri = data.getData();
// Pre- and post-Honeycomb need different URIs.
String desiredContactUri = "";
if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1) {
desiredContactUri = "//com.android.contacts/contact";
} else {
desiredContactUri = "//contacts/people";
}
if (checkContactUri(receivedContactUri, desiredContactUri)) {
Cursor contactCursor = null;
Cursor dataCursor = null;
try {
if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1) {
CONTACT_PROJECTION = HoneycombMR1Util.getContactProjection();
contactCursor = activityContext.getContentResolver().query(receivedContactUri,
CONTACT_PROJECTION, null, null, null);
String id = postHoneycombGetContactNameAndPicture(contactCursor);
DATA_PROJECTION = HoneycombMR1Util.getDataProjection();
dataCursor = HoneycombMR1Util.getDataCursor(id, activityContext, DATA_PROJECTION);
postHoneycombGetContactEmailAndPhone(dataCursor);
//explicit set TextContactUri
contactUri = receivedContactUri.toString();
} else {
contactCursor = activityContext.getContentResolver().query(receivedContactUri,
PROJECTION, null, null, null);
preHoneycombGetContactInfo(contactCursor, receivedContactUri);
}
Log.i("ContactPicker",
"Contact name = " + contactName + ", email address = " + emailAddress + ",contact Uri = " + contactUri +
", phone number = " + phoneNumber + ", contactPhotoUri = " + contactPictureUri);
} catch (Exception e) {
// There was an exception in trying to extract the cursor from the activity context.
// It's bad form to catch an arbitrary exception, but if there is an error here
// it's unclear what's going on.
Log.i("ContactPicker", "checkContactUri failed: D");
puntContactSelection(ErrorMessages.ERROR_PHONE_UNSUPPORTED_CONTACT_PICKER);
} finally {
if (contactCursor != null) {
contactCursor.close();
}
if (dataCursor != null) {
dataCursor.close();
}
}
} // ends if (checkContactUri ...
AfterPicking();
} // ends if (requestCode ...
}
/**
* For versions before Honeycomb, we get all the contact info from the same table.
*/
public void preHoneycombGetContactInfo(Cursor contactCursor, Uri theContactUri) {
if (contactCursor.moveToFirst()) {
contactName = guardCursorGetString(contactCursor, NAME_INDEX);
String emailId = guardCursorGetString(contactCursor, EMAIL_INDEX);
emailAddress = getEmailAddress(emailId);
contactUri = theContactUri.toString();
contactPictureUri = theContactUri.toString();
emailAddressList = emailAddress.equals("") ? new ArrayList() : Arrays.asList(emailAddress);
}
}
/**
* Assigns contactName and contactPictureUri for Honeycomb and up.
* Returns id for getting emailAddress and phoneNumber.
*/
public String postHoneycombGetContactNameAndPicture(Cursor contactCursor) {
String id = "";
if (contactCursor.moveToFirst()) {
final int ID_INDEX = HoneycombMR1Util.getIdIndex(contactCursor);
final int NAME_INDEX = HoneycombMR1Util.getNameIndex(contactCursor);
final int THUMBNAIL_INDEX = HoneycombMR1Util.getThumbnailIndex(contactCursor);
final int PHOTO_INDEX = HoneycombMR1Util.getPhotoIndex(contactCursor);
id = guardCursorGetString(contactCursor, ID_INDEX);
contactName = guardCursorGetString(contactCursor, NAME_INDEX);
contactPictureUri = guardCursorGetString(contactCursor, THUMBNAIL_INDEX);
Log.i("ContactPicker", "photo_uri=" + guardCursorGetString(contactCursor, PHOTO_INDEX));
}
return id;
}
/**
* Assigns emailAddress, phoneNumber, emailAddressList, and phoneNumberList
* for Honeycomb and up.
*/
public void postHoneycombGetContactEmailAndPhone(Cursor dataCursor) {
phoneNumber = "";
emailAddress = "";
List<String> phoneListToStore = new ArrayList<String>();
List<String> emailListToStore = new ArrayList<String>();
if (dataCursor.moveToFirst()) {
final int PHONE_INDEX = HoneycombMR1Util.getPhoneIndex(dataCursor);
final int EMAIL_INDEX = HoneycombMR1Util.getEmailIndex(dataCursor);
final int MIME_INDEX = HoneycombMR1Util.getMimeIndex(dataCursor);
String phoneType = HoneycombMR1Util.getPhoneType();
String emailType = HoneycombMR1Util.getEmailType();
while (!dataCursor.isAfterLast()) {
String type = guardCursorGetString(dataCursor, MIME_INDEX);
if (type.contains(phoneType)) {
phoneListToStore.add(guardCursorGetString(dataCursor, PHONE_INDEX));
} else if (type.contains(emailType)) {
emailListToStore.add(guardCursorGetString(dataCursor, EMAIL_INDEX));
} else {
Log.i("ContactPicker", "Type mismatch: " + type +
" not " + phoneType +
" or " + emailType);
}
dataCursor.moveToNext();
}
}
if (!phoneListToStore.isEmpty()) {
phoneNumber = phoneListToStore.get(0);
}
if (!emailListToStore.isEmpty()) {
emailAddress = emailListToStore.get(0);
}
phoneNumberList = phoneListToStore;
emailAddressList = emailListToStore;
}
// Check that the contact URI has the right form to permit the information to be
// extracted and try to show a meaningful error notice to the end user of the app.
// Sadly, different phones can produce different kinds of URIs. You
// can also get a different Uri depending on whether or not the user
// does a search to get the contact, versus just picking it. For example,
// Motorola Global phones produce an intent whose data part is null.
// Or using search on Nexus phones will produce a contact URI of the form
// content://com.android.contacts/contact, whereas doing direct selection
// produces a Uri have a specific required pattern that is
// passed in as an argument.
// TODO(halabelson): Create a better set of tests and/or generalize the extraction
// methods to permit more URIs.
// This should be done in conjunction with updating the way we handle contacts.
protected boolean checkContactUri(Uri suspectUri, String requiredPattern) {
Log.i("ContactPicker", "contactUri is " + suspectUri);
if (suspectUri == null || (!("content".equals(suspectUri.getScheme())))) {
Log.i("ContactPicker", "checkContactUri failed: A");
puntContactSelection(
ErrorMessages.ERROR_PHONE_UNSUPPORTED_CONTACT_PICKER);
return false;
}
String UriSpecific = suspectUri.getSchemeSpecificPart();
if (!UriSpecific.startsWith(requiredPattern)) {
Log.i("ContactPicker", "checkContactUri failed: C");
Log.i("ContactPicker", suspectUri.getPath());
puntContactSelection(ErrorMessages.ERROR_PHONE_UNSUPPORTED_CONTACT_PICKER);
return false;
} else {
return true;
}
}
// set the (supposedly) extracted properties to the empty string and
// report an error
protected void puntContactSelection(int errorNumber) {
contactName = "";
emailAddress = "";
contactPictureUri = "";
container.$form().dispatchErrorOccurredEvent(this, "", errorNumber);
}
/**
* Email address getter for pre-Honeycomb.
*/
protected String getEmailAddress(String emailId) {
int id;
try {
id = Integer.parseInt(emailId);
} catch (NumberFormatException e) {
return "";
}
String data = "";
String where = "contact_methods._id = " + id;
String[] projection = {
Contacts.ContactMethods.DATA
};
Cursor cursor = activityContext.getContentResolver().query(
Contacts.ContactMethods.CONTENT_EMAIL_URI,
projection, where, null, null);
try {
if (cursor.moveToFirst()) {
data = guardCursorGetString(cursor, 0);
}
} finally {
cursor.close();
}
// this extra check for null might be redundant, but we given that there are mysterious errors
// on some phones, we'll leave it in just to be extra careful
return ensureNotNull(data);
}
// If the selection returns null, this should be passed back as a
// an empty string to prevent errors if the app tries to convert this
// to a string. In some cases, getString can also throw an exception, for example,
// in selecting the name for a contact where there is no name.
// We also call ensureNotNull in the property selectors for ContactName, etc.
// This would appear to be redundant, but in testing, there have been some mysterious
// error conditions on some phones that permit nulls to sneak through from guardCursonGetString,
// so we'll do the extra check.
protected String guardCursorGetString(Cursor cursor, int index) {
String result;
try {
result = cursor.getString(index);
} catch (Exception e) {
// It's bad practice to catch a general exception, but unfortunately,
// the exception thrown is implementation dependent, according to the
// Android documentation.
result = "";
}
return ensureNotNull(result);
}
protected String ensureNotNull(String value) {
if (value == null) {
return "";
} else {
return value;
}
}
protected List ensureNotNull(List value) {
if (value == null) {
return new ArrayList();
} else {
return value;
}
}
}