Merge "Import revised translations."
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 13d2e6c..79e9ae5 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -287,7 +287,16 @@
             mCallerIdThread.start();
         }
 
+        /**
+         * Stops the background thread that processes updates and cancels any pending requests to
+         * start it.
+         * <p>
+         * Should be called from the main thread to prevent a race condition between the request to
+         * start the thread being processed and stopping the thread.
+         */
         public void stopRequestProcessing() {
+            // Remove any pending requests to start the processing thread.
+            mHandler.removeMessages(START_THREAD);
             mDone = true;
             if (mCallerIdThread != null) mCallerIdThread.interrupt();
         }
diff --git a/src/com/android/contacts/interactions/ContactDeletionInteraction.java b/src/com/android/contacts/interactions/ContactDeletionInteraction.java
index f195c2f..dfcd7b6 100644
--- a/src/com/android/contacts/interactions/ContactDeletionInteraction.java
+++ b/src/com/android/contacts/interactions/ContactDeletionInteraction.java
@@ -21,11 +21,13 @@
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
 import com.google.android.collect.Sets;
+import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Fragment;
 import android.app.FragmentManager;
+import android.app.LoaderManager;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Context;
 import android.content.CursorLoader;
@@ -37,6 +39,7 @@
 import android.os.Bundle;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Contacts.Entity;
+import android.util.Log;
 
 import java.util.HashSet;
 
@@ -69,14 +72,43 @@
     private Uri mContactUri;
     private boolean mFinishActivityWhenDone;
     private Context mContext;
-
     private AlertDialog mDialog;
 
-    // Visible for testing
+    /** This is a wrapper around the fragment's loader manager to be used only during testing. */
+    private TestLoaderManager mTestLoaderManager;
+
+    @VisibleForTesting
     int mMessageId;
 
+    /**
+     * Starts the interaction.
+     *
+     * @param activity the activity within which to start the interaction
+     * @param contactUri the URI of the contact to delete
+     * @param finishActivityWhenDone whether to finish the activity upon completion of the
+     *        interaction
+     * @return the newly created interaction
+     */
     public static ContactDeletionInteraction start(
             Activity activity, Uri contactUri, boolean finishActivityWhenDone) {
+        return startWithTestLoaderManager(activity, contactUri, finishActivityWhenDone, null);
+    }
+
+    /**
+     * Starts the interaction and optionally set up a {@link TestLoaderManager}.
+     *
+     * @param activity the activity within which to start the interaction
+     * @param contactUri the URI of the contact to delete
+     * @param finishActivityWhenDone whether to finish the activity upon completion of the
+     *        interaction
+     * @param testLoaderManager the {@link TestLoaderManager} to use to load the data, may be null
+     *        in which case the default {@link LoaderManager} is used
+     * @return the newly created interaction
+     */
+    @VisibleForTesting
+    static ContactDeletionInteraction startWithTestLoaderManager(
+            Activity activity, Uri contactUri, boolean finishActivityWhenDone,
+            TestLoaderManager testLoaderManager) {
         if (contactUri == null) {
             return null;
         }
@@ -86,10 +118,12 @@
                 (ContactDeletionInteraction) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
         if (fragment == null) {
             fragment = new ContactDeletionInteraction();
+            fragment.setTestLoaderManager(testLoaderManager);
             fragment.setContactUri(contactUri);
             fragment.setFinishActivityWhenDone(finishActivityWhenDone);
             fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG).commit();
         } else {
+            fragment.setTestLoaderManager(testLoaderManager);
             fragment.setContactUri(contactUri);
             fragment.setFinishActivityWhenDone(finishActivityWhenDone);
         }
@@ -97,6 +131,24 @@
     }
 
     @Override
+    public LoaderManager getLoaderManager() {
+        // Return the TestLoaderManager if one is set up.
+        LoaderManager loaderManager = super.getLoaderManager();
+        if (mTestLoaderManager != null) {
+            // Set the delegate: this operation is idempotent, so let's just do it every time.
+            mTestLoaderManager.setDelegate(loaderManager);
+            return mTestLoaderManager;
+        } else {
+            return loaderManager;
+        }
+    }
+
+    /** Sets the TestLoaderManager that is used to wrap the actual LoaderManager in tests. */
+    private void setTestLoaderManager(TestLoaderManager mockLoaderManager) {
+        mTestLoaderManager = mockLoaderManager;
+    }
+
+    @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
         mContext = activity;
diff --git a/src/com/android/contacts/interactions/TestLoaderManager.java b/src/com/android/contacts/interactions/TestLoaderManager.java
new file mode 100644
index 0000000..db975ed
--- /dev/null
+++ b/src/com/android/contacts/interactions/TestLoaderManager.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 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.interactions;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.content.AsyncTaskLoader;
+import android.content.Loader;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import junit.framework.Assert;
+
+/**
+ * A {@link LoaderManager} that records which loaders have been completed.
+ * <p>
+ * You should wrap the existing LoaderManager with an instance of this class, which will then
+ * delegate to the original object.
+ * <p>
+ * Typically, one would override {@link Activity#getLoaderManager()} to return the
+ * TestLoaderManager and ensuring it wraps the {@link LoaderManager} for this object, e.g.:
+ * <pre>
+ *   private TestLoaderManager mTestLoaderManager;
+ *
+ *   public LoaderManager getLoaderManager() {
+ *     LoaderManager loaderManager = super.getLoaderManager();
+ *     if (mTestLoaderManager != null) {
+ *       mTestLoaderManager.setDelegate(loaderManager);
+ *       return mTestLoaderManager;
+ *     } else {
+ *       return loaderManager;
+ *     }
+ *   }
+ *
+ *   void setTestLoaderManager(TestLoaderManager testLoaderManager) {
+ *     mTestLoaderManager = testLoaderManager;
+ *   }
+ * </pre>
+ * In the tests, one would set the TestLoaderManager upon creating the activity, and then wait for
+ * the loader to complete.
+ * <pre>
+ *   public void testLoadedCorrect() {
+ *     TestLoaderManager testLoaderManager = new TestLoaderManager();
+ *     getActivity().setTestLoaderManager(testLoaderManager);
+ *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
+ *     testLoaderManager.waitForLoader(R.id.test_loader_id);
+ *   }
+ * </pre>
+ * If the loader completes before the call to {@link #waitForLoaders(int...)}, the TestLoaderManager
+ * will have stored the fact that the loader has completed and correctly terminate immediately.
+ * <p>
+ * It one needs to wait for the same loader multiple times, call {@link #reset()} between the them
+ * as in:
+ * <pre>
+ *   public void testLoadedCorrect() {
+ *     TestLoaderManager testLoaderManager = new TestLoaderManager();
+ *     getActivity().setTestLoaderManager(testLoaderManager);
+ *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
+ *     testLoaderManager.waitForLoader(R.id.test_loader_id);
+ *     testLoaderManager.reset();
+ *     // Load and wait again.
+ *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
+ *     testLoaderManager.waitForLoader(R.id.test_loader_id);
+ *   }
+ * </pre>
+ */
+public class TestLoaderManager extends LoaderManager {
+    private static final String TAG = "TestLoaderManager";
+
+    private final HashSet<Integer> mFinishedLoaders;
+
+    private LoaderManager mDelegate;
+
+    public TestLoaderManager() {
+        mFinishedLoaders = new HashSet<Integer>();
+    }
+
+    /**
+     * Sets the object to which we delegate the actual work.
+     * <p>
+     * It can not be set to null. Once set, it cannot be changed (but it allows setting it to the
+     * same value again).
+     */
+    public void setDelegate(LoaderManager delegate) {
+        if (delegate == null || (mDelegate != null && mDelegate != delegate)) {
+            throw new IllegalArgumentException("TestLoaderManager cannot be shared");
+        }
+
+        mDelegate = delegate;
+    }
+
+    public LoaderManager getDelegate() {
+        return mDelegate;
+    }
+
+    public void reset() {
+        mFinishedLoaders.clear();
+    }
+
+    /**
+     * Waits for the specified loaders to complete loading.
+     * <p>
+     * If one of the loaders has already completed since the last call to {@link #reset()}, it will
+     * not wait for it to complete again.
+     */
+    public synchronized void waitForLoaders(int... loaderIds) {
+        List<Loader<?>> loaders = new ArrayList<Loader<?>>(loaderIds.length);
+        for (int loaderId : loaderIds) {
+            if (mFinishedLoaders.contains(loaderId)) {
+                // This loader has already completed since the last reset, do not wait for it.
+                continue;
+            }
+
+            final AsyncTaskLoader<?> loader =
+                    (AsyncTaskLoader<?>) mDelegate.getLoader(loaderId);
+            if (loader == null) {
+                Assert.fail("Loader does not exist: " + loaderId);
+                return;
+            }
+
+            loaders.add(loader);
+        }
+
+        waitForLoaders(loaders.toArray(new Loader<?>[0]));
+    }
+
+    /**
+     * Waits for the specified loaders to complete loading.
+     */
+    public static void waitForLoaders(Loader<?>... loaders) {
+        // We want to wait for each loader using a separate thread, so that we can
+        // simulate race conditions.
+        Thread[] waitThreads = new Thread[loaders.length];
+        for (int i = 0; i < loaders.length; i++) {
+            final AsyncTaskLoader<?> loader = (AsyncTaskLoader<?>) loaders[i];
+            waitThreads[i] = new Thread("LoaderWaitingThread" + i) {
+                @Override
+                public void run() {
+                    try {
+                        loader.waitForLoader();
+                    } catch (Throwable e) {
+                        Log.e(TAG, "Exception while waiting for loader: " + loader.getId(), e);
+                        Assert.fail("Exception while waiting for loader: " + loader.getId());
+                    }
+                }
+            };
+            waitThreads[i].start();
+        }
+
+        // Now we wait for all these threads to finish
+        for (Thread thread : waitThreads) {
+            try {
+                thread.join();
+            } catch (InterruptedException e) {
+                // Ignore
+            }
+        }
+    }
+
+    @Override
+    public <D> Loader<D> initLoader(final int id, Bundle args, final LoaderCallbacks<D> callback) {
+        return mDelegate.initLoader(id, args, new LoaderManager.LoaderCallbacks<D>() {
+            @Override
+            public Loader<D> onCreateLoader(int id, Bundle args) {
+                return callback.onCreateLoader(id, args);
+            }
+
+            @Override
+            public void onLoadFinished(Loader<D> loader, D data) {
+                callback.onLoadFinished(loader, data);
+                synchronized (this) {
+                    mFinishedLoaders.add(id);
+                }
+            }
+
+            @Override
+            public void onLoaderReset(Loader<D> loader) {
+                callback.onLoaderReset(loader);
+            }
+        });
+    }
+
+    @Override
+    public <D> Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) {
+        return mDelegate.restartLoader(id, args, callback);
+    }
+
+    @Override
+    public void destroyLoader(int id) {
+        mDelegate.destroyLoader(id);
+    }
+
+    @Override
+    public <D> Loader<D> getLoader(int id) {
+        return mDelegate.getLoader(id);
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        mDelegate.dump(prefix, fd, writer, args);
+    }
+}
diff --git a/tests/src/com/android/contacts/activities/PeopleActivityTest.java b/tests/src/com/android/contacts/activities/PeopleActivityTest.java
index 4c1bb16..43158e5 100644
--- a/tests/src/com/android/contacts/activities/PeopleActivityTest.java
+++ b/tests/src/com/android/contacts/activities/PeopleActivityTest.java
@@ -20,6 +20,7 @@
 import com.android.contacts.ContactsApplication;
 import com.android.contacts.R;
 import com.android.contacts.detail.ContactDetailFragment;
+import com.android.contacts.interactions.TestLoaderManager;
 import com.android.contacts.list.ContactBrowseListFragment;
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
@@ -129,7 +130,7 @@
 
         // TODO: wait for detail loader
         // TODO: wait for lookup key loading
-        mContext.waitForLoaders(filterLoader, listLoader);
+        TestLoaderManager.waitForLoaders(filterLoader, listLoader);
 
         getInstrumentation().waitForIdleSync();
 
diff --git a/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java b/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
index 1db0f4c..c1eefc0 100644
--- a/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
+++ b/tests/src/com/android/contacts/interactions/ContactDeletionInteractionTest.java
@@ -129,16 +129,18 @@
     private void assertWithMessageId(int messageId) {
         final FragmentTestActivity activity = getActivity();
 
+        final TestLoaderManager mockLoaderManager = new TestLoaderManager();
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                mFragment = ContactDeletionInteraction.start(activity, CONTACT_URI, false);
+                mFragment = ContactDeletionInteraction.startWithTestLoaderManager(
+                        activity, CONTACT_URI, false, mockLoaderManager);
             }
         });
 
         getInstrumentation().waitForIdleSync();
 
-        mContext.waitForLoaders(mFragment.getLoaderManager(), R.id.dialog_delete_contact_loader_id);
+        mockLoaderManager.waitForLoaders(R.id.dialog_delete_contact_loader_id);
 
         getInstrumentation().waitForIdleSync();
 
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
index 9af1350..cea22b2 100644
--- a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -16,22 +16,14 @@
 
 package com.android.contacts.tests.mocks;
 
-//import com.android.providers.contacts.ContactsMockPackageManager;
-
-import android.app.LoaderManager;
-import android.content.AsyncTaskLoader;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.provider.ContactsContract;
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
-import android.util.Log;
-
-import junit.framework.Assert;
 
 /**
  * A mock context for contacts unit tests. Forwards everything to
@@ -86,58 +78,4 @@
         mContactsProvider.verify();
         mSettingsProvider.verify();
     }
-
-    /**
-     * Waits for the specified loaders to complete loading.
-     */
-    public void waitForLoaders(LoaderManager loaderManager, int... loaderIds) {
-        Loader<?>[] loaders = new Loader<?>[loaderIds.length];
-        for (int i = 0; i < loaderIds.length; i++) {
-            final int loaderId = loaderIds[i];
-            final AsyncTaskLoader<?> loader =
-                    (AsyncTaskLoader<?>) loaderManager.getLoader(loaderId);
-            if (loader == null) {
-                Assert.fail("Loader does not exist: " + loaderId);
-                return;
-            }
-
-            loaders[i] = loader;
-        }
-
-        waitForLoaders(loaders);
-    }
-
-    /**
-     * Waits for the specified loaders to complete loading.
-     */
-    public void waitForLoaders(Loader<?>... loaders) {
-        // We want to wait for each loader using a separate thread, so that we can
-        // simulate race conditions.
-        Thread[] waitThreads = new Thread[loaders.length];
-        for (int i = 0; i < loaders.length; i++) {
-            final AsyncTaskLoader<?> loader = (AsyncTaskLoader<?>) loaders[i];
-            waitThreads[i] = new Thread("LoaderWaitingThread" + i) {
-                @Override
-                public void run() {
-                    try {
-                        loader.waitForLoader();
-                    } catch (Throwable e) {
-                        Log.e(TAG, "Exception while waiting for loader: " + loader.getId(), e);
-                        Assert.fail("Exception while waiting for loader: " + loader.getId());
-                    }
-                }
-            };
-            waitThreads[i].start();
-        }
-
-        // Now we wait for all these threads to finish
-        for (Thread thread : waitThreads) {
-            try {
-                thread.join();
-            } catch (InterruptedException e) {
-                // Ignore
-            }
-        }
-    }
-
 }