Merge "Remove the call button area if we cannot call."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 61f0960..98a864a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -600,6 +600,13 @@
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/BackgroundOnly">
<intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED" />
+ <data android:mimeType="text/x-vcard" />
+ <data android:mimeType="text/x-vCard" />
+ <data android:mimeType="text/vcard" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="text/directory" />
<data android:mimeType="text/vcard" />
diff --git a/res/drawable-hdpi/dial_num_1_wht.png b/res/drawable-hdpi/dial_num_1_wht.png
index 5381f03..0c79720 100644
--- a/res/drawable-hdpi/dial_num_1_wht.png
+++ b/res/drawable-hdpi/dial_num_1_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_2_wht.png b/res/drawable-hdpi/dial_num_2_wht.png
index 903f036..ab90531 100644
--- a/res/drawable-hdpi/dial_num_2_wht.png
+++ b/res/drawable-hdpi/dial_num_2_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_3_wht.png b/res/drawable-hdpi/dial_num_3_wht.png
index e840f86..956cba9 100644
--- a/res/drawable-hdpi/dial_num_3_wht.png
+++ b/res/drawable-hdpi/dial_num_3_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_4_wht.png b/res/drawable-hdpi/dial_num_4_wht.png
index 45a238a..34e157c 100644
--- a/res/drawable-hdpi/dial_num_4_wht.png
+++ b/res/drawable-hdpi/dial_num_4_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_5_wht.png b/res/drawable-hdpi/dial_num_5_wht.png
index e563242..4a3560a 100644
--- a/res/drawable-hdpi/dial_num_5_wht.png
+++ b/res/drawable-hdpi/dial_num_5_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_6_wht.png b/res/drawable-hdpi/dial_num_6_wht.png
index e916968..a60420b 100644
--- a/res/drawable-hdpi/dial_num_6_wht.png
+++ b/res/drawable-hdpi/dial_num_6_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_7_wht.png b/res/drawable-hdpi/dial_num_7_wht.png
index 090366a..95e4cff 100644
--- a/res/drawable-hdpi/dial_num_7_wht.png
+++ b/res/drawable-hdpi/dial_num_7_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_8_wht.png b/res/drawable-hdpi/dial_num_8_wht.png
index f898450..4b17084 100644
--- a/res/drawable-hdpi/dial_num_8_wht.png
+++ b/res/drawable-hdpi/dial_num_8_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_9_wht.png b/res/drawable-hdpi/dial_num_9_wht.png
index 999457c..f772901 100644
--- a/res/drawable-hdpi/dial_num_9_wht.png
+++ b/res/drawable-hdpi/dial_num_9_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_pound_wht.png b/res/drawable-hdpi/dial_num_pound_wht.png
index 44aa276..1d7f55a 100644
--- a/res/drawable-hdpi/dial_num_pound_wht.png
+++ b/res/drawable-hdpi/dial_num_pound_wht.png
Binary files differ
diff --git a/res/drawable-hdpi/dial_num_star_wht.png b/res/drawable-hdpi/dial_num_star_wht.png
index edd6e06..2add63b 100644
--- a/res/drawable-hdpi/dial_num_star_wht.png
+++ b/res/drawable-hdpi/dial_num_star_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_1_wht.png b/res/drawable-mdpi/dial_num_1_wht.png
index fe79a16..ff8f125 100644
--- a/res/drawable-mdpi/dial_num_1_wht.png
+++ b/res/drawable-mdpi/dial_num_1_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_2_wht.png b/res/drawable-mdpi/dial_num_2_wht.png
index 759ed42..041bafb 100644
--- a/res/drawable-mdpi/dial_num_2_wht.png
+++ b/res/drawable-mdpi/dial_num_2_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_3_wht.png b/res/drawable-mdpi/dial_num_3_wht.png
index 5cddb1d..b91b4f5 100644
--- a/res/drawable-mdpi/dial_num_3_wht.png
+++ b/res/drawable-mdpi/dial_num_3_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_4_wht.png b/res/drawable-mdpi/dial_num_4_wht.png
index 10878ec..912b4cb 100644
--- a/res/drawable-mdpi/dial_num_4_wht.png
+++ b/res/drawable-mdpi/dial_num_4_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_5_wht.png b/res/drawable-mdpi/dial_num_5_wht.png
index 514744b..efd385f 100644
--- a/res/drawable-mdpi/dial_num_5_wht.png
+++ b/res/drawable-mdpi/dial_num_5_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_6_wht.png b/res/drawable-mdpi/dial_num_6_wht.png
index 8877c64..c0f47c5 100644
--- a/res/drawable-mdpi/dial_num_6_wht.png
+++ b/res/drawable-mdpi/dial_num_6_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_7_wht.png b/res/drawable-mdpi/dial_num_7_wht.png
index 0e76d7d..5644f2b 100644
--- a/res/drawable-mdpi/dial_num_7_wht.png
+++ b/res/drawable-mdpi/dial_num_7_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_8_wht.png b/res/drawable-mdpi/dial_num_8_wht.png
index 62ea5fd..d0c517d 100644
--- a/res/drawable-mdpi/dial_num_8_wht.png
+++ b/res/drawable-mdpi/dial_num_8_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_9_wht.png b/res/drawable-mdpi/dial_num_9_wht.png
index 53194a4..fb443ec 100644
--- a/res/drawable-mdpi/dial_num_9_wht.png
+++ b/res/drawable-mdpi/dial_num_9_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_pound_wht.png b/res/drawable-mdpi/dial_num_pound_wht.png
index 9dfd878..11751ec 100644
--- a/res/drawable-mdpi/dial_num_pound_wht.png
+++ b/res/drawable-mdpi/dial_num_pound_wht.png
Binary files differ
diff --git a/res/drawable-mdpi/dial_num_star_wht.png b/res/drawable-mdpi/dial_num_star_wht.png
index cbb21da..61b24c1 100644
--- a/res/drawable-mdpi/dial_num_star_wht.png
+++ b/res/drawable-mdpi/dial_num_star_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_1_wht.png b/res/drawable-xhdpi/dial_num_1_wht.png
index 28295ee..5a54bfd 100644
--- a/res/drawable-xhdpi/dial_num_1_wht.png
+++ b/res/drawable-xhdpi/dial_num_1_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_2_wht.png b/res/drawable-xhdpi/dial_num_2_wht.png
index ecc8568..3407d79 100644
--- a/res/drawable-xhdpi/dial_num_2_wht.png
+++ b/res/drawable-xhdpi/dial_num_2_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_3_wht.png b/res/drawable-xhdpi/dial_num_3_wht.png
index 1872936..dd16bbb 100644
--- a/res/drawable-xhdpi/dial_num_3_wht.png
+++ b/res/drawable-xhdpi/dial_num_3_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_4_wht.png b/res/drawable-xhdpi/dial_num_4_wht.png
index cde2e4c..98f8773 100644
--- a/res/drawable-xhdpi/dial_num_4_wht.png
+++ b/res/drawable-xhdpi/dial_num_4_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_5_wht.png b/res/drawable-xhdpi/dial_num_5_wht.png
index 0b94669..12a92bf 100644
--- a/res/drawable-xhdpi/dial_num_5_wht.png
+++ b/res/drawable-xhdpi/dial_num_5_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_6_wht.png b/res/drawable-xhdpi/dial_num_6_wht.png
index aff27dd..39c3eda 100644
--- a/res/drawable-xhdpi/dial_num_6_wht.png
+++ b/res/drawable-xhdpi/dial_num_6_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_7_wht.png b/res/drawable-xhdpi/dial_num_7_wht.png
index 77da19d..5e3a0b0 100644
--- a/res/drawable-xhdpi/dial_num_7_wht.png
+++ b/res/drawable-xhdpi/dial_num_7_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_8_wht.png b/res/drawable-xhdpi/dial_num_8_wht.png
index e450e62..d68142d 100644
--- a/res/drawable-xhdpi/dial_num_8_wht.png
+++ b/res/drawable-xhdpi/dial_num_8_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_9_wht.png b/res/drawable-xhdpi/dial_num_9_wht.png
index 0c993e5..b34bc1d 100644
--- a/res/drawable-xhdpi/dial_num_9_wht.png
+++ b/res/drawable-xhdpi/dial_num_9_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_pound_wht.png b/res/drawable-xhdpi/dial_num_pound_wht.png
index 3be13ae..a4ead0a 100644
--- a/res/drawable-xhdpi/dial_num_pound_wht.png
+++ b/res/drawable-xhdpi/dial_num_pound_wht.png
Binary files differ
diff --git a/res/drawable-xhdpi/dial_num_star_wht.png b/res/drawable-xhdpi/dial_num_star_wht.png
index 9e699ab..ba0a787 100644
--- a/res/drawable-xhdpi/dial_num_star_wht.png
+++ b/res/drawable-xhdpi/dial_num_star_wht.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ab1f86f..54ed931 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1500,7 +1500,7 @@
<string name="activity_title_contacts_filter">Contacts to display</string>
<!-- Menu item for the settings activity [CHAR LIMIT=64] -->
- <string name="menu_settings">Settings</string>
+ <string name="menu_settings">Display Options</string>
<!-- The preference section title for contact display options [CHAR LIMIT=128] -->
<string name="preference_displayOptions">Display options</string>
@@ -1659,4 +1659,7 @@
<!-- Hint text in the group name box in the edit group view. [CHAR LIMIT=20]-->
<string name="group_name_hint">Group\'s Name</string>
+
+ <!-- The "file name" displayed for vCards received directly via NFC [CHAR LIMIT=16] -->
+ <string name="nfc_vcard_file_name">Contact received over NFC</string>
</resources>
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 07fa8f1..6d031e9 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -1221,6 +1221,8 @@
final MenuItem searchMenu = menu.findItem(R.id.menu_search);
final MenuItem addContactMenu = menu.findItem(R.id.menu_add_contact);
+ final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter);
+
MenuItem addGroupMenu = menu.findItem(R.id.menu_add_group);
if (addGroupMenu == null) {
addGroupMenu = menu.findItem(R.id.menu_custom_add_group);
@@ -1255,6 +1257,10 @@
searchMenu.setVisible(!mActionBarAdapter.isSearchMode());
}
+ if (contactsFilterMenu != null) {
+ contactsFilterMenu.setVisible(!mActionBarAdapter.isSearchMode());
+ }
+
MenuItem settings = menu.findItem(R.id.menu_settings);
if (settings != null) {
settings.setVisible(!ContactsPreferenceActivity.isEmpty(this));
diff --git a/src/com/android/contacts/calllog/CallLogGroupBuilder.java b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
index 95d85da..f5aef3f 100644
--- a/src/com/android/contacts/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
@@ -16,6 +16,7 @@
package com.android.contacts.calllog;
+import com.android.common.widget.GroupingListAdapter;
import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
import android.database.CharArrayBuffer;
@@ -25,6 +26,8 @@
/**
* Groups together calls in the call log.
+ * <p>
+ * This class is meant to be used in conjunction with {@link GroupingListAdapter}.
*/
public class CallLogGroupBuilder {
/** Reusable char array buffer. */
@@ -32,66 +35,95 @@
/** Reusable char array buffer. */
private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
- private final CallLogFragment.GroupCreator mAdapter;
+ /** The object on which the groups are created. */
+ private final CallLogFragment.GroupCreator mGroupCreator;
- public CallLogGroupBuilder(CallLogFragment.GroupCreator adapter) {
- mAdapter = adapter;
+ public CallLogGroupBuilder(CallLogFragment.GroupCreator groupCreator) {
+ mGroupCreator = groupCreator;
}
+ /**
+ * Finds all groups of adjacent entries in the call log which should be grouped together and
+ * calls {@link CallLogFragment.GroupCreator#addGroup(int, int, boolean)} on
+ * {@link #mGroupCreator} for each of them.
+ * <p>
+ * For entries that are not grouped with others, we do not need to create a group of size one.
+ * <p>
+ * It assumes that the cursor will not change during its execution.
+ *
+ * @see GroupingListAdapter#addGroups(Cursor)
+ */
public void addGroups(Cursor cursor) {
- int count = cursor.getCount();
+ final int count = cursor.getCount();
if (count == 0) {
return;
}
- int groupItemCount = 1;
-
- CharArrayBuffer currentValue = mBuffer1;
- CharArrayBuffer value = mBuffer2;
+ int currentGroupSize = 1;
+ // The number of the first entry in the group.
+ CharArrayBuffer firstNumber = mBuffer1;
+ // The number of the current row in the cursor.
+ CharArrayBuffer currentNumber = mBuffer2;
cursor.moveToFirst();
- cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue);
- int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
- for (int i = 1; i < count; i++) {
- cursor.moveToNext();
- cursor.copyStringToBuffer(CallLogQuery.NUMBER, value);
- boolean sameNumber = equalPhoneNumbers(value, currentValue);
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, firstNumber);
+ // This is the type of the first call in the group.
+ int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ while (cursor.moveToNext()) {
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentNumber);
+ final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ final boolean sameNumber = equalPhoneNumbers(firstNumber, currentNumber);
+ final boolean shouldGroup;
- // Group adjacent calls with the same number. Make an exception
- // for the latest item if it was a missed call. We don't want
- // a missed call to be hidden inside a group.
- if (sameNumber && currentCallType != Calls.MISSED_TYPE
- && !CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
- groupItemCount++;
+ if (CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
+ // Cannot group headers.
+ shouldGroup = false;
+ } else if (!sameNumber) {
+ // Should only group with calls from the same number.
+ shouldGroup = false;
+ } else if (firstCallType == Calls.VOICEMAIL_TYPE
+ || firstCallType == Calls.MISSED_TYPE) {
+ // Voicemail and missed calls should only be grouped with subsequent missed calls.
+ shouldGroup = callType == Calls.MISSED_TYPE;
} else {
- if (groupItemCount > 1) {
- addGroup(i - groupItemCount, groupItemCount, false);
+ // Incoming and outgoing calls group together.
+ shouldGroup = callType == Calls.INCOMING_TYPE || callType == Calls.OUTGOING_TYPE;
+ }
+
+ if (shouldGroup) {
+ // Increment the size of the group to include the current call, but do not create
+ // the group until we find a call that does not match.
+ currentGroupSize++;
+ } else {
+ // Create a group for the previous set of calls, excluding the current one, but do
+ // not create a group for a single call.
+ if (currentGroupSize > 1) {
+ addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
}
-
- groupItemCount = 1;
-
- // Swap buffers
- CharArrayBuffer temp = currentValue;
- currentValue = value;
- value = temp;
-
- // If we have just examined a row following a missed call, make
- // sure that it is grouped with subsequent calls from the same number
- // even if it was also missed.
- if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
- currentCallType = 0; // "not a missed call"
- } else {
- currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
- }
+ // Start a new group; it will include at least the current call.
+ currentGroupSize = 1;
+ // The current entry is now the first in the group. For the CharArrayBuffers, we
+ // need to swap them.
+ firstCallType = callType;
+ CharArrayBuffer temp = firstNumber; // Used to swap.
+ firstNumber = currentNumber;
+ currentNumber = temp;
}
}
- if (groupItemCount > 1) {
- addGroup(count - groupItemCount, groupItemCount, false);
+ // If the last set of calls at the end of the call log was itself a group, create it now.
+ if (currentGroupSize > 1) {
+ addGroup(count - currentGroupSize, currentGroupSize);
}
}
- /** @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean) */
- private void addGroup(int cursorPosition, int size, boolean expanded) {
- mAdapter.addGroup(cursorPosition, size, expanded);
+ /**
+ * Creates a group of items in the cursor.
+ * <p>
+ * The group is always unexpanded.
+ *
+ * @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean)
+ */
+ private void addGroup(int cursorPosition, int size) {
+ mGroupCreator.addGroup(cursorPosition, size, false);
}
private boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 2a5583d..3092087 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -38,6 +38,7 @@
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -77,7 +78,7 @@
mImportRequest = request;
mJobId = jobId;
mNotifier = new ImportProgressNotifier(service, mNotificationManager, jobId,
- request.originalUri.getLastPathSegment());
+ request.displayName);
}
@Override
@@ -141,11 +142,36 @@
new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
constructor.addEntryHandler(committer);
- constructor.addEntryHandler(mNotifier);
+ if (!request.showImmediately) {
+ constructor.addEntryHandler(mNotifier);
+ }
- final boolean successful =
- readOneVCard(uri, estimatedVCardType, estimatedCharset,
- constructor, possibleVCardVersions);
+ InputStream is = null;
+ boolean successful = false;
+ try {
+ if (uri != null) {
+ Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")");
+ is = mResolver.openInputStream(uri);
+ } else if (request.data != null){
+ Log.i(LOG_TAG, "start importing one vCard (byte[])");
+ is = new ByteArrayInputStream(request.data);
+ }
+
+ if (is != null) {
+ successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor,
+ possibleVCardVersions);
+ }
+ } catch (IOException e) {
+ successful = false;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
mService.handleFinishImportNotification(mJobId, successful);
@@ -177,7 +203,7 @@
private void doCancelNotification() {
final String description = mService.getString(R.string.importing_vcard_canceled_title,
- mImportRequest.originalUri.getLastPathSegment());
+ mImportRequest.displayName);
final Notification notification =
VCardService.constructCancelNotification(mService, description);
mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
@@ -185,7 +211,7 @@
private void doFinishNotification(final Uri createdUri) {
final String description = mService.getString(R.string.importing_vcard_finished_title,
- mImportRequest.originalUri.getLastPathSegment());
+ mImportRequest.displayName);
final Intent intent;
if (createdUri != null) {
final long rawContactId = ContentUris.parseId(createdUri);
@@ -196,20 +222,24 @@
} else {
intent = null;
}
- final Notification notification =
- VCardService.constructFinishNotification(mService,
- description, null, intent);
- mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
+ if (mImportRequest.showImmediately && (intent != null)) {
+ mNotificationManager.cancel(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mService.startActivity(intent);
+ } else {
+ final Notification notification = VCardService.constructFinishNotification(mService,
+ description, null, intent);
+ mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId,
+ notification);
+ }
}
- private boolean readOneVCard(Uri uri, int vcardType, String charset,
+ private boolean readOneVCard(InputStream is, int vcardType, String charset,
final VCardInterpreter interpreter,
final int[] possibleVCardVersions) {
- Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")");
boolean successful = false;
final int length = possibleVCardVersions.length;
for (int i = 0; i < length; i++) {
- InputStream is = null;
final int vcardVersion = possibleVCardVersions[i];
try {
if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
@@ -217,8 +247,6 @@
((VCardEntryConstructor) interpreter).clear();
}
- is = mResolver.openInputStream(uri);
-
// We need synchronized block here,
// since we need to handle mCanceled and mVCardParser at once.
// In the worst case, a user may call cancel() just before creating
diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java
index e8b5606..84fbb0e 100644
--- a/src/com/android/contacts/vcard/ImportRequest.java
+++ b/src/com/android/contacts/vcard/ImportRequest.java
@@ -35,30 +35,42 @@
* Can be null (typically when there's no Account available in the system).
*/
public final Account account;
+
/**
* Uri to be imported. May have different content than originally given from users, so
* when displaying user-friendly information (e.g. "importing xxx.vcf"), use
- * {@link #originalUri} instead.
+ * {@link #displayName} instead.
+ *
+ * If this is null {@link #data} contains the byte stream of the vcard.
*/
public final Uri uri;
/**
- * Original uri given from users.
- * Useful when showing user-friendly information ("importing xxx.vcf"), as
- * {@link #uri} may have different name than the original (like "import_tmp_1.vcf").
- *
- * This variable must not be used for doing actual processing like re-import, as the app
- * may not have right permission to do so.
+ * Holds the byte stream of the vcard, if {@link #uri} is null.
*/
- public final Uri originalUri;
+ public final byte[] data;
+
+ /**
+ * String to be displayed to the user to indicate the source of the VCARD.
+ */
+ public final String displayName;
+
+ /**
+ * Whether to show the imported vcard immediately after the import is done.
+ * If set to false, just a notification will be shown.
+ */
+ public final boolean showImmediately;
+
/**
* Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
*/
public final int estimatedVCardType;
+
/**
* Can be null, meaning no preferable charset is available.
*/
public final String estimatedCharset;
+
/**
* Assumes that one Uri contains only one version, while there's a (tiny) possibility
* we may have two types in one vCard.
@@ -88,15 +100,18 @@
* and may become invalid after its close() request).
*/
public final int entryCount;
+
public ImportRequest(Account account,
- Uri uri, Uri originalUri, int estimatedType, String estimatedCharset,
- int vcardVersion, int entryCount) {
+ byte[] data, Uri uri, String displayName, int estimatedType, String estimatedCharset,
+ int vcardVersion, int entryCount, boolean showImmediately) {
this.account = account;
+ this.data = data;
this.uri = uri;
- this.originalUri = originalUri;
+ this.displayName = displayName;
this.estimatedVCardType = estimatedType;
this.estimatedCharset = estimatedCharset;
this.vcardVersion = vcardVersion;
this.entryCount = entryCount;
+ this.showImmediately = showImmediately;
}
}
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 2cfd71a..0b84440 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -46,6 +46,9 @@
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@@ -61,6 +64,7 @@
import android.util.Log;
import android.widget.Toast;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -68,6 +72,7 @@
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -115,9 +120,6 @@
private Account mAccount;
- private String mAction;
- private Uri mUri;
-
private ProgressDialog mProgressDialogForScanVCard;
private ProgressDialog mProgressDialogForCachingVCard;
@@ -229,15 +231,38 @@
private PowerManager.WakeLock mWakeLock;
private VCardParser mVCardParser;
private final Uri[] mSourceUris; // Given from a caller.
+ private final byte[] mSource;
+ private final String mDisplayName;
+ private final boolean mShowImmediately;
public VCardCacheThread(final Uri[] sourceUris) {
mSourceUris = sourceUris;
+ mSource = null;
final Context context = ImportVCardActivity.this;
final PowerManager powerManager =
(PowerManager)context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_DIM_WAKE_LOCK |
PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ mDisplayName = null;
+ // Showing immediately could make sense here if we restrict
+ // it to cases where we import a single vcard. For now disable
+ // this feature though.
+ mShowImmediately = false;
+ }
+
+ public VCardCacheThread(final byte[] data, String displayName,
+ final boolean showImmediately) {
+ mSource = data;
+ mSourceUris = null;
+ final Context context = ImportVCardActivity.this;
+ final PowerManager powerManager =
+ (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ mDisplayName = displayName;
+ mShowImmediately = showImmediately;
}
@Override
@@ -274,47 +299,59 @@
// to local storage, but currently vCard code does not allow us to do so.
int cache_index = 0;
ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
- for (Uri sourceUri : mSourceUris) {
- String filename = null;
- // Note: caches are removed by VCardService.
- while (true) {
- filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
- final File file = context.getFileStreamPath(filename);
- if (!file.exists()) {
- break;
- } else {
- if (cache_index == Integer.MAX_VALUE) {
- throw new RuntimeException("Exceeded cache limit");
- }
- cache_index++;
- }
- }
- final Uri localDataUri = copyTo(sourceUri, filename);
- if (mCanceled) {
- Log.i(LOG_TAG, "vCard cache operation is canceled.");
- break;
- }
- if (localDataUri == null) {
- Log.w(LOG_TAG, "destUri is null");
- break;
- }
- final ImportRequest request;
+ if (mSource != null) {
try {
- request = constructImportRequest(localDataUri, sourceUri);
+ requests.add(constructImportRequest(mSource, null, mDisplayName,
+ mShowImmediately));
} catch (VCardException e) {
Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
showFailureNotification(R.string.fail_reason_not_supported);
return;
- } catch (IOException e) {
- Log.e(LOG_TAG, "Unexpected IOException", e);
- showFailureNotification(R.string.fail_reason_io_error);
- return;
}
- if (mCanceled) {
- Log.i(LOG_TAG, "vCard cache operation is canceled.");
- return;
+ } else {
+ for (Uri sourceUri : mSourceUris) {
+ String filename = null;
+ // Note: caches are removed by VCardService.
+ while (true) {
+ filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
+ final File file = context.getFileStreamPath(filename);
+ if (!file.exists()) {
+ break;
+ } else {
+ if (cache_index == Integer.MAX_VALUE) {
+ throw new RuntimeException("Exceeded cache limit");
+ }
+ cache_index++;
+ }
+ }
+ final Uri localDataUri = copyTo(sourceUri, filename);
+ if (mCanceled) {
+ Log.i(LOG_TAG, "vCard cache operation is canceled.");
+ break;
+ }
+ if (localDataUri == null) {
+ Log.w(LOG_TAG, "destUri is null");
+ break;
+ }
+ final ImportRequest request;
+ try {
+ request = constructImportRequest(null, localDataUri,
+ sourceUri.getLastPathSegment(), mShowImmediately);
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
+ showFailureNotification(R.string.fail_reason_not_supported);
+ return;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unexpected IOException", e);
+ showFailureNotification(R.string.fail_reason_io_error);
+ return;
+ }
+ if (mCanceled) {
+ Log.i(LOG_TAG, "vCard cache operation is canceled.");
+ return;
+ }
+ requests.add(request);
}
- requests.add(request);
}
if (!requests.isEmpty()) {
mConnection.sendImportRequest(requests);
@@ -395,11 +432,12 @@
* @arg localDataUri Uri actually used for the import. Should be stored in
* app local storage, as we cannot guarantee other types of Uris can be read
* multiple times. This variable populates {@link ImportRequest#uri}.
- * @arg originalUri Uri requested to be imported. Used mainly for displaying
- * information. This variable populates {@link ImportRequest#originalUri}.
+ * @arg displayName Used for displaying information to the user. This variable populates
+ * {@link ImportRequest#displayName}.
*/
- private ImportRequest constructImportRequest(
- final Uri localDataUri, final Uri originalUri)
+ private ImportRequest constructImportRequest(final byte[] data,
+ final Uri localDataUri, final String displayName,
+ final boolean showImmediately)
throws IOException, VCardException {
final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
VCardEntryCounter counter = null;
@@ -407,7 +445,12 @@
int vcardVersion = VCARD_VERSION_V21;
try {
boolean shouldUseV30 = false;
- InputStream is = resolver.openInputStream(localDataUri);
+ InputStream is;
+ if (data != null) {
+ is = new ByteArrayInputStream(data);
+ } else {
+ is = resolver.openInputStream(localDataUri);
+ }
mVCardParser = new VCardParser_V21();
try {
counter = new VCardEntryCounter();
@@ -422,7 +465,11 @@
}
shouldUseV30 = true;
- is = resolver.openInputStream(localDataUri);
+ if (data != null) {
+ is = new ByteArrayInputStream(data);
+ } else {
+ is = resolver.openInputStream(localDataUri);
+ }
mVCardParser = new VCardParser_V30();
try {
counter = new VCardEntryCounter();
@@ -449,10 +496,11 @@
// version before it
}
return new ImportRequest(mAccount,
- localDataUri, originalUri,
+ data, localDataUri, displayName,
detector.getEstimatedType(),
detector.getEstimatedCharset(),
- vcardVersion, counter.getCount());
+ vcardVersion, counter.getCount(),
+ showImmediately);
}
public Uri[] getSourceUris() {
@@ -719,6 +767,17 @@
});
}
+ private void importVCard(final byte[] data, final String displayName,
+ final boolean showImmediately) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ mVCardCacheThread = new VCardCacheThread(data, displayName,
+ showImmediately);
+ showDialog(R.id.dialog_cache_vcard);
+ }
+ });
+ }
+
private Dialog getSelectImportTypeDialog() {
final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
final AlertDialog.Builder builder = new AlertDialog.Builder(this)
@@ -784,8 +843,6 @@
if (intent != null) {
accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
- mAction = intent.getAction();
- mUri = intent.getData();
} else {
Log.e(LOG_TAG, "intent does not exist");
}
@@ -806,7 +863,7 @@
}
}
- startImport(mAction, mUri);
+ startImport();
}
@Override
@@ -816,7 +873,7 @@
mAccount = new Account(
intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE));
- startImport(mAction, mUri);
+ startImport();
} else {
if (resultCode != RESULT_CANCELED) {
Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
@@ -826,13 +883,34 @@
}
}
- private void startImport(String action, Uri uri) {
- if (uri != null) {
- Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
- importVCard(uri);
+ private void startImport() {
+ Intent intent = getIntent();
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+ // Handle inbound NDEF
+ NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
+ NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
+ NdefRecord record = msg.getRecords()[0];
+ String type = new String(record.getType(), Charset.forName("UTF8"));
+ if (record.getTnf() != NdefRecord.TNF_MIME_MEDIA ||
+ (!"text/x-vcard".equalsIgnoreCase(type) && !"text/vcard".equals(type))) {
+ Log.d(LOG_TAG, "Not a vcard");
+ showFailureNotification(R.string.fail_reason_not_supported);
+ finish();
+ return;
+ }
+ // For NFC imports, we always show the contact once import is
+ // complete.
+ importVCard(record.getPayload(), getString(R.string.nfc_vcard_file_name), true);
} else {
- Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
- doScanExternalStorageAndImportVCard();
+ // Handle inbound files
+ Uri uri = intent.getData();
+ if (uri != null) {
+ Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
+ importVCard(uri);
+ } else {
+ Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
+ doScanExternalStorageAndImportVCard();
+ }
}
}
@@ -967,7 +1045,7 @@
}
}
- private void showFailureNotification(int reasonId) {
+ /* package */ void showFailureNotification(int reasonId) {
final NotificationManager notificationManager =
(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
final Notification notification =
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index 261c1c8..0358e22 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -232,46 +232,46 @@
private synchronized void handleImportRequest(List<ImportRequest> requests) {
if (DEBUG) {
final ArrayList<String> uris = new ArrayList<String>();
- final ArrayList<String> originalUris = new ArrayList<String>();
+ final ArrayList<String> displayNames = new ArrayList<String>();
for (ImportRequest request : requests) {
uris.add(request.uri.toString());
- originalUris.add(request.originalUri.toString());
+ displayNames.add(request.displayName);
}
Log.d(LOG_TAG,
- String.format("received multiple import request (uri: %s, originalUri: %s)",
- uris.toString(), originalUris.toString()));
+ String.format("received multiple import request (uri: %s, displayName: %s)",
+ uris.toString(), displayNames.toString()));
}
final int size = requests.size();
for (int i = 0; i < size; i++) {
ImportRequest request = requests.get(i);
if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
- final String displayName;
- final String message;
- final String lastPathSegment = request.originalUri.getLastPathSegment();
- if ("file".equals(request.originalUri.getScheme()) &&
- lastPathSegment != null) {
- displayName = lastPathSegment;
- message = getString(R.string.vcard_import_will_start_message, displayName);
- } else {
- displayName = getString(R.string.vcard_unknown_filename);
- message = getString(
- R.string.vcard_import_will_start_message_with_default_name);
- }
+ if (!request.showImmediately) {
+ // Show a notification about the status
+ final String displayName;
+ final String message;
+ if (request.displayName != null) {
+ displayName = request.displayName;
+ message = getString(R.string.vcard_import_will_start_message, displayName);
+ } else {
+ displayName = getString(R.string.vcard_unknown_filename);
+ message = getString(
+ R.string.vcard_import_will_start_message_with_default_name);
+ }
- // We just want to show notification for the first vCard.
- if (i == 0) {
- // TODO: Ideally we should detect the current status of import/export and show
- // "started" when we can import right now and show "will start" when we cannot.
- Toast.makeText(this, message, Toast.LENGTH_LONG).show();
- }
+ // We just want to show notification for the first vCard.
+ if (i == 0) {
+ // TODO: Ideally we should detect the current status of import/export and
+ // show "started" when we can import right now and show "will start" when
+ // we cannot.
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+ }
- final Notification notification =
- constructProgressNotification(
- this, TYPE_IMPORT, message, message, mCurrentJobId,
- displayName, -1, 0);
- mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mCurrentJobId,
- notification);
+ final Notification notification = constructProgressNotification(this,
+ TYPE_IMPORT, message, message, mCurrentJobId, displayName, -1, 0);
+ mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG,
+ mCurrentJobId, notification);
+ }
mCurrentJobId++;
} else {
// TODO: a little unkind to show Toast in this case, which is shown just a moment.
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 1b8df6b..739f5f0 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -73,6 +73,16 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+
+ <activity android:name=".streamitems.StreamItemPopulatorActivity"
+ android:label="@string/streamItemPopulator"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/res/drawable/android.jpg b/tests/res/drawable/android.jpg
new file mode 100644
index 0000000..95693b2
--- /dev/null
+++ b/tests/res/drawable/android.jpg
Binary files differ
diff --git a/tests/res/drawable/goldengate.jpg b/tests/res/drawable/goldengate.jpg
new file mode 100644
index 0000000..7bd3f67
--- /dev/null
+++ b/tests/res/drawable/goldengate.jpg
Binary files differ
diff --git a/tests/res/drawable/iceland.jpg b/tests/res/drawable/iceland.jpg
new file mode 100644
index 0000000..0ed210e
--- /dev/null
+++ b/tests/res/drawable/iceland.jpg
Binary files differ
diff --git a/tests/res/drawable/japan.jpg b/tests/res/drawable/japan.jpg
new file mode 100644
index 0000000..e39f387
--- /dev/null
+++ b/tests/res/drawable/japan.jpg
Binary files differ
diff --git a/tests/res/drawable/sydney.jpg b/tests/res/drawable/sydney.jpg
new file mode 100644
index 0000000..02b407c
--- /dev/null
+++ b/tests/res/drawable/sydney.jpg
Binary files differ
diff --git a/tests/res/drawable/wharf.jpg b/tests/res/drawable/wharf.jpg
new file mode 100644
index 0000000..fa6b04f
--- /dev/null
+++ b/tests/res/drawable/wharf.jpg
Binary files differ
diff --git a/tests/res/drawable/whiskey.jpg b/tests/res/drawable/whiskey.jpg
new file mode 100644
index 0000000..e8ffb85
--- /dev/null
+++ b/tests/res/drawable/whiskey.jpg
Binary files differ
diff --git a/tests/res/layout/stream_item_populator.xml b/tests/res/layout/stream_item_populator.xml
new file mode 100644
index 0000000..acc46bf
--- /dev/null
+++ b/tests/res/layout/stream_item_populator.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+>
+ <Button
+ android:id="@+id/add"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/chooseAContactButton"
+ android:layout_marginBottom="50px"
+ />
+ <Button
+ android:id="@+id/exit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/exitButton"
+ />
+</LinearLayout>
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 9f9f5a4..a2e9640 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -96,6 +96,10 @@
<string name="addedLogEntriesToast">Added %1$d call log entries.</string>
<string name="noLogEntriesToast">No entries in the call log yet.</string>
+ <string name="chooseAContactButton">Choose a contact to add stream items to</string>
+ <string name="exitButton">Exit</string>
+ <string name="streamItemPopulator">Populate stream items</string>
+
<string-array name="pinnedHeaderUseCases">
<item>One short section - no headers</item>
<item>Two short sections with headers</item>
diff --git a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
index f8da9c8..9aa5d7b 100644
--- a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
@@ -47,7 +47,7 @@
super.setUp();
mFakeGroupCreator = new FakeGroupCreator();
mBuilder = new CallLogGroupBuilder(mFakeGroupCreator);
- mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ createCursor();
}
@Override
@@ -107,10 +107,111 @@
assertGroupIs(4, 2, false, mFakeGroupCreator.groups.get(1));
}
+ public void testAddGroups_Voicemail() {
+ // Groups with one or more missed calls.
+ assertCallsAreGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE);
+ assertCallsAreGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ // Does not group with other types of calls, include voicemail themselves.
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.OUTGOING_TYPE);
+ }
+
+ public void testAddGroups_Missed() {
+ // Groups with one or more missed calls.
+ assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ // Does not group with other types of calls.
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.OUTGOING_TYPE);
+ }
+
+ public void testAddGroups_Incoming() {
+ // Groups with one or more incoming or outgoing.
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ // Does not group with voicemail and missed calls.
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+ }
+
+ public void testAddGroups_Outgoing() {
+ // Groups with one or more incoming or outgoing.
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ // Does not group with voicemail and missed calls.
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+ }
+
+ public void testAddGroups_Mixed() {
+ addMultipleOldCallLogEntries(TEST_NUMBER1,
+ Calls.VOICEMAIL_TYPE, // Stand-alone
+ Calls.INCOMING_TYPE, // Group 1: 1-2
+ Calls.OUTGOING_TYPE,
+ Calls.MISSED_TYPE, // Group 2: 3-4
+ Calls.MISSED_TYPE,
+ Calls.VOICEMAIL_TYPE, // Stand-alone
+ Calls.INCOMING_TYPE, // Stand-alone
+ Calls.VOICEMAIL_TYPE, // Group 3: 7-9
+ Calls.MISSED_TYPE,
+ Calls.MISSED_TYPE,
+ Calls.OUTGOING_TYPE); // Stand-alone
+ mBuilder.addGroups(mCursor);
+ assertEquals(3, mFakeGroupCreator.groups.size());
+ assertGroupIs(1, 2, false, mFakeGroupCreator.groups.get(0));
+ assertGroupIs(3, 2, false, mFakeGroupCreator.groups.get(1));
+ assertGroupIs(7, 3, false, mFakeGroupCreator.groups.get(2));
+ }
+
+ /** Creates (or recreates) the cursor used to store the call log content for the tests. */
+ private void createCursor() {
+ mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ }
+
+ /** Clears the content of the {@link FakeGroupCreator} used in the tests. */
+ private void clearFakeGroupCreator() {
+ mFakeGroupCreator.groups.clear();
+ }
+
+ /** Asserts that calls of the given types are grouped together into a single group. */
+ private void assertCallsAreGrouped(int... types) {
+ createCursor();
+ clearFakeGroupCreator();
+ addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+ mBuilder.addGroups(mCursor);
+ assertEquals(1, mFakeGroupCreator.groups.size());
+ assertGroupIs(0, types.length, false, mFakeGroupCreator.groups.get(0));
+
+ }
+
+ /** Asserts that calls of the given types are not grouped together at all. */
+ private void assertCallsAreNotGrouped(int... types) {
+ createCursor();
+ clearFakeGroupCreator();
+ addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+ mBuilder.addGroups(mCursor);
+ assertEquals(0, mFakeGroupCreator.groups.size());
+ }
+
+ /** Adds a set of calls with the given types, all from the same number, in the old section. */
+ private void addMultipleOldCallLogEntries(String number, int... types) {
+ for (int type : types) {
+ addOldCallLogEntry(number, type);
+ }
+ }
+
+ /** Adds a call with the given number and type to the old section of the call log. */
private void addOldCallLogEntry(String number, int type) {
addCallLogEntry(number, type, CallLogQuery.SECTION_OLD_ITEM);
}
+ /** Adds a call with the given number and type to the new section of the call log. */
private void addNewCallLogEntry(String number, int type) {
addCallLogEntry(number, type, CallLogQuery.SECTION_NEW_ITEM);
}
@@ -127,10 +228,12 @@
});
}
+ /** Adds the old section header to the call log. */
private void addOldCallLogHeader() {
addCallLogHeader(CallLogQuery.SECTION_OLD_HEADER);
}
+ /** Adds the new section header to the call log. */
private void addNewCallLogHeader() {
addCallLogHeader(CallLogQuery.SECTION_NEW_HEADER);
}
@@ -152,9 +255,13 @@
assertEquals(expanded, group.expanded);
}
+ /** Defines an added group. Used by the {@link FakeGroupCreator}. */
private static class GroupSpec {
+ /** The starting position of the group. */
public final int cursorPosition;
+ /** The number of elements in the group. */
public final int size;
+ /** Whether the group should be initially expanded. */
public final boolean expanded;
public GroupSpec(int cursorPosition, int size, boolean expanded) {
@@ -164,8 +271,11 @@
}
}
+ /** Fake implementation of a GroupCreator which stores the created groups in a member field. */
private static class FakeGroupCreator implements CallLogFragment.GroupCreator {
+ /** The list of created groups. */
public final List<GroupSpec> groups = newArrayList();
+
@Override
public void addGroup(int cursorPosition, int size, boolean expanded) {
groups.add(new GroupSpec(cursorPosition, size, expanded));
diff --git a/tests/src/com/android/contacts/tests/streamitems/StreamItemPopulatorActivity.java b/tests/src/com/android/contacts/tests/streamitems/StreamItemPopulatorActivity.java
new file mode 100644
index 0000000..5613bc3
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/streamitems/StreamItemPopulatorActivity.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.tests.streamitems;
+
+import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.tests.R;
+import com.google.android.collect.Lists;
+
+import android.app.Activity;
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StreamItemPhotos;
+import android.provider.ContactsContract.StreamItems;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Random;
+
+/**
+ * Testing activity that will populate stream items and stream item photos to selected
+ * entries in the user's contacts list.
+ *
+ * The contact selected must have at least one raw contact that was provided by Google.
+ */
+public class StreamItemPopulatorActivity extends Activity {
+
+ // Test data to randomly select from.
+ private String[] snippetStrings = new String[]{
+ "Just got back from a vacation in %1$s - what a great place! Can't wait to go back.",
+ "If I never see %1$s again it will be too soon.",
+ "This is a public service announcement. If you were even close to considering visiting"
+ + " %1$s, I strongly advise you to reconsider. The food was terrible, the people were "
+ + "rude, the hygiene of the bus and taxi drivers was positively <i>barbaric</i>. I "
+ + "feared for my life almost the entire time I was there, and feel lucky to be back "
+ + "<b>home</b>.",
+ "Check out these pictures! I took them in %1$s"
+ };
+
+ private String[] placeNames = new String[]{
+ "the Google campus in Mountain View",
+ "the deserts on Arrakis",
+ "Iceland",
+ "Japan",
+ "Sydney",
+ "San Francisco",
+ "Munich",
+ "Istanbul",
+ "Tanagra",
+ "the restricted section of Area 51",
+ "the middle of nowhere"
+ };
+
+ // Photos to randomly select from.
+ private Integer[] imageIds = new Integer[]{
+ R.drawable.android,
+ R.drawable.goldengate,
+ R.drawable.iceland,
+ R.drawable.japan,
+ R.drawable.sydney,
+ R.drawable.wharf,
+ R.drawable.whiskey
+ };
+
+ // The contact ID that was picked.
+ private long mContactId = -1;
+
+ private Random mRandom;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mRandom = new Random(System.currentTimeMillis());
+
+ setContentView(R.layout.stream_item_populator);
+ Button pickButton = (Button) findViewById(R.id.add);
+ pickButton.setOnClickListener(new View.OnClickListener(){
+ @Override
+ public void onClick(View v) {
+ // Reset the contact ID.
+ mContactId = -1;
+
+ // Forward the Intent to the picker
+ final Intent pickerIntent =
+ new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
+ pickerIntent.setFlags(
+ Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivityForResult(pickerIntent, 0);
+ }
+ });
+
+ Button exitButton = (Button) findViewById(R.id.exit);
+ exitButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ Uri contactUri = data.getData();
+ mContactId = ContentUris.parseId(contactUri);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mContactId != -1) {
+ long rawContactId = -1;
+ String accountType = null;
+ String accountName = null;
+
+ // Lookup the com.google raw contact for the contact.
+ Cursor c = getContentResolver().query(RawContacts.CONTENT_URI,
+ new String[]{
+ RawContacts._ID,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.ACCOUNT_NAME
+ },
+ RawContacts.CONTACT_ID + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
+ new String[]{String.valueOf(mContactId), GoogleAccountType.ACCOUNT_TYPE}, null);
+ try {
+ c.moveToFirst();
+ rawContactId = c.getLong(0);
+ accountType = c.getString(1);
+ accountName = c.getString(2);
+ } finally {
+ c.close();
+ }
+ if (rawContactId != -1) {
+ addStreamItemsToRawContact(rawContactId, accountType, accountName);
+ } else {
+ Toast.makeText(this,
+ "Failed to find raw contact ID for contact ID " + mContactId, 5).show();
+ }
+ }
+ }
+
+ protected byte[] loadPhotoFromResource(int resourceId) {
+ InputStream is = getResources().openRawResource(resourceId);
+ return readInputStreamFully(is);
+ }
+
+ protected byte[] readInputStreamFully(InputStream is) {
+ try {
+ byte[] buffer = new byte[is.available()];
+ is.read(buffer);
+ is.close();
+ return buffer;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void addStreamItemsToRawContact(long rawContactId, String accountType,
+ String accountName) {
+ ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
+
+ // Add from 1-5 stream items.
+ int itemsToAdd = randInt(5) + 1;
+ int opCount = 0;
+ for (int i = 0; i < itemsToAdd; i++) {
+ ContentValues streamItemValues = buildStreamItemValues(accountType, accountName);
+ ops.add(ContentProviderOperation.newInsert(
+ Uri.withAppendedPath(
+ ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId),
+ ContactsContract.RawContacts.StreamItems.CONTENT_DIRECTORY))
+ .withValues(streamItemValues).build());
+
+ // Maybe add photos - 30% chance per stream item.
+ boolean includePhotos = randInt(100) < 30;
+ if (includePhotos) {
+ // Add 1-5 photos if we're including any.
+ int numPhotos = randInt(5) + 1;
+ for (int j = 0; j < numPhotos; j++) {
+ ContentValues streamItemPhotoValues =
+ buildStreamItemPhotoValues(j, accountType, accountName);
+ ops.add(ContentProviderOperation.newInsert(StreamItems.CONTENT_PHOTO_URI)
+ .withValues(streamItemPhotoValues)
+ .withValueBackReference(StreamItemPhotos.STREAM_ITEM_ID, opCount)
+ .build());
+ }
+ opCount += numPhotos;
+ }
+ opCount++;
+ }
+ try {
+ getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
+ } catch (Exception e) {
+ // We don't care. This is just for test purposes.
+ throw new RuntimeException(e);
+ }
+ Toast.makeText(this, "Added " + itemsToAdd + " stream item(s) and "
+ + (opCount - itemsToAdd) + " photos", 5).show();
+ }
+
+ private ContentValues buildStreamItemValues(String accountType, String accountName) {
+ ContentValues values = new ContentValues();
+ values.put(StreamItems.TEXT,
+ String.format(pickRandom(snippetStrings), pickRandom(placeNames)));
+ values.put(StreamItems.COMMENTS, "");
+ // Set the timestamp to some point in the past.
+ values.put(StreamItems.TIMESTAMP,
+ System.currentTimeMillis() - randInt(360000000));
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ return values;
+ }
+
+ private ContentValues buildStreamItemPhotoValues(int index, String accountType,
+ String accountName) {
+ ContentValues values = new ContentValues();
+ values.put(StreamItemPhotos.SORT_INDEX, index);
+ values.put(StreamItemPhotos.PHOTO, loadPhotoFromResource(pickRandom(imageIds)));
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ return values;
+ }
+
+ private <T> T pickRandom(T[] from) {
+ return from[randInt(from.length)];
+ }
+
+ private int randInt(int max) {
+ return Math.abs(mRandom.nextInt()) % max;
+ }
+}