Tolerate crashes during re-aggregation.

This CL consists of two parts:
- Don't crash even if the name_lookup table contains malformed data.  Just
treat it as name-unmatch.

- Don't crash even if an exception is thrown durign re-aggregation.
Re-aggregation is not *that* important anyway, so in that case just bump the
logic version and start the provider normally.  (And even if we fail to bump
the version number, just go ahead and continue.)

Bug 6827136
Change-Id: Ifa3d4697c5d81f3480e0b8a9238b49312ac75e3b
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index c321489..26aac63 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -7954,40 +7954,66 @@
             int count = 0;
             SQLiteDatabase db = null;
             boolean success = false;
+            boolean transactionStarted = false;
             try {
-                // Re-aggregation os only for the contacts DB.
+                // Re-aggregation is only for the contacts DB.
                 switchToContactMode();
                 db = mContactsHelper.getWritableDatabase();
                 mActiveDb.set(db);
 
                 // Start the actual process.
                 db.beginTransaction();
+                transactionStarted = true;
 
                 count = mContactAggregator.markAllVisibleForAggregation(db);
                 mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db);
 
                 updateSearchIndexInTransaction();
 
-                mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM,
-                        String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
+                updateAggregationAlgorithmVersion();
 
                 db.setTransactionSuccessful();
 
                 success = true;
             } finally {
                 mTransactionContext.get().clearAll();
-                if (db != null) {
+                if (transactionStarted) {
                     db.endTransaction();
                 }
                 final long end = SystemClock.elapsedRealtime();
                 Log.i(TAG, "Aggregation algorithm upgraded for " + count + " raw contacts"
                         + (success ? (" in " + (end - start) + "ms") : " failed"));
             }
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to upgrade aggregation algorithm; continuing anyway.", e);
+
+            // Got some exception during re-aggregation.  Re-aggregation isn't that important, so
+            // just bump the aggregation algorithm version and let the provider start normally.
+            try {
+                final SQLiteDatabase db =  mContactsHelper.getWritableDatabase();
+                db.beginTransaction();
+                try {
+                    updateAggregationAlgorithmVersion();
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            } catch (RuntimeException e2) {
+                // Couldn't even update the algorithm version...  There's really nothing we can do
+                // here, so just go ahead and start the provider.  Next time the provider starts
+                // it'll try re-aggregation again, which may or may not succeed.
+                Log.e(TAG, "Failed to bump aggregation algorithm version; continuing anyway.", e2);
+            }
         } finally { // Need one more finally because endTransaction() may fail.
             setProviderStatus(ProviderStatus.STATUS_NORMAL);
         }
     }
 
+    private void updateAggregationAlgorithmVersion() {
+        mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM,
+                String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
+    }
+
     @VisibleForTesting
     boolean isPhone() {
         if (!mIsPhoneInitialized) {
diff --git a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
index a29735d..2e552e9 100644
--- a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
+++ b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
@@ -18,6 +18,8 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
 import com.android.providers.contacts.util.Hex;
 
+import android.util.Log;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -27,6 +29,7 @@
  * Logic for matching contacts' data and accumulating match scores.
  */
 public class ContactMatcher {
+    private static final String TAG = "ContactMatcher";
 
     // Best possible match score
     public static final int MAX_SCORE = 100;
@@ -296,8 +299,16 @@
             return;
         }
 
-        byte[] decodedCandidateName = Hex.decodeHex(candidateName);
-        byte[] decodedName = Hex.decodeHex(name);
+        final byte[] decodedCandidateName;
+        final byte[] decodedName;
+        try {
+            decodedCandidateName = Hex.decodeHex(candidateName);
+            decodedName = Hex.decodeHex(name);
+        } catch (RuntimeException e) {
+            // How could this happen??  See bug 6827136
+            Log.e(TAG, "Failed to decode normalized name.  Skipping.", e);
+            return;
+        }
 
         NameDistance nameDistance = algorithm == MATCHING_ALGORITHM_CONSERVATIVE ?
                 mNameDistanceConservative : mNameDistanceApproximate;
diff --git a/src/com/android/providers/contacts/util/Hex.java b/src/com/android/providers/contacts/util/Hex.java
index ad26f4b..c3e5e74 100644
--- a/src/com/android/providers/contacts/util/Hex.java
+++ b/src/com/android/providers/contacts/util/Hex.java
@@ -74,12 +74,16 @@
 
     /**
      * Quickly converts a hexadecimal string to a byte array.
+     *
+     * TODO Use checked exceptions instead of RuntimeException.  Apparently normalized names *may*
+     * contain non-hex strings and we want to make sure the provider won't crash even with such
+     * input.
      */
     public static byte[] decodeHex(String hexString) {
         int length = hexString.length();
 
         if ((length & 0x01) != 0) {
-            throw new IllegalArgumentException("Odd number of characters.");
+            throw new IllegalArgumentException("Odd number of characters: " + hexString);
         }
 
         boolean badHex = false;
diff --git a/tests/src/com/android/providers/contacts/aggregation/util/ContactMatcherTest.java b/tests/src/com/android/providers/contacts/aggregation/util/ContactMatcherTest.java
new file mode 100644
index 0000000..97faacd
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/aggregation/util/ContactMatcherTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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.providers.contacts.aggregation.util;
+
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+
+import android.test.AndroidTestCase;
+
+public class ContactMatcherTest extends AndroidTestCase {
+
+    public void testMatchName_invalidHexDecimal() {
+        final ContactMatcher matcher = new ContactMatcher();
+
+        // This shouldn't throw.  Bug 6827136
+        matcher.matchName(1, NameLookupType.NAME_COLLATION_KEY, "InvalidHex",
+                NameLookupType.NAME_COLLATION_KEY, "InvalidHex2",
+                ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE);
+    }
+}