Make AppPredictor thread-safe

Make callback registration, unregistration and session deletion mutually
exclusive.

Test: manual testing

Change-Id: I2ebc8ac37f2b34e0c6a10b563e9c4f36697101b1
diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java
index db3a192..d736a21 100644
--- a/core/java/android/app/prediction/AppPredictor.java
+++ b/core/java/android/app/prediction/AppPredictor.java
@@ -30,6 +30,8 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import dalvik.system.CloseGuard;
 
 import java.util.List;
@@ -74,12 +76,12 @@
 
     private static final String TAG = AppPredictor.class.getSimpleName();
 
-
     private final IPredictionManager mPredictionManager;
     private final CloseGuard mCloseGuard = CloseGuard.get();
     private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
 
     private final AppPredictionSessionId mSessionId;
+    @GuardedBy("itself")
     private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();
 
     private final IBinder mToken = new Binder();
@@ -97,7 +99,7 @@
         IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE);
         mPredictionManager = IPredictionManager.Stub.asInterface(b);
         mSessionId = new AppPredictionSessionId(
-                context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId());
+                context.getPackageName() + ":" + UUID.randomUUID(), context.getUserId());
         try {
             mPredictionManager.createPredictionSession(predictionContext, mSessionId, mToken);
         } catch (RemoteException e) {
@@ -158,6 +160,15 @@
      */
     public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor,
             @NonNull AppPredictor.Callback callback) {
+        synchronized (mRegisteredCallbacks) {
+            registerPredictionUpdatesLocked(callbackExecutor, callback);
+        }
+    }
+
+    @GuardedBy("mRegisteredCallbacks")
+    private void registerPredictionUpdatesLocked(
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull AppPredictor.Callback callback) {
         if (mIsClosed.get()) {
             throw new IllegalStateException("This client has already been destroyed.");
         }
@@ -186,6 +197,13 @@
      * @param callback The callback to be unregistered.
      */
     public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) {
+        synchronized (mRegisteredCallbacks) {
+            unregisterPredictionUpdatesLocked(callback);
+        }
+    }
+
+    @GuardedBy("mRegisteredCallbacks")
+    private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) {
         if (mIsClosed.get()) {
             throw new IllegalStateException("This client has already been destroyed.");
         }
@@ -238,7 +256,7 @@
         }
 
         try {
-            mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice(targets),
+            mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets),
                     new CallbackWrapper(callbackExecutor, callback));
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to sort targets", e);
@@ -254,19 +272,25 @@
         if (!mIsClosed.getAndSet(true)) {
             mCloseGuard.close();
 
-            // Do destroy;
-            try {
-                mPredictionManager.onDestroyPredictionSession(mSessionId);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to notify app target event", e);
-                e.rethrowAsRuntimeException();
+            synchronized (mRegisteredCallbacks) {
+                destroySessionLocked();
             }
-            mRegisteredCallbacks.clear();
         } else {
             throw new IllegalStateException("This client has already been destroyed.");
         }
     }
 
+    @GuardedBy("mRegisteredCallbacks")
+    private void destroySessionLocked() {
+        try {
+            mPredictionManager.onDestroyPredictionSession(mSessionId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to notify app target event", e);
+            e.rethrowAsRuntimeException();
+        }
+        mRegisteredCallbacks.clear();
+    }
+
     @Override
     protected void finalize() throws Throwable {
         try {