Merge "Add fields to AccountType for the new "invite" feature"
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/layout/call_log_list_item_layout.xml b/res/layout/call_log_list_item_layout.xml
index 344413c..4fbe426 100644
--- a/res/layout/call_log_list_item_layout.xml
+++ b/res/layout/call_log_list_item_layout.xml
@@ -20,6 +20,7 @@
android:layout_height="?attr/call_log_list_contact_photo_size"
android:layout_toRightOf="@id/contact_photo"
android:layout_toLeftOf="@id/divider"
+ android:layout_alignWithParentIfMissing="true"
android:layout_marginLeft="?attr/call_log_inner_margin"
>
<include layout="@layout/call_log_phone_call_details"/>
diff --git a/res/layout/contact_tile_list.xml b/res/layout/contact_tile_list.xml
index 79c6ecf..69ae1cc 100644
--- a/res/layout/contact_tile_list.xml
+++ b/res/layout/contact_tile_list.xml
@@ -14,9 +14,22 @@
limitations under the License.
-->
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/contact_tile_list"
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:divider="@null"
-/>
+ android:layout_height="match_parent">
+
+ <ListView
+ android:id="@+id/contact_tile_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null" />
+
+ <TextView
+ android:id="@+id/contact_tile_list_empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+</FrameLayout>
diff --git a/res/layout/item_group_membership.xml b/res/layout/item_group_membership.xml
index 34ba773..13ddb20 100644
--- a/res/layout/item_group_membership.xml
+++ b/res/layout/item_group_membership.xml
@@ -20,10 +20,11 @@
android:layout_height="wrap_content"
android:orientation="vertical">
- <View
+ <ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="@drawable/divider_horizontal_light" />
+ android:scaleType="fitXY"
+ android:src="@drawable/divider_horizontal_light" />
<include
android:id="@+id/kind_title"
diff --git a/res/layout/item_kind_section.xml b/res/layout/item_kind_section.xml
index 6c6f960..2c6dc6f 100644
--- a/res/layout/item_kind_section.xml
+++ b/res/layout/item_kind_section.xml
@@ -23,10 +23,11 @@
android:paddingBottom="@dimen/editor_field_bottom_padding"
android:orientation="vertical">
- <View
+ <ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="@drawable/divider_horizontal_light" />
+ android:scaleType="fitXY"
+ android:src="@drawable/divider_horizontal_light" />
<LinearLayout
android:id="@+id/kind_editors"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ab1f86f..b90898d 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>
@@ -1623,6 +1623,9 @@
<!-- Initial display for position of current playback, do not translate. -->
<string name="voicemail_initial_time">00:05</string>
+ <!-- Message to show when there is an error playing back the voicemail. -->
+ <string name="voicemail_playback_error">Could not play voicemail</string>
+
<!-- The separator between the call type text and the date in the call log [CHAR LIMIT=3] -->
<string name="call_log_type_date_separator">/</string>
@@ -1659,4 +1662,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/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index d411927..14fec85 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -174,13 +174,7 @@
public void onResume() {
super.onResume();
updateData(getCallLogEntryUris());
- Uri voicemailUri = getIntent().getExtras().getParcelable(EXTRA_VOICEMAIL_URI);
- optionallyHandleVoicemail(voicemailUri);
- if (voicemailUri != null) {
- mAsyncQueryHandler.startVoicemailStatusQuery(voicemailUri);
- } else {
- mStatusMessageView.setVisibility(View.GONE);
- }
+ optionallyHandleVoicemail();
}
/**
@@ -189,18 +183,22 @@
* If the Intent used to start this Activity contains the suitable extras, then start voicemail
* playback. If it doesn't, then hide the voicemail ui.
*/
- private void optionallyHandleVoicemail(Uri voicemailUri) {
+ private void optionallyHandleVoicemail() {
+ Uri voicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
FragmentManager manager = getFragmentManager();
VoicemailPlaybackFragment fragment = (VoicemailPlaybackFragment) manager.findFragmentById(
R.id.voicemail_playback_fragment);
- if (voicemailUri == null) {
- // No voicemail uri: hide the voicemail fragment.
- manager.beginTransaction().hide(fragment).commit();
- } else {
- // A voicemail: extra tells us if we should start playback or not.
- boolean startPlayback = getIntent().getExtras().getBoolean(
+ if (voicemailUri != null) {
+ // Has voicemail uri: leave the fragment visible. Optionally start the playback.
+ // Do a query to fetch the voicemail status messages.
+ boolean startPlayback = getIntent().getBooleanExtra(
EXTRA_VOICEMAIL_START_PLAYBACK, false);
fragment.setVoicemailUri(voicemailUri, startPlayback);
+ mAsyncQueryHandler.startVoicemailStatusQuery(voicemailUri);
+ } else {
+ // No voicemail uri: hide the voicemail fragment and the status view.
+ manager.beginTransaction().hide(fragment).commit();
+ mStatusMessageView.setVisibility(View.GONE);
}
}
@@ -307,11 +305,16 @@
// and then we can remove the "!isSipNumber" check above.
mainActionIntent = null;
mainActionIcon = 0;
- } else {
+ } else if (canPlaceCallsTo) {
mainActionIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
mainActionIntent.setType(Contacts.CONTENT_ITEM_TYPE);
mainActionIntent.putExtra(Insert.PHONE, mNumber);
mainActionIcon = R.drawable.sym_action_add;
+ } else {
+ // If we cannot call the number, when we probably cannot add it as a contact either.
+ // This is usually the case of private, unknown, or payphone numbers.
+ mainActionIntent = null;
+ mainActionIcon = 0;
}
if (mainActionIntent == null) {
diff --git a/src/com/android/contacts/CallDetailActivityQueryHandler.java b/src/com/android/contacts/CallDetailActivityQueryHandler.java
index c1d87b2..df5b4f7 100644
--- a/src/com/android/contacts/CallDetailActivityQueryHandler.java
+++ b/src/com/android/contacts/CallDetailActivityQueryHandler.java
@@ -57,8 +57,8 @@
* If the voicemail record does not have an audio yet then it fires the second query to get the
* voicemail status of the associated source.
*/
- public void startVoicemailStatusQuery(Uri voicemaiUri) {
- startQuery(QUERY_VOICEMAIL_CONTENT_TOKEN, null, voicemaiUri, VOICEMAIL_CONTENT_PROJECTION,
+ public void startVoicemailStatusQuery(Uri voicemailUri) {
+ startQuery(QUERY_VOICEMAIL_CONTENT_TOKEN, null, voicemailUri, VOICEMAIL_CONTENT_PROJECTION,
null, null, null);
}
@@ -67,11 +67,12 @@
try {
if (token == QUERY_VOICEMAIL_CONTENT_TOKEN) {
// Query voicemail status only if this voicemail record does not have audio.
- if (cursor.moveToFirst() && hasNoAudio(cursor)) {
+ if (moveToFirst(cursor) && hasNoAudio(cursor)) {
startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null,
Status.buildSourceUri(getSourcePackage(cursor)),
VoicemailStatusHelperImpl.PROJECTION, null, null, null);
} else {
+ // nothing to show in status
mCallDetailActivity.updateVoicemailStatusMessage(null);
}
} else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
@@ -84,6 +85,15 @@
}
}
+ /** Check that the cursor is non-null and can be moved to first. */
+ private boolean moveToFirst(Cursor cursor) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ Log.e(TAG, "Cursor not valid, could not move to first");
+ return false;
+ }
+ return true;
+ }
+
private boolean hasNoAudio(Cursor voicemailCursor) {
return voicemailCursor.getInt(HAS_CONTENT_COLUMN_INDEX) == 0;
}
diff --git a/src/com/android/contacts/PhoneCallDetailsHelper.java b/src/com/android/contacts/PhoneCallDetailsHelper.java
index 2c36cec..b2810bc 100644
--- a/src/com/android/contacts/PhoneCallDetailsHelper.java
+++ b/src/com/android/contacts/PhoneCallDetailsHelper.java
@@ -119,8 +119,7 @@
if (TextUtils.isEmpty(details.name)) {
nameText = displayNumber;
numberText = mPhoneNumberHelper.getGeocodeForNumber(
- mPhoneNumberHelper.parsePhoneNumber(
- details.number.toString(), details.countryIso));
+ details.number.toString(), details.countryIso);
} else {
nameText = details.name;
if (numberFormattedLabel != null) {
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index a12154d..863c2f4 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -25,8 +25,10 @@
import android.app.ActionBar.Tab;
import android.app.FragmentTransaction;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -57,6 +59,8 @@
private static final String EXTRA_KEY_QUERY = "navBar.query";
private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
+ private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
+
private boolean mSearchMode;
private String mQueryString;
@@ -64,6 +68,7 @@
private SearchView mSearchView;
private final Context mContext;
+ private final SharedPreferences mPrefs;
private final boolean mAlwaysShowSearchView;
private Listener mListener;
@@ -89,12 +94,14 @@
}
}
- private TabState mCurrentTab = TabState.FAVORITES;
+ private static final TabState DEFAULT_TAB = TabState.ALL;
+ private TabState mCurrentTab = DEFAULT_TAB;
public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar) {
mContext = context;
mListener = listener;
mActionBar = actionBar;
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
mSearchLabelText = mContext.getString(R.string.search_label);
mAlwaysShowSearchView = mContext.getResources().getBoolean(R.bool.always_show_search_view);
@@ -126,6 +133,7 @@
if (savedState == null) {
mSearchMode = request.isSearchMode();
mQueryString = request.getQueryString();
+ mCurrentTab = loadLastTabPreference();
} else {
mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
mQueryString = savedState.getString(EXTRA_KEY_QUERY);
@@ -190,6 +198,7 @@
}
if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
+ saveLastTabPreference(mCurrentTab);
}
public TabState getCurrentTab() {
@@ -340,4 +349,17 @@
mSearchView.requestFocus();
mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue.
}
+
+ private void saveLastTabPreference(TabState tab) {
+ mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab.ordinal()).apply();
+ }
+
+ private TabState loadLastTabPreference() {
+ try {
+ return TabState.fromInt(mPrefs.getInt(PERSISTENT_LAST_TAB, DEFAULT_TAB.ordinal()));
+ } catch (IllegalArgumentException e) {
+ // Preference is corrupt?
+ return DEFAULT_TAB;
+ }
+ }
}
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 62b8321..7a84e6d 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -20,6 +20,7 @@
import com.android.contacts.calllog.CallLogFragment;
import com.android.contacts.dialpad.DialpadFragment;
import com.android.contacts.interactions.PhoneNumberInteraction;
+import com.android.contacts.list.ContactTileAdapter.DisplayType;
import com.android.contacts.list.OnPhoneNumberPickerActionListener;
import com.android.contacts.list.PhoneNumberPickerFragment;
import com.android.contacts.list.ContactTileListFragment;
@@ -335,6 +336,7 @@
mStrequentFragment = (ContactTileListFragment) fragment;
mStrequentFragment.enableQuickContact(false);
mStrequentFragment.setListener(mStrequentListener);
+ mStrequentFragment.setDisplayType(DisplayType.STREQUENT_PHONE_ONLY);
} else if (fragment instanceof PhoneNumberPickerFragment) {
mSearchFragment = (PhoneNumberPickerFragment) fragment;
mSearchFragment.setOnPhoneNumberPickerActionListener(mPhoneNumberPickerActionListener);
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 0b403cb..6d031e9 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -202,6 +202,13 @@
return mProviderStatus == ProviderStatus.STATUS_NORMAL;
}
+ private boolean areAccountsAvailable() {
+ final ArrayList<Account> accounts =
+ AccountTypeManager.getInstance(this).getAccounts(true /* writeable */);
+ return !accounts.isEmpty();
+ }
+
+
/**
* Initialize fragments that are (or may not be) in the layout.
*
@@ -575,10 +582,12 @@
if (mActionBarAdapter.isSearchMode()) {
mTabPagerAdapter.setSearchMode(true);
} else {
+ // No smooth scrolling if quitting from the search mode.
+ final boolean wasSearchMode = mTabPagerAdapter.isSearchMode();
mTabPagerAdapter.setSearchMode(false);
int tabIndex = tab.ordinal();
if (mTabPager.getCurrentItem() != tabIndex) {
- mTabPager.setCurrentItem(tab.ordinal(), false /* no smooth scroll */);
+ mTabPager.setCurrentItem(tabIndex, !wasSearchMode);
}
}
invalidateOptionsMenu();
@@ -1212,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);
@@ -1230,8 +1241,13 @@
addGroupMenu.setVisible(false);
break;
case GROUPS:
+ // Do not display the "new group" button if no accounts are available
+ if (areAccountsAvailable()) {
+ addGroupMenu.setVisible(true);
+ } else {
+ addGroupMenu.setVisible(false);
+ }
addContactMenu.setVisible(false);
- addGroupMenu.setVisible(true);
break;
}
}
@@ -1241,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/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index c59cbf9..3383ab2 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -27,8 +27,8 @@
import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
import com.android.contacts.util.ExpirableCache;
import com.android.contacts.voicemail.VoicemailStatusHelper;
-import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
+import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
import com.android.internal.telephony.CallerInfo;
import com.android.internal.telephony.ITelephony;
import com.google.common.annotations.VisibleForTesting;
@@ -186,6 +186,42 @@
public String lookupKey;
public static ContactInfo EMPTY = new ContactInfo();
+
+ @Override
+ public int hashCode() {
+ // Uses only name and personId to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same personId.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (personId ^ (personId >>> 32));
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ ContactInfo other = (ContactInfo) obj;
+ if (personId != other.personId) return false;
+ if (!TextUtils.equals(name, other.name)) return false;
+ if (type != other.type) return false;
+ if (!TextUtils.equals(label, other.label)) return false;
+ if (!TextUtils.equals(number, other.number)) return false;
+ // Ignore formatted number.
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+ if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
+ if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
+ return true;
+ }
+
+ private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
+ if (thumbnailUri1 == thumbnailUri2) return true;
+ if (thumbnailUri1 == null) return false;
+ return thumbnailUri1.equals(thumbnailUri2);
+ }
}
public interface GroupCreator {
@@ -363,129 +399,175 @@
}
}
- private boolean queryContactInfo(String number) {
- // First check if there was a prior request for the same number
- // that was already satisfied
- ContactInfo info = mContactInfoCache.get(number);
- boolean needNotify = false;
- if (info != null && info != ContactInfo.EMPTY) {
- return true;
- } else {
- // Ok, do a fresh Contacts lookup for ciq.number.
- boolean infoUpdated = false;
+ /**
+ * Determines the contact information for the given SIP address.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+ final ContactInfo info;
- if (PhoneNumberUtils.isUriNumber(number)) {
- // This "number" is really a SIP address.
+ // TODO: This code is duplicated from the
+ // CallerInfoAsyncQuery class. To avoid that, could the
+ // code here just use CallerInfoAsyncQuery, rather than
+ // manually running ContentResolver.query() itself?
- // TODO: This code is duplicated from the
- // CallerInfoAsyncQuery class. To avoid that, could the
- // code here just use CallerInfoAsyncQuery, rather than
- // manually running ContentResolver.query() itself?
+ // We look up SIP addresses directly in the Data table:
+ Uri contactRef = Data.CONTENT_URI;
- // We look up SIP addresses directly in the Data table:
- Uri contactRef = Data.CONTENT_URI;
+ // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+ //
+ // Also note we use "upper(data1)" in the WHERE clause, and
+ // uppercase the incoming SIP address, in order to do a
+ // case-insensitive match.
+ //
+ // TODO: May also need to normalize by adding "sip:" as a
+ // prefix, if we start storing SIP addresses that way in the
+ // database.
+ String selection = "upper(" + Data.DATA1 + ")=?"
+ + " AND "
+ + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
+ String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
+ Cursor dataTableCursor =
+ getActivity().getContentResolver().query(
+ contactRef,
+ null, // projection
+ selection, // selection
+ selectionArgs, // selectionArgs
+ null); // sortOrder
+
+ if (dataTableCursor != null) {
+ if (dataTableCursor.moveToFirst()) {
+ info = new ContactInfo();
+
+ // TODO: we could slightly speed this up using an
+ // explicit projection (and thus not have to do
+ // those getColumnIndex() calls) but the benefit is
+ // very minimal.
+
+ // Note the Data.CONTACT_ID column here is
+ // equivalent to the PERSON_ID_COLUMN_INDEX column
+ // we use with "phonesCursor" below.
+ info.personId = dataTableCursor.getLong(
+ dataTableCursor.getColumnIndex(Data.CONTACT_ID));
+ info.name = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
+ // "type" and "label" are currently unused for SIP addresses
+ info.type = SipAddress.TYPE_OTHER;
+ info.label = null;
+
+ // And "number" is the SIP address.
// Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- //
- // Also note we use "upper(data1)" in the WHERE clause, and
- // uppercase the incoming SIP address, in order to do a
- // case-insensitive match.
- //
- // TODO: May also need to normalize by adding "sip:" as a
- // prefix, if we start storing SIP addresses that way in the
- // database.
- String selection = "upper(" + Data.DATA1 + ")=?"
- + " AND "
- + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
- String[] selectionArgs = new String[] { number.toUpperCase() };
-
- Cursor dataTableCursor =
- getActivity().getContentResolver().query(
- contactRef,
- null, // projection
- selection, // selection
- selectionArgs, // selectionArgs
- null); // sortOrder
-
- if (dataTableCursor != null) {
- if (dataTableCursor.moveToFirst()) {
- info = new ContactInfo();
-
- // TODO: we could slightly speed this up using an
- // explicit projection (and thus not have to do
- // those getColumnIndex() calls) but the benefit is
- // very minimal.
-
- // Note the Data.CONTACT_ID column here is
- // equivalent to the PERSON_ID_COLUMN_INDEX column
- // we use with "phonesCursor" below.
- info.personId = dataTableCursor.getLong(
- dataTableCursor.getColumnIndex(Data.CONTACT_ID));
- info.name = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
- // "type" and "label" are currently unused for SIP addresses
- info.type = SipAddress.TYPE_OTHER;
- info.label = null;
-
- // And "number" is the SIP address.
- // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- info.number = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DATA1));
- info.normalizedNumber = null; // meaningless for SIP addresses
- final String thumbnailUriString = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
- info.thumbnailUri = thumbnailUriString == null
- ? null
- : Uri.parse(thumbnailUriString);
- info.lookupKey = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
-
- infoUpdated = true;
- }
- dataTableCursor.close();
- }
+ info.number = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DATA1));
+ info.normalizedNumber = null; // meaningless for SIP addresses
+ final String thumbnailUriString = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
+ info.lookupKey = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
} else {
- // "number" is a regular phone number, so use the
- // PhoneLookup table:
- Cursor phonesCursor =
- getActivity().getContentResolver().query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
- Uri.encode(number)),
- PhoneQuery._PROJECTION, null, null, null);
- if (phonesCursor != null) {
- if (phonesCursor.moveToFirst()) {
- info = new ContactInfo();
- info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
- info.name = phonesCursor.getString(PhoneQuery.NAME);
- info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
- info.label = phonesCursor.getString(PhoneQuery.LABEL);
- info.number = phonesCursor
- .getString(PhoneQuery.MATCHED_NUMBER);
- info.normalizedNumber = phonesCursor
- .getString(PhoneQuery.NORMALIZED_NUMBER);
- final String thumbnailUriString = phonesCursor.getString(
- PhoneQuery.THUMBNAIL_URI);
- info.thumbnailUri = thumbnailUriString == null
- ? null
- : Uri.parse(thumbnailUriString);
- info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
-
- infoUpdated = true;
- }
- phonesCursor.close();
- }
+ info = ContactInfo.EMPTY;
}
-
- if (infoUpdated) {
- // New incoming phone number invalidates our formatted
- // cache. Any cache fills happen only on the GUI thread.
- info.formattedNumber = null;
- mContactInfoCache.put(number, info);
- // Inform list to update this item, if in view
- needNotify = true;
- }
+ dataTableCursor.close();
+ } else {
+ // Failed to fetch the data, ignore this request.
+ info = null;
}
- return needNotify;
+ return info;
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForPhoneNumber(String number) {
+ final ContactInfo info;
+
+ // "number" is a regular phone number, so use the
+ // PhoneLookup table:
+ Cursor phonesCursor =
+ getActivity().getContentResolver().query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(number)),
+ PhoneQuery._PROJECTION, null, null, null);
+ if (phonesCursor != null) {
+ if (phonesCursor.moveToFirst()) {
+ info = new ContactInfo();
+ info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+ info.name = phonesCursor.getString(PhoneQuery.NAME);
+ info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phonesCursor.getString(PhoneQuery.LABEL);
+ info.number = phonesCursor
+ .getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phonesCursor
+ .getString(PhoneQuery.NORMALIZED_NUMBER);
+ final String thumbnailUriString = phonesCursor.getString(
+ PhoneQuery.THUMBNAIL_URI);
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
+ info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+ } else {
+ info = ContactInfo.EMPTY;
+ }
+ phonesCursor.close();
+ } else {
+ // Failed to fetch the data, ignore this request.
+ info = null;
+ }
+ return info;
+ }
+
+ /**
+ * Queries the appropriate content provider for the contact associated with the number.
+ * <p>
+ * The number might be either a SIP address or a phone number.
+ * <p>
+ * It returns true if it updated the content of the cache and we should therefore tell the
+ * view to update its content.
+ */
+ private boolean queryContactInfo(String number) {
+ final ContactInfo info;
+
+ // Determine the contact info.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // This "number" is really a SIP address.
+ info = queryContactInfoForSipAddress(number);
+ } else {
+ info = queryContactInfoForPhoneNumber(number);
+ }
+
+ if (info == null) {
+ // The lookup failed, just return without requesting to update the view.
+ return false;
+ }
+
+ // Check the existing entry in the cache: only if it has changed we should update the
+ // view.
+ ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
+ boolean updated = !info.equals(existingInfo);
+ if (updated) {
+ // The formattedNumber is computed by the UI thread when needed. Since we updated
+ // the details of the contact, set this value to null for now.
+ info.formattedNumber = null;
+ }
+ // Store the data in the cache so that the UI thread can use to display it. Store it
+ // even if it has not changed so that it is marked as not expired.
+ mContactInfoCache.put(number, info);
+ return updated;
}
/*
@@ -766,7 +848,6 @@
if (getActivity() == null || getActivity().isFinishing()) {
return;
}
- Log.d(TAG, "updating adapter");
mAdapter.setLoading(false);
mAdapter.changeCursor(cursor);
if (mScrollToTop) {
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/calllog/CallLogListItemHelper.java b/src/com/android/contacts/calllog/CallLogListItemHelper.java
index 72d489d..d4f2291 100644
--- a/src/com/android/contacts/calllog/CallLogListItemHelper.java
+++ b/src/com/android/contacts/calllog/CallLogListItemHelper.java
@@ -55,10 +55,19 @@
boolean useIcons, boolean isHighlighted) {
mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details, useIcons,
isHighlighted);
- views.callView.setVisibility(
- mPhoneNumberHelper.canPlaceCallsTo(details.number)
- ? View.VISIBLE : View.INVISIBLE);
- views.playView.setVisibility(
- details.callTypes[0] == Calls.VOICEMAIL_TYPE ? View.VISIBLE : View.GONE);
+ boolean callVisible = mPhoneNumberHelper.canPlaceCallsTo(details.number);
+ boolean playVisible = details.callTypes[0] == Calls.VOICEMAIL_TYPE;
+
+ if (callVisible || playVisible) {
+ // At least one is visible. Keep the divider and the space for the call button.
+ views.callView.setVisibility(callVisible ? View.VISIBLE : View.INVISIBLE);
+ views.playView.setVisibility(playVisible ? View.VISIBLE : View.GONE);
+ views.dividerView.setVisibility(View.VISIBLE);
+ } else {
+ // Neither is visible, remove all of them entirely.
+ views.callView.setVisibility(View.GONE);
+ views.playView.setVisibility(View.GONE);
+ views.dividerView.setVisibility(View.GONE);
+ }
}
}
diff --git a/src/com/android/contacts/calllog/CallLogListItemViews.java b/src/com/android/contacts/calllog/CallLogListItemViews.java
index c34779d..b66d84e 100644
--- a/src/com/android/contacts/calllog/CallLogListItemViews.java
+++ b/src/com/android/contacts/calllog/CallLogListItemViews.java
@@ -34,6 +34,8 @@
public final ImageView callView;
/** The play action button used for voicemail. */
public final ImageView playView;
+ /** The divider between callView and playView. */
+ public final View dividerView;
/** The details of the phone call. */
public final PhoneCallDetailsViews phoneCallDetailsViews;
/** The item view for a stand-alone row, or null for other types of rows. */
@@ -44,11 +46,12 @@
public final TextView listHeaderTextView;
private CallLogListItemViews(QuickContactBadge photoView, ImageView callView,
- ImageView playView, PhoneCallDetailsViews phoneCallDetailsViews, View listItemView,
- View listHeaderView, TextView listHeaderTextView) {
+ ImageView playView, View dividerView, PhoneCallDetailsViews phoneCallDetailsViews,
+ View listItemView, View listHeaderView, TextView listHeaderTextView) {
this.photoView = photoView;
this.callView = callView;
this.playView = playView;
+ this.dividerView = dividerView;
this.phoneCallDetailsViews = phoneCallDetailsViews;
this.listItemView = listItemView;
this.listHeaderView = listHeaderView;
@@ -59,6 +62,7 @@
return new CallLogListItemViews((QuickContactBadge) view.findViewById(R.id.contact_photo),
(ImageView) view.findViewById(R.id.call_icon),
(ImageView) view.findViewById(R.id.play_icon),
+ view.findViewById(R.id.divider),
PhoneCallDetailsViews.fromView(view),
view.findViewById(R.id.call_log_item),
view.findViewById(R.id.call_log_header),
@@ -66,9 +70,11 @@
}
public static CallLogListItemViews createForTest(QuickContactBadge photoView,
- ImageView callView, ImageView playView, PhoneCallDetailsViews phoneCallDetailsViews,
- View standAloneItemView, View standAloneHeaderView, TextView standAloneHeaderTextView) {
- return new CallLogListItemViews(photoView, callView, playView, phoneCallDetailsViews,
- standAloneItemView, standAloneHeaderView, standAloneHeaderTextView);
+ ImageView callView, ImageView playView, View dividerView,
+ PhoneCallDetailsViews phoneCallDetailsViews, View standAloneItemView,
+ View standAloneHeaderView, TextView standAloneHeaderTextView) {
+ return new CallLogListItemViews(photoView, callView, playView, dividerView,
+ phoneCallDetailsViews, standAloneItemView, standAloneHeaderView,
+ standAloneHeaderTextView);
}
}
diff --git a/src/com/android/contacts/calllog/PhoneNumberHelper.java b/src/com/android/contacts/calllog/PhoneNumberHelper.java
index c898a09..ba24021 100644
--- a/src/com/android/contacts/calllog/PhoneNumberHelper.java
+++ b/src/com/android/contacts/calllog/PhoneNumberHelper.java
@@ -111,7 +111,7 @@
* Returns a structured phone number from the given text representation, or null if the number
* cannot be parsed.
*/
- public PhoneNumber parsePhoneNumber(String number, String countryIso) {
+ private PhoneNumber parsePhoneNumber(String number, String countryIso) {
try {
return mPhoneNumberUtil.parse(number, countryIso);
} catch (NumberParseException e) {
@@ -120,7 +120,8 @@
}
/** Returns the geocode associated with a phone number or the empty string if not available. */
- public String getGeocodeForNumber(PhoneNumber structuredPhoneNumber) {
+ public String getGeocodeForNumber(String number, String countryIso) {
+ PhoneNumber structuredPhoneNumber = parsePhoneNumber(number, countryIso);
if (structuredPhoneNumber != null) {
return mPhoneNumberOfflineGeocoder.getDescriptionForNumber(
structuredPhoneNumber, mResources.getConfiguration().locale);
diff --git a/src/com/android/contacts/list/ContactTileAdapter.java b/src/com/android/contacts/list/ContactTileAdapter.java
index 596cceb..9674960 100644
--- a/src/com/android/contacts/list/ContactTileAdapter.java
+++ b/src/com/android/contacts/list/ContactTileAdapter.java
@@ -202,7 +202,7 @@
@Override
public int getCount() {
if (mContactCursor == null || mContactCursor.getCount() == 0) {
- return 0;
+ return 0;
}
switch (mDisplayType) {
@@ -304,7 +304,7 @@
int itemViewType = getItemViewType(position);
if (itemViewType == ViewTypes.DIVIDER) {
// Checking For Divider First so not to cast convertView
- return convertView == null ? createDivider() : convertView;
+ return convertView == null ? getDivider() : convertView;
}
ContactTileRow contactTileRowView = (ContactTileRow) convertView;
@@ -322,7 +322,7 @@
* Divider uses a list_seperator.xml along with text to denote
* the most frequently contacted contacts.
*/
- private View createDivider() {
+ private View getDivider() {
View dividerView = View.inflate(mContext, R.layout.list_separator, null);
dividerView.setFocusable(false);
TextView text = (TextView) dividerView.findViewById(R.id.header_text);
diff --git a/src/com/android/contacts/list/ContactTileListFragment.java b/src/com/android/contacts/list/ContactTileListFragment.java
index f4bd1b5..875d5d2 100644
--- a/src/com/android/contacts/list/ContactTileListFragment.java
+++ b/src/com/android/contacts/list/ContactTileListFragment.java
@@ -34,6 +34,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
+import android.widget.TextView;
/**
* Fragment containing a list of starred contacts followed by a list of frequently contacted.
@@ -49,8 +50,9 @@
private Listener mListener;
private ContactTileAdapter mAdapter;
+ private DisplayType mDisplayType;
+ private TextView mEmptyView;
private ListView mListView;
- private DisplayType mDisplayType = DisplayType.STREQUENT_PHONE_ONLY;
@Override
public void onAttach(Activity activity) {
@@ -67,11 +69,15 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.contact_tile_list, container, false);
- mListView = (ListView) v.findViewById(R.id.contact_tile_list);
+ View listLayout = inflater.inflate(R.layout.contact_tile_list, container, false);
+
+ mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty);
+ mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list);
+
mListView.setItemsCanFocus(true);
mListView.setAdapter(mAdapter);
- return v;
+
+ return listLayout;
}
@Override
@@ -117,12 +123,32 @@
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.setContactCursor(data);
+ mEmptyView.setText(getEmptyStateText());
+ mListView.setEmptyView(mEmptyView);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {}
};
+ private String getEmptyStateText() {
+ String emptyText;
+ switch (mDisplayType) {
+ case STREQUENT:
+ case STREQUENT_PHONE_ONLY:
+ case STARRED_ONLY:
+ emptyText = getString(R.string.listTotalAllContactsZeroStarred);
+ break;
+ case FREQUENT_ONLY:
+ case GROUP_MEMBERS:
+ emptyText = getString(R.string.noContacts);
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
+ }
+ return emptyText;
+ }
+
public void setListener(Listener listener) {
mListener = listener;
}
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/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
index 328360b..436f13b 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -16,21 +16,23 @@
package com.android.contacts.voicemail;
+import com.android.contacts.R;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.VariableSpeed;
+
import android.app.Fragment;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
-
-import com.android.contacts.R;
-import com.android.ex.variablespeed.MediaPlayerProxy;
-import com.android.ex.variablespeed.VariableSpeed;
+import android.widget.Toast;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -50,6 +52,7 @@
*/
@NotThreadSafe
public class VoicemailPlaybackFragment extends Fragment {
+ private static final String TAG = "VoicemailPlayback";
private static final int NUMBER_OF_THREADS_IN_POOL = 2;
private VoicemailPlaybackPresenter mPresenter;
@@ -217,10 +220,14 @@
}
@Override
- public void playbackError() {
+ public void playbackError(Exception e) {
+ mRateIncreaseButton.setEnabled(false);
+ mRateDecreaseButton.setEnabled(false);
mStartStopButton.setEnabled(false);
mPlaybackSeek.setProgress(0);
mPlaybackSeek.setEnabled(false);
+ Toast.makeText(getActivity(), R.string.voicemail_playback_error, Toast.LENGTH_SHORT);
+ Log.e(TAG, "Could not play voicemail", e);
}
@Override
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
index 11764c9..53e64e9 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -16,6 +16,9 @@
package com.android.contacts.voicemail;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
+
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
@@ -23,9 +26,6 @@
import android.view.View;
import android.widget.SeekBar;
-import com.android.ex.variablespeed.MediaPlayerProxy;
-import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
-
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -61,7 +61,7 @@
int getDesiredClipPosition();
void playbackStarted();
void playbackStopped();
- void playbackError();
+ void playbackError(Exception e);
boolean isSpeakerPhoneOn();
void setSpeakerPhoneOn(boolean on);
void finish();
@@ -216,7 +216,7 @@
}
private void handleError(Exception e) {
- mView.playbackError();
+ mView.playbackError(e);
mPlayer.release();
mPositionUpdater.stopUpdating();
}
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 df74655..4d4d10b 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -97,6 +97,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/CallDetailActivityTest.java b/tests/src/com/android/contacts/CallDetailActivityTest.java
new file mode 100644
index 0000000..a36216e
--- /dev/null
+++ b/tests/src/com/android/contacts/CallDetailActivityTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import com.android.contacts.util.IntegrationTestUtils;
+import com.google.common.base.Preconditions;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * Unit tests for the {@link CallDetailActivity}.
+ */
+public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<CallDetailActivity> {
+ private static final String FAKE_VOICEMAIL_URI_STRING = "content://fake_uri";
+ private Uri mUri;
+ private IntegrationTestUtils mTestUtils;
+
+ public CallDetailActivityTest() {
+ super(CallDetailActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ // I don't like the default of focus-mode for tests, the green focus border makes the
+ // screenshots look weak.
+ setActivityInitialTouchMode(true);
+ mTestUtils = new IntegrationTestUtils(getInstrumentation());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ cleanUpUri();
+ mTestUtils = null;
+ super.tearDown();
+ }
+
+ /**
+ * Test for bug where increase rate button with invalid voicemail causes a crash.
+ * <p>
+ * The repro steps for this crash were to open a voicemail that does not have an attachment,
+ * then click the play button (which just reported an error), then after that try to adjust the
+ * rate.
+ */
+ public void testClickIncreaseRateButtonWithInvalidVoicemailDoesNotCrash() throws Throwable {
+ setActivityIntentForTestVoicemailEntry();
+ Activity activity = getActivity();
+ mTestUtils.clickButton(activity, R.id.playback_start_stop);
+ mTestUtils.clickButton(activity, R.id.rate_increase_button);
+ }
+
+ /** Test for bug where missing Extras on intent used to start Activity causes NPE. */
+ public void testCallLogUriWithMissingExtrasShouldNotCauseNPE() throws Exception {
+ setActivityIntentForTestCallEntry();
+ getActivity();
+ }
+
+ private void setActivityIntentForTestCallEntry() {
+ createTestCallEntry(false);
+ setActivityIntent(new Intent(Intent.ACTION_VIEW, mUri));
+ }
+
+ private void setActivityIntentForTestVoicemailEntry() {
+ createTestCallEntry(true);
+ Intent intent = new Intent(Intent.ACTION_VIEW, mUri);
+ Uri voicemailUri = Uri.parse(FAKE_VOICEMAIL_URI_STRING);
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, voicemailUri);
+ setActivityIntent(intent);
+ }
+
+ /** Inserts an entry into the call log. */
+ private void createTestCallEntry(boolean isVoicemail) {
+ Preconditions.checkState(mUri == null, "mUri should be null");
+ ContentResolver contentResolver = getContentResolver();
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(CallLog.Calls.NUMBER, "01234567890");
+ if (isVoicemail) {
+ contentValues.put(CallLog.Calls.TYPE, CallLog.Calls.VOICEMAIL_TYPE);
+ contentValues.put(CallLog.Calls.VOICEMAIL_URI, FAKE_VOICEMAIL_URI_STRING);
+ } else {
+ contentValues.put(CallLog.Calls.TYPE, CallLog.Calls.INCOMING_TYPE);
+ }
+ contentValues.put(CallLog.Calls.VOICEMAIL_URI, FAKE_VOICEMAIL_URI_STRING);
+ mUri = contentResolver.insert(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, contentValues);
+ }
+
+ private void cleanUpUri() {
+ if (mUri != null) {
+ getContentResolver().delete(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+ "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mUri)) });
+ mUri = null;
+ }
+ }
+
+ private ContentResolver getContentResolver() {
+ return getInstrumentation().getTargetContext().getContentResolver();
+ }
+}
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/calllog/CallLogListItemHelperTest.java b/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
index c56041e..626d7c6 100644
--- a/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
@@ -71,7 +71,7 @@
resources, callTypeHelper, mPhoneNumberHelper);
mHelper = new CallLogListItemHelper(phoneCallDetailsHelper, mPhoneNumberHelper);
mViews = CallLogListItemViews.createForTest(new QuickContactBadge(context),
- new ImageView(context), new ImageView(context),
+ new ImageView(context), new ImageView(context), new View(context),
PhoneCallDetailsViews.createForTest(new TextView(context),
new LinearLayout(context), new TextView(context), new TextView(context),
new TextView(context), new TextView(context)),
@@ -93,20 +93,17 @@
public void testSetPhoneCallDetails_Unknown() {
setPhoneCallDetailsWithNumber(CallerInfo.UNKNOWN_NUMBER, CallerInfo.UNKNOWN_NUMBER);
- assertEquals(View.INVISIBLE, mViews.callView.getVisibility());
- assertEquals(View.GONE, mViews.playView.getVisibility());
+ assertNoCallButton();
}
public void testSetPhoneCallDetails_Private() {
setPhoneCallDetailsWithNumber(CallerInfo.PRIVATE_NUMBER, CallerInfo.PRIVATE_NUMBER);
- assertEquals(View.INVISIBLE, mViews.callView.getVisibility());
- assertEquals(View.GONE, mViews.playView.getVisibility());
+ assertNoCallButton();
}
public void testSetPhoneCallDetails_Payphone() {
setPhoneCallDetailsWithNumber(CallerInfo.PAYPHONE_NUMBER, CallerInfo.PAYPHONE_NUMBER);
- assertEquals(View.INVISIBLE, mViews.callView.getVisibility());
- assertEquals(View.GONE, mViews.playView.getVisibility());
+ assertNoCallButton();
}
public void testSetPhoneCallDetails_VoicemailNumber() {
@@ -121,11 +118,37 @@
assertEquals(View.VISIBLE, mViews.playView.getVisibility());
}
+ public void testSetPhoneCallDetails_VoicemailFromUnknown() {
+ setPhoneCallDetailsWithNumberAndType(CallerInfo.UNKNOWN_NUMBER, CallerInfo.UNKNOWN_NUMBER,
+ Calls.VOICEMAIL_TYPE);
+ assertEquals(View.VISIBLE, mViews.playView.getVisibility());
+ assertEmptyCallButton();
+ }
+
+ /** Asserts that the whole call area is gone. */
+ private void assertNoCallButton() {
+ assertEquals(View.GONE, mViews.callView.getVisibility());
+ assertEquals(View.GONE, mViews.playView.getVisibility());
+ assertEquals(View.GONE, mViews.dividerView.getVisibility());
+ }
+
+ /** Asserts that the call area is present but empty. */
+ private void assertEmptyCallButton() {
+ assertEquals(View.INVISIBLE, mViews.callView.getVisibility());
+ assertEquals(View.VISIBLE, mViews.dividerView.getVisibility());
+ }
+
/** Sets the details of a phone call using the specified phone number. */
private void setPhoneCallDetailsWithNumber(String number, String formattedNumber) {
+ setPhoneCallDetailsWithNumberAndType(number, formattedNumber, Calls.INCOMING_TYPE);
+ }
+
+ /** Sets the details of a phone call using the specified phone number. */
+ private void setPhoneCallDetailsWithNumberAndType(String number, String formattedNumber,
+ int callType) {
mHelper.setPhoneCallDetails(mViews,
new PhoneCallDetails(number, formattedNumber, TEST_COUNTRY_ISO,
- new int[]{ Calls.INCOMING_TYPE }, TEST_DATE, TEST_DURATION),
+ new int[]{ callType }, TEST_DATE, TEST_DURATION),
true, false);
}
diff --git a/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java b/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
index c1eefc0..fc3545b 100644
--- a/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
+++ b/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
@@ -27,6 +27,7 @@
import com.android.contacts.tests.mocks.MockAccountTypeManager;
import com.android.contacts.tests.mocks.MockContentProvider;
import com.android.contacts.tests.mocks.MockContentProvider.Query;
+import com.android.contacts.util.IntegrationTestUtils;
import android.content.ContentUris;
import android.net.Uri;
@@ -65,6 +66,7 @@
private ContactsMockContext mContext;
private MockContentProvider mContactsProvider;
private ContactDeletionInteraction mFragment;
+ private IntegrationTestUtils mUtils;
public ContactDeletionInteractionTest() {
super(FragmentTestActivity.class);
@@ -73,6 +75,10 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ // This test requires that the screen be turned on.
+ mUtils = new IntegrationTestUtils(getInstrumentation());
+ mUtils.acquireScreenWakeLock(getInstrumentation().getTargetContext());
+
mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
InjectedServices services = new InjectedServices();
services.setContentResolver(mContext.getContentResolver());
@@ -94,6 +100,7 @@
@Override
protected void tearDown() throws Exception {
ContactsApplication.injectServices(null);
+ mUtils.releaseScreenWakeLock();
super.tearDown();
}
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;
+ }
+}
diff --git a/tests/src/com/android/contacts/util/IntegrationTestUtils.java b/tests/src/com/android/contacts/util/IntegrationTestUtils.java
new file mode 100644
index 0000000..45dc981
--- /dev/null
+++ b/tests/src/com/android/contacts/util/IntegrationTestUtils.java
@@ -0,0 +1,121 @@
+/*
+ * 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.util;
+
+import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP;
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+
+import com.google.common.base.Preconditions;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.PowerManager;
+import android.view.View;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Some utility methods for making integration testing smoother. */
+@ThreadSafe
+public class IntegrationTestUtils {
+ private static final String TAG = "IntegrationTestUtils";
+
+ private final Instrumentation mInstrumentation;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock") private PowerManager.WakeLock mWakeLock;
+
+ public IntegrationTestUtils(Instrumentation instrumentation) {
+ mInstrumentation = instrumentation;
+ }
+
+ /**
+ * Find a view by a given resource id, from the given activity, and click it, iff it is
+ * enabled according to {@link View#isEnabled()}.
+ */
+ public void clickButton(final Activity activity, final int buttonResourceId) throws Throwable {
+ runOnUiThreadAndGetTheResult(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ View view = activity.findViewById(buttonResourceId);
+ Assert.assertNotNull(view);
+ if (view.isEnabled()) {
+ view.performClick();
+ }
+ return null;
+ }
+ });
+ }
+
+ // TODO: Move this class and the appropriate documentation into a test library, having checked
+ // first to see if exactly this code already exists or not.
+ /**
+ * Execute a callable on the ui thread, returning its result synchronously.
+ * <p>
+ * Waits for an idle sync on the main thread (see {@link Instrumentation#waitForIdle(Runnable)})
+ * before executing this callable.
+ */
+ private <T> T runOnUiThreadAndGetTheResult(Callable<T> callable) throws Throwable {
+ FutureTask<T> future = new FutureTask<T>(callable);
+ mInstrumentation.waitForIdle(future);
+ try {
+ return future.get();
+ } catch (ExecutionException e) {
+ // Unwrap the cause of the exception and re-throw it.
+ throw e.getCause();
+ }
+ }
+
+ /**
+ * Wake up the screen, useful in tests that want or need the screen to be on.
+ * <p>
+ * This is usually called from setUp() for tests that require it. After calling this method,
+ * {@link #releaseScreenWakeLock()} must be called, this is usually done from tearDown().
+ */
+ public void acquireScreenWakeLock(Context context) {
+ synchronized (mLock) {
+ Preconditions.checkState(mWakeLock == null, "mWakeLock was already held");
+ mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE))
+ .newWakeLock(ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASE | FULL_WAKE_LOCK, TAG);
+ mWakeLock.acquire();
+ }
+ }
+
+ /** Release the wake lock previously acquired with {@link #acquireScreenWakeLock(Context)}. */
+ public void releaseScreenWakeLock() {
+ synchronized (mLock) {
+ // We don't use Preconditions to force you to have acquired before release.
+ // This is because we don't want unnecessary exceptions in tearDown() since they'll
+ // typically mask the actual exception that happened during the test.
+ // The other reason is that this method is most likely to be called from tearDown(),
+ // which is invoked within a finally block, so it's not infrequently the case that
+ // the setUp() method fails before getting the lock, at which point we don't want
+ // to fail in tearDown().
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+ }
+}