Merge "Fix NPE in setGroupMetaData" into klp-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0d25bb2..7cd8227 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -330,6 +330,11 @@
          was found that could perform the selected action. [CHAR LIMIT=NONE] -->
     <string name="quickcontact_missing_app">No app was found to handle this action.</string>
 
+    <!-- Shown as a toast when the user attempts an action (add contact, edit
+         contact, etc) and no application was found that could perform that
+         action. [CHAR LIMIT=NONE] -->
+    <string name="missing_app">No app was found to handle this action.</string>
+
     <!-- The menu item to share the currently viewed contact [CHAR LIMIT=30] -->
     <string name="menu_share">Share</string>
 
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 7a8f9f3..ff76844 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -410,6 +410,12 @@
                 Log.e(TAG, "Problem persisting user edits", e);
                 break;
 
+            } catch (IllegalArgumentException e) {
+                // This is thrown by applyBatch on malformed requests
+                Log.e(TAG, "Problem persisting user edits", e);
+                showToast(R.string.contactSavedErrorToast);
+                break;
+
             } catch (OperationApplicationException e) {
                 // Version consistency failed, re-parent change and try again
                 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
diff --git a/src/com/android/contacts/SplitAggregateView.java b/src/com/android/contacts/SplitAggregateView.java
index 6e38549..2281ec6 100644
--- a/src/com/android/contacts/SplitAggregateView.java
+++ b/src/com/android/contacts/SplitAggregateView.java
@@ -157,6 +157,9 @@
         Uri dataUri = Uri.withAppendedPath(mAggregateUri, Data.CONTENT_DIRECTORY);
         Cursor cursor = getContext().getContentResolver().query(dataUri,
                 SplitQuery.COLUMNS, null, null, null);
+        if (cursor == null) {
+            return Collections.emptyList();
+        }
         try {
             while (cursor.moveToNext()) {
                 long rawContactId = cursor.getLong(SplitQuery.RAW_CONTACT_ID);
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index 2d9e9f5..6a55c30 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -69,6 +69,8 @@
 
     // Height and width (in pixels) to request for the photo - queried from the provider.
     private static int mPhotoDim;
+    // Default photo dimension to use if unable to query the provider.
+    private static final int mDefaultPhotoDim = 720;
 
     private Uri mContactUri;
 
@@ -91,14 +93,20 @@
 
         mContentResolver = getContentResolver();
 
-        // Load the photo dimension to request.
-        Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
-                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
-        try {
-            c.moveToFirst();
-            mPhotoDim = c.getInt(0);
-        } finally {
-            c.close();
+        // Load the photo dimension to request. mPhotoDim is a static class
+        // member varible so only need to load this if this is the first time
+        // through.
+        if (mPhotoDim == 0) {
+            Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+                    new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
+            if (c != null) {
+                try {
+                    c.moveToFirst();
+                    mPhotoDim = c.getInt(0);
+                } finally {
+                    c.close();
+                }
+            }
         }
     }
 
@@ -128,28 +136,20 @@
             final Intent myIntent = getIntent();
             final Uri inputUri = myIntent.getData();
 
-            final int perm = checkUriPermission(inputUri, android.os.Process.myPid(),
-                    android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION |
-                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-
             final Uri toCrop;
-
-            if (perm == PackageManager.PERMISSION_DENIED) {
-                // Work around to save a read-only URI into a temporary file provider URI so that
-                // we can add the FLAG_GRANT_WRITE_URI_PERMISSION flag to the eventual
-                // crop intent b/10837468
-                ContactPhotoUtils.savePhotoFromUriToUri(this, inputUri, mTempPhotoUri, false);
-                toCrop = mTempPhotoUri;
-            } else {
-                toCrop = inputUri;
-            }
+            // Save the URI into a temporary file provider URI so that
+            // we can add the FLAG_GRANT_WRITE_URI_PERMISSION flag to the eventual
+            // crop intent for read-only URI's.
+            // TODO: With b/10837468 fixed should be able to avoid this copy.
+            ContactPhotoUtils.savePhotoFromUriToUri(this, inputUri, mTempPhotoUri, false);
+            toCrop = mTempPhotoUri;
 
             final Intent intent = new Intent("com.android.camera.action.CROP", toCrop);
             if (myIntent.getStringExtra("mimeType") != null) {
                 intent.setDataAndType(toCrop, myIntent.getStringExtra("mimeType"));
             }
             ContactPhotoUtils.addPhotoPickerExtras(intent, mCroppedPhotoUri);
-            ContactPhotoUtils.addCropExtras(intent, mPhotoDim);
+            ContactPhotoUtils.addCropExtras(intent, mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim);
 
             startActivityForResult(intent, REQUEST_CROP_PHOTO);
 
@@ -223,6 +223,10 @@
             Log.w(TAG, "Could not find bitmap");
             return;
         }
+        if (bitmap == null) {
+            Log.w(TAG, "Could not decode bitmap");
+            return;
+        }
 
         final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false);
         final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index d60cc73..3f9116f 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -683,12 +683,15 @@
             // Skip kind that are not editable
             if (!kind.editable) continue;
             if (mMimetype.equals(kind.mimeType)) {
-                for (ValuesDelta valuesDelta : mRawContactDelta.getMimeEntries(mMimetype)) {
-                    // Skip entries that aren't visible
-                    if (!valuesDelta.isVisible()) continue;
-                    if (valuesDelta.isInsert()) {
-                        inflateEditorView(kind, valuesDelta, mRawContactDelta);
-                        return;
+                final ArrayList<ValuesDelta> deltas = mRawContactDelta.getMimeEntries(mMimetype);
+                if (deltas != null) {
+                    for (ValuesDelta valuesDelta : deltas) {
+                        // Skip entries that aren't visible
+                        if (!valuesDelta.isVisible()) continue;
+                        if (valuesDelta.isInsert()) {
+                            inflateEditorView(kind, valuesDelta, mRawContactDelta);
+                            return;
+                        }
                     }
                 }
             }
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 03821a7..a4e0470 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -105,8 +105,6 @@
                     | ActionBar.DISPLAY_SHOW_HOME);
             actionBar.setTitle("");
         }
-
-        Log.i(TAG, getIntent().getData().toString());
     }
 
     @Override
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index 1eb610a..9bb7395 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -20,6 +20,7 @@
 import android.app.ActionBar.LayoutParams;
 import android.app.Activity;
 import android.app.Fragment;
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -39,6 +40,7 @@
 import android.widget.SearchView;
 import android.widget.SearchView.OnCloseListener;
 import android.widget.SearchView.OnQueryTextListener;
+import android.widget.Toast;
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
@@ -317,6 +319,7 @@
                 break;
             }
 
+            case ContactsRequest.ACTION_DEFAULT:
             case ContactsRequest.ACTION_PICK_CONTACT: {
                 ContactPickerFragment fragment = new ContactPickerFragment();
                 fragment.setIncludeProfile(mRequest.shouldIncludeProfile());
@@ -538,7 +541,13 @@
         if (extras != null) {
             intent.putExtras(extras);
         }
-        startActivity(intent);
+        try {
+            startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Log.e(TAG, "startActivity() failed: " + e);
+            Toast.makeText(ContactSelectionActivity.this, R.string.missing_app,
+                    Toast.LENGTH_SHORT).show();
+        }
         finish();
     }
 
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index abaa8eb..629e36b 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -2066,6 +2066,7 @@
                     GroupMembership.CONTENT_ITEM_TYPE);
             final ValuesDelta entry = RawContactModifier.insertChild(rawContactEntityDelta,
                     groupMembershipKind);
+            if (entry == null) return;
             entry.setGroupRowId(defaultGroupId);
 
             // and fire off the intent. we don't need a callback, as the database listener
diff --git a/src/com/android/contacts/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
index 2f77858..f121605 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
@@ -300,6 +300,9 @@
     private void loadAggregationSuggestions(Uri uri) {
         ContentResolver contentResolver = mContext.getContentResolver();
         Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null);
+        if (cursor == null) {
+            return;
+        }
         try {
             // If a new request is pending, chuck the result of the previous request
             if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) {
@@ -324,7 +327,9 @@
 
             Cursor dataCursor = contentResolver.query(Data.CONTENT_URI,
                     DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID);
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
+            if (dataCursor != null) {
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
+            }
         } finally {
             cursor.close();
         }
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 9243625..54c9d3b 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -1392,14 +1392,14 @@
             String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET);
             final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2);
 
-            // Check read-only
+            // Check read-only. Sort read/write before read-only.
             if (!type1.areContactsWritable() && type2.areContactsWritable()) {
                 return 1;
             } else if (type1.areContactsWritable() && !type2.areContactsWritable()) {
                 return -1;
             }
 
-            // Check account type
+            // Check account type. Sort Google before non-Google.
             boolean skipAccountTypeCheck = false;
             boolean isGoogleAccount1 = type1 instanceof GoogleAccountType;
             boolean isGoogleAccount2 = type2 instanceof GoogleAccountType;
@@ -1413,21 +1413,32 @@
 
             int value;
             if (!skipAccountTypeCheck) {
-                if (type1.accountType == null) {
+                // Sort accounts with type before accounts without types.
+                if (type1.accountType != null && type2.accountType == null) {
+                    return -1;
+                } else if (type1.accountType == null && type2.accountType != null) {
                     return 1;
                 }
-                value = type1.accountType.compareTo(type2.accountType);
-                if (value != 0) {
-                    return value;
-                } else {
-                    // Fall back to data set.
-                    if (type1.dataSet != null) {
-                        value = type1.dataSet.compareTo(type2.dataSet);
-                        if (value != 0) {
-                            return value;
-                        }
-                    } else if (type2.dataSet != null) {
-                        return 1;
+
+                if (type1.accountType != null && type2.accountType != null) {
+                    value = type1.accountType.compareTo(type2.accountType);
+                    if (value != 0) {
+                        return value;
+                    }
+                }
+
+                // Fall back to data set. Sort accounts with data sets before
+                // those without.
+                if (type1.dataSet != null && type2.dataSet == null) {
+                    return -1;
+                } else if (type1.dataSet == null && type2.dataSet != null) {
+                    return 1;
+                }
+
+                if (type1.dataSet != null && type2.dataSet != null) {
+                    value = type1.dataSet.compareTo(type2.dataSet);
+                    if (value != 0) {
+                        return value;
                     }
                 }
             }
diff --git a/src/com/android/contacts/editor/GroupMembershipView.java b/src/com/android/contacts/editor/GroupMembershipView.java
index fec0aba..bcea53d 100644
--- a/src/com/android/contacts/editor/GroupMembershipView.java
+++ b/src/com/android/contacts/editor/GroupMembershipView.java
@@ -370,7 +370,9 @@
             long groupId = item.getGroupId();
             if (item.isChecked() && !hasMembership(groupId)) {
                 ValuesDelta entry = RawContactModifier.insertChild(mState, mKind);
-                entry.setGroupRowId(groupId);
+                if (entry != null) {
+                    entry.setGroupRowId(groupId);
+                }
             }
         }
 
diff --git a/src/com/android/contacts/editor/RawContactEditorView.java b/src/com/android/contacts/editor/RawContactEditorView.java
index c9d3c33..7fcdb7d 100644
--- a/src/com/android/contacts/editor/RawContactEditorView.java
+++ b/src/com/android/contacts/editor/RawContactEditorView.java
@@ -391,7 +391,9 @@
             long defaultGroupId = getDefaultGroupId();
             if (defaultGroupId != -1) {
                 ValuesDelta entry = RawContactModifier.insertChild(mState, mGroupMembershipKind);
-                entry.setGroupRowId(defaultGroupId);
+                if (entry != null) {
+                    entry.setGroupRowId(defaultGroupId);
+                }
             }
         }
     }
diff --git a/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java b/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
index d1fa282..36e96a2 100644
--- a/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
+++ b/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
@@ -199,39 +199,43 @@
         // Phones
         ArrayList<ValuesDelta> phones = state.getMimeEntries(Phone.CONTENT_ITEM_TYPE);
         if (phones != null) {
-            for (int i = 0; i < phones.size(); i++) {
-                ValuesDelta phone = phones.get(i);
-                final String phoneNumber = PhoneNumberUtils.formatNumber(
-                        phone.getPhoneNumber(),
-                        phone.getPhoneNormalizedNumber(),
+            boolean isFirstPhoneBound = true;
+            for (ValuesDelta phone : phones) {
+                final String phoneNumber = phone.getPhoneNumber();
+                if (TextUtils.isEmpty(phoneNumber)) {
+                    continue;
+                }
+                final String formattedNumber = PhoneNumberUtils.formatNumber(
+                        phoneNumber, phone.getPhoneNormalizedNumber(),
                         GeoUtil.getCurrentCountryIso(getContext()));
-                final CharSequence phoneType;
+                CharSequence phoneType = null;
                 if (phone.phoneHasType()) {
                     phoneType = Phone.getTypeLabel(
                             res, phone.getPhoneType(), phone.getPhoneLabel());
-                } else {
-                    phoneType = null;
                 }
-                bindData(mContext.getText(R.string.phoneLabelsGroup), phoneNumber, phoneType,
-                        i == 0, true);
+                bindData(mContext.getText(R.string.phoneLabelsGroup), formattedNumber,
+                        phoneType, isFirstPhoneBound, true);
+                isFirstPhoneBound = false;
             }
         }
 
         // Emails
         ArrayList<ValuesDelta> emails = state.getMimeEntries(Email.CONTENT_ITEM_TYPE);
         if (emails != null) {
-            for (int i = 0; i < emails.size(); i++) {
-                ValuesDelta email = emails.get(i);
+            boolean isFirstEmailBound = true;
+            for (ValuesDelta email : emails) {
                 final String emailAddress = email.getEmailData();
-                final CharSequence emailType;
+                if (TextUtils.isEmpty(emailAddress)) {
+                    continue;
+                }
+                CharSequence emailType = null;
                 if (email.emailHasType()) {
                     emailType = Email.getTypeLabel(
                             res, email.getEmailType(), email.getEmailLabel());
-                } else {
-                    emailType = null;
                 }
                 bindData(mContext.getText(R.string.emailLabelsGroup), emailAddress, emailType,
-                        i == 0);
+                        isFirstEmailBound);
+                isFirstEmailBound = false;
             }
         }
 
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index f48e0a7..48751e7 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -197,7 +197,7 @@
     private void bindHeaderView(GroupListItem entry, GroupListItemViewCache viewCache) {
         AccountType accountType = mAccountTypeManager.getAccountType(
                 entry.getAccountType(), entry.getDataSet());
-        viewCache.accountType.setText(accountType.getDisplayLabel(mContext).toString());
+        viewCache.accountType.setText(accountType.getDisplayLabel(mContext));
         viewCache.accountName.setText(entry.getAccountName());
     }
 
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index 834e2c3..f2d70aa 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -319,12 +319,18 @@
         if (size == -1) {
             groupSizeString = null;
         } else {
-            String groupSizeTemplateString = getResources().getQuantityString(
-                    R.plurals.num_contacts_in_group, size);
             AccountType accountType = mAccountTypeManager.getAccountType(mAccountTypeString,
                     mDataSet);
-            groupSizeString = String.format(groupSizeTemplateString, size,
-                    accountType.getDisplayLabel(mContext));
+            final CharSequence dispLabel = accountType.getDisplayLabel(mContext);
+            if (!TextUtils.isEmpty(dispLabel)) {
+                String groupSizeTemplateString = getResources().getQuantityString(
+                        R.plurals.num_contacts_in_group, size);
+                groupSizeString = String.format(groupSizeTemplateString, size, dispLabel);
+            } else {
+                String groupSizeTemplateString = getResources().getQuantityString(
+                        R.plurals.group_list_num_contacts_in_group, size);
+                groupSizeString = String.format(groupSizeTemplateString, size);
+            }
         }
 
         if (mGroupSize != null) {
diff --git a/src/com/android/contacts/group/SuggestedMemberListAdapter.java b/src/com/android/contacts/group/SuggestedMemberListAdapter.java
index 067c052..6d60a3e 100644
--- a/src/com/android/contacts/group/SuggestedMemberListAdapter.java
+++ b/src/com/android/contacts/group/SuggestedMemberListAdapter.java
@@ -263,31 +263,33 @@
                     "=?) AND " + rawContactIdSelectionBuilder.toString(),
                     selectionArgs.toArray(new String[0]), null);
 
-            try {
-                memberDataCursor.moveToPosition(-1);
-                while (memberDataCursor.moveToNext()) {
-                    long rawContactId = memberDataCursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX);
-                    SuggestedMember member = suggestionsMap.get(rawContactId);
-                    if (member == null) {
-                        continue;
-                    }
-                    String mimetype = memberDataCursor.getString(MIMETYPE_COLUMN_INDEX);
-                    if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
-                        // Set photo
-                        byte[] bitmapArray = memberDataCursor.getBlob(PHOTO_COLUMN_INDEX);
-                        member.setPhotoByteArray(bitmapArray);
-                    } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype) ||
-                            Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
-                        // Set at most 1 extra piece of contact info that can be a phone number or
-                        // email
-                        if (!member.hasExtraInfo()) {
-                            String info = memberDataCursor.getString(DATA_COLUMN_INDEX);
-                            member.setExtraInfo(info);
+            if (memberDataCursor != null) {
+                try {
+                    memberDataCursor.moveToPosition(-1);
+                    while (memberDataCursor.moveToNext()) {
+                        long rawContactId = memberDataCursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX);
+                        SuggestedMember member = suggestionsMap.get(rawContactId);
+                        if (member == null) {
+                            continue;
+                        }
+                        String mimetype = memberDataCursor.getString(MIMETYPE_COLUMN_INDEX);
+                        if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                            // Set photo
+                            byte[] bitmapArray = memberDataCursor.getBlob(PHOTO_COLUMN_INDEX);
+                            member.setPhotoByteArray(bitmapArray);
+                        } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype) ||
+                                Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                            // Set at most 1 extra piece of contact info that can be a phone number or
+                            // email
+                            if (!member.hasExtraInfo()) {
+                                String info = memberDataCursor.getString(DATA_COLUMN_INDEX);
+                                member.setExtraInfo(info);
+                            }
                         }
                     }
+                } finally {
+                    memberDataCursor.close();
                 }
-            } finally {
-                memberDataCursor.close();
             }
             results.values = suggestionsList;
             return results;