Fix API review: Camera prewarm

Let the intent receiver of a camea launch intent declare a prewarm
service instead of sending broadcasts.

Bug: 21347653
Change-Id: I11e31aad4f788ad90eb46a661b819d3e808ddb51
diff --git a/api/current.txt b/api/current.txt
index be6e873..54647ad 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26126,8 +26126,6 @@
     method public static java.lang.String getVersion(android.content.Context);
     field public static final java.lang.String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
     field public static final java.lang.String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
-    field public static final java.lang.String ACTION_STILL_IMAGE_CAMERA_COOLDOWN = "android.media.action.STILL_IMAGE_CAMERA_COOLDOWN";
-    field public static final java.lang.String ACTION_STILL_IMAGE_CAMERA_PREWARM = "android.media.action.STILL_IMAGE_CAMERA_PREWARM";
     field public static final java.lang.String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
     field public static final java.lang.String AUTHORITY = "media";
     field public static final java.lang.String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
@@ -26155,6 +26153,7 @@
     field public static final java.lang.String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH = "android.media.action.VIDEO_PLAY_FROM_SEARCH";
     field public static final java.lang.String MEDIA_IGNORE_FILENAME = ".nomedia";
     field public static final java.lang.String MEDIA_SCANNER_VOLUME = "volume";
+    field public static final java.lang.String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service";
     field public static final java.lang.String UNKNOWN_STRING = "<unknown>";
   }
 
@@ -28713,6 +28712,13 @@
 
 package android.service.media {
 
+  public abstract class CameraPrewarmService extends android.app.Service {
+    ctor public CameraPrewarmService();
+    method public android.os.IBinder onBind(android.content.Intent);
+    method public abstract void onCooldown(boolean);
+    method public abstract void onPrewarm();
+  }
+
   public abstract class MediaBrowserService extends android.app.Service {
     ctor public MediaBrowserService();
     method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
diff --git a/api/system-current.txt b/api/system-current.txt
index a072bd7..87bdff5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -28049,8 +28049,6 @@
     method public static java.lang.String getVersion(android.content.Context);
     field public static final java.lang.String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
     field public static final java.lang.String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
-    field public static final java.lang.String ACTION_STILL_IMAGE_CAMERA_COOLDOWN = "android.media.action.STILL_IMAGE_CAMERA_COOLDOWN";
-    field public static final java.lang.String ACTION_STILL_IMAGE_CAMERA_PREWARM = "android.media.action.STILL_IMAGE_CAMERA_PREWARM";
     field public static final java.lang.String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
     field public static final java.lang.String AUTHORITY = "media";
     field public static final java.lang.String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
@@ -28078,6 +28076,7 @@
     field public static final java.lang.String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH = "android.media.action.VIDEO_PLAY_FROM_SEARCH";
     field public static final java.lang.String MEDIA_IGNORE_FILENAME = ".nomedia";
     field public static final java.lang.String MEDIA_SCANNER_VOLUME = "volume";
+    field public static final java.lang.String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service";
     field public static final java.lang.String UNKNOWN_STRING = "<unknown>";
   }
 
@@ -30739,6 +30738,13 @@
 
 package android.service.media {
 
+  public abstract class CameraPrewarmService extends android.app.Service {
+    ctor public CameraPrewarmService();
+    method public android.os.IBinder onBind(android.content.Intent);
+    method public abstract void onCooldown(boolean);
+    method public abstract void onPrewarm();
+  }
+
   public abstract class MediaBrowserService extends android.app.Service {
     ctor public MediaBrowserService();
     method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 7565654b..51dbdee 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -22,6 +22,7 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteException;
@@ -33,6 +34,7 @@
 import android.net.Uri;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
+import android.service.media.CameraPrewarmService;
 import android.util.Log;
 
 import java.io.FileInputStream;
@@ -226,33 +228,24 @@
     public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
 
     /**
-     * The name of the Intent action used to indicate that a camera launch might be imminent. This
-     * broadcast should be targeted to the package that is receiving
-     * {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
-     * {@link #INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE}, depending on the context. If such
-     * intent would launch the resolver activity, this broadcast should not be sent at all.
+     * Name under which an activity handling {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
+     * {@link #INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE} publishes the service name for its prewarm
+     * service.
      * <p>
-     * A receiver of this broadcast should do the absolute minimum amount of work to initialize the
-     * camera in order to reduce startup time in likely case that shortly after an actual camera
-     * launch intent would be sent.
+     * This meta-data should reference the fully qualified class name of the prewarm service
+     * extending {@link CameraPrewarmService}.
      * <p>
-     * In case the actual intent will not be fired, the target package will receive
-     * {@link #ACTION_STILL_IMAGE_CAMERA_COOLDOWN}. However, it is recommended that the receiver
-     * also implements a timeout to close the camera after receiving this intent, as there is no
-     * guarantee that {@link #ACTION_STILL_IMAGE_CAMERA_COOLDOWN} will be delivered.
+     * The prewarm service will get bound and receive a prewarm signal
+     * {@link CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent.
+     * An application implementing a prewarm service should do the absolute minimum amount of work
+     * to initialize the camera in order to reduce startup time in likely case that shortly after a
+     * camera launch intent would be sent.
+     * <p>
+     * If the camera launch intent gets fired shortly after, the service will be unbound
+     * asynchronously, without receiving
      */
-    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
-    public static final String ACTION_STILL_IMAGE_CAMERA_PREWARM = "android.media.action.STILL_IMAGE_CAMERA_PREWARM";
-
-    /**
-     * The name of the Intent action used to indicate that an imminent camera launch has been
-     * cancelled by the user. This broadcast should be targeted to the package that has received
-     * {@link #ACTION_STILL_IMAGE_CAMERA_PREWARM}.
-     * <p>
-     * A receiver of this broadcast should close the camera immediately.
-     */
-    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
-    public static final String ACTION_STILL_IMAGE_CAMERA_COOLDOWN = "android.media.action.STILL_IMAGE_CAMERA_COOLDOWN";
+    public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE =
+            "android.media.still_image_camera_preview_service";
 
     /**
      * The name of the Intent action used to launch a camera in still image mode
@@ -2268,5 +2261,4 @@
         }
         return null;
     }
-
 }
diff --git a/core/java/android/service/media/CameraPrewarmService.java b/core/java/android/service/media/CameraPrewarmService.java
new file mode 100644
index 0000000..335b00a
--- /dev/null
+++ b/core/java/android/service/media/CameraPrewarmService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 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 android.service.media;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+
+/**
+ * Extend this class to implement a camera prewarm service. See
+ * {@link android.provider.MediaStore#META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE}.
+ */
+public abstract class CameraPrewarmService extends Service {
+
+    /**
+     * Intent action to bind the service as a prewarm service.
+     * @hide
+     */
+    public static final String ACTION_PREWARM =
+            "android.service.media.CameraPrewarmService.ACTION_PREWARM";
+
+    /**
+     * Message sent by the client indicating that the camera intent has been fired.
+     * @hide
+     */
+    public static final int MSG_CAMERA_FIRED = 1;
+
+    private final Handler mHandler = new Handler() {
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CAMERA_FIRED:
+                    mCameraIntentFired = true;
+                    break;
+                default:
+                    super.handleMessage(msg);
+            }
+        }
+    };
+    private boolean mCameraIntentFired;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (ACTION_PREWARM.equals(intent.getAction())) {
+            onPrewarm();
+            return new Messenger(mHandler).getBinder();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        if (ACTION_PREWARM.equals(intent.getAction())) {
+            onCooldown(mCameraIntentFired);
+        }
+        return false;
+    }
+
+    /**
+     * Called when the camera should be prewarmed.
+     */
+    public abstract void onPrewarm();
+
+    /**
+     * Called when prewarm phase is done, either because the camera launch intent has been fired
+     * at this point or prewarm is no longer needed. A client should close the camera
+     * immediately in the latter case.
+     * <p>
+     * In case the camera launch intent has been fired, there is no guarantee about the ordering
+     * of these two events. Cooldown might happen either before or after the activity has been
+     * created that handles the camera intent.
+     *
+     * @param cameraIntentFired Indicates whether the intent to launch the camera has been
+     *                          fired.
+     */
+    public abstract void onCooldown(boolean cameraIntentFired);
+}
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
index 2cf30ba..cd4b24a 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java
@@ -60,6 +60,7 @@
     protected ViewMediatorCallback mViewMediatorCallback;
     protected LockPatternUtils mLockPatternUtils;
     private OnDismissAction mDismissAction;
+    private Runnable mCancelAction;
 
     private final KeyguardUpdateMonitorCallback mUpdateCallback =
             new KeyguardUpdateMonitorCallback() {
@@ -126,8 +127,17 @@
      *
      * @param action
      */
-    public void setOnDismissAction(OnDismissAction action) {
+    public void setOnDismissAction(OnDismissAction action, Runnable cancelAction) {
+        if (mCancelAction != null) {
+            mCancelAction.run();
+            mCancelAction = null;
+        }
         mDismissAction = action;
+        mCancelAction = cancelAction;
+    }
+
+    public void cancelDismissAction() {
+        setOnDismissAction(null, null);
     }
 
     @Override
@@ -197,6 +207,7 @@
         if (mDismissAction != null) {
             deferKeyguardDone = mDismissAction.onDismiss();
             mDismissAction = null;
+            mCancelAction = null;
         }
         if (mViewMediatorCallback != null) {
             if (deferKeyguardDone) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
index ee5eb38..9ef320bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
@@ -24,6 +24,11 @@
  * Keyguard.
  */
 public interface ActivityStarter {
-    public void startActivity(Intent intent, boolean dismissShade);
+    void startActivity(Intent intent, boolean dismissShade);
+    void startActivity(Intent intent, boolean dismissShade, Callback callback);
     void preventNextAnimation();
+
+    interface Callback {
+        void onActivityStarted(int resultCode);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
index 8bffdc9..64735ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -160,7 +160,7 @@
                 } else {
                     mTouchSlopExeeded = false;
                 }
-                mCallback.onSwipingStarted(targetView == mLeftIcon);
+                mCallback.onSwipingStarted(targetView == mRightIcon);
                 mSwipingInProgress = true;
                 mTargetedView = targetView;
                 mInitialTouchX = x;
@@ -550,7 +550,7 @@
 
         float getMaxTranslationDistance();
 
-        void onSwipingStarted(boolean isRightwardMotion);
+        void onSwipingStarted(boolean rightIcon);
 
         void onSwipingAborted();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index adee5a8..3258a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -16,12 +16,17 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
+import android.app.Application;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
@@ -29,9 +34,13 @@
 import android.graphics.drawable.InsetDrawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.MediaStore;
+import android.service.media.CameraPrewarmService;
 import android.telecom.TelecomManager;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -100,7 +109,23 @@
     private PhoneStatusBar mPhoneStatusBar;
 
     private final Interpolator mLinearOutSlowInInterpolator;
-    private boolean mPrewarmSent;
+    private boolean mPrewarmBound;
+    private Messenger mPrewarmMessenger;
+    private final ServiceConnection mPrewarmConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mPrewarmMessenger = new Messenger(service);
+            mPrewarmBound = true;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mPrewarmBound = false;
+            mPrewarmMessenger = null;
+        }
+    };
+
     private boolean mLeftIsVoiceAssist;
     private AssistManager mAssistManager;
 
@@ -343,37 +368,44 @@
         mLockPatternUtils.requireCredentialEntry(KeyguardUpdateMonitor.getCurrentUser());
     }
 
-    public void prewarmCamera() {
+    public void bindCameraPrewarmService() {
         Intent intent = getCameraIntent();
-        String targetPackage = PreviewInflater.getTargetPackage(mContext, intent,
+        ActivityInfo targetInfo = PreviewInflater.getTargetActivityInfo(mContext, intent,
                 KeyguardUpdateMonitor.getCurrentUser());
-        if (targetPackage != null) {
-            Intent prewarm = new Intent(MediaStore.ACTION_STILL_IMAGE_CAMERA_PREWARM);
-            prewarm.setPackage(targetPackage);
-            mPrewarmSent = true;
-            mContext.sendBroadcast(prewarm);
+        if (targetInfo != null) {
+            String clazz = targetInfo.metaData.getString(
+                    MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE);
+            if (clazz != null) {
+                Intent serviceIntent = new Intent();
+                serviceIntent.setClassName(targetInfo.packageName, clazz);
+                serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM);
+                try {
+                    getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection,
+                            Context.BIND_AUTO_CREATE, new UserHandle(UserHandle.USER_CURRENT));
+                } catch (SecurityException e) {
+                    Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName
+                            + " class=" + clazz, e);
+                }
+            }
         }
     }
 
-    public void maybeCooldownCamera() {
-        if (!mPrewarmSent) {
-            return;
-        }
-        mPrewarmSent = false;
-        Intent intent = getCameraIntent();
-        String targetPackage = PreviewInflater.getTargetPackage(mContext, intent,
-                KeyguardUpdateMonitor.getCurrentUser());
-        if (targetPackage != null) {
-            Intent prewarm = new Intent(MediaStore.ACTION_STILL_IMAGE_CAMERA_COOLDOWN);
-            prewarm.setPackage(targetPackage);
-            mContext.sendBroadcast(prewarm);
+    public void unbindCameraPrewarmService(boolean launched) {
+        if (mPrewarmBound) {
+            if (launched) {
+                try {
+                    mPrewarmMessenger.send(Message.obtain(null /* handler */,
+                            CameraPrewarmService.MSG_CAMERA_FIRED));
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Error sending camera fired message", e);
+                }
+            }
+            mContext.unbindService(mPrewarmConnection);
+            mPrewarmBound = false;
         }
     }
 
     public void launchCamera() {
-
-        // Reset prewarm state.
-        mPrewarmSent = false;
         final Intent intent = getCameraIntent();
         boolean wouldLaunchResolverActivity = PreviewInflater.wouldLaunchResolverActivity(
                 mContext, intent, KeyguardUpdateMonitor.getCurrentUser());
@@ -381,18 +413,47 @@
             AsyncTask.execute(new Runnable() {
                 @Override
                 public void run() {
-                    mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    int result = ActivityManager.START_CANCELED;
+                    try {
+                        result = ActivityManagerNative.getDefault().startActivityAsUser(
+                                null, getContext().getBasePackageName(),
+                                intent,
+                                intent.resolveTypeIfNeeded(getContext().getContentResolver()),
+                                null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, null,
+                                UserHandle.CURRENT.getIdentifier());
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Unable to start camera activity", e);
+                    }
                     mActivityStarter.preventNextAnimation();
+                    final boolean launched = isSuccessfulLaunch(result);
+                    post(new Runnable() {
+                        @Override
+                        public void run() {
+                            unbindCameraPrewarmService(launched);
+                        }
+                    });
                 }
             });
         } else {
 
             // We need to delay starting the activity because ResolverActivity finishes itself if
             // launched behind lockscreen.
-            mActivityStarter.startActivity(intent, false /* dismissShade */);
+            mActivityStarter.startActivity(intent, false /* dismissShade */,
+                    new ActivityStarter.Callback() {
+                        @Override
+                        public void onActivityStarted(int resultCode) {
+                            unbindCameraPrewarmService(isSuccessfulLaunch(resultCode));
+                        }
+                    });
         }
     }
 
+    private static boolean isSuccessfulLaunch(int result) {
+        return result == ActivityManager.START_SUCCESS
+                || result == ActivityManager.START_DELIVERED_TO_TOP
+                || result == ActivityManager.START_TASK_TO_FRONT;
+    }
+
     public void launchLeftAffordance() {
         if (mLeftIsVoiceAssist) {
             launchVoiceAssist();
@@ -412,8 +473,8 @@
         if (mPhoneStatusBar.isKeyguardCurrentlySecure()) {
             AsyncTask.execute(runnable);
         } else {
-            mPhoneStatusBar.executeRunnableDismissingKeyguard(runnable, false /* dismissShade */,
-                    false /* afterKeyguardGone */);
+            mPhoneStatusBar.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */,
+                    false /* dismissShade */, false /* afterKeyguardGone */);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 262d955..3d57d54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -95,16 +95,16 @@
         mShowingSoon = false;
     }
 
-    public void showWithDismissAction(OnDismissAction r) {
+    public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) {
         ensureView();
-        mKeyguardView.setOnDismissAction(r);
+        mKeyguardView.setOnDismissAction(r, cancelAction);
         show(false /* resetSecuritySelection */);
     }
 
     public void hide(boolean destroyView) {
         cancelShowRunnable();
          if (mKeyguardView != null) {
-            mKeyguardView.setOnDismissAction(null);
+            mKeyguardView.cancelDismissAction();
             mKeyguardView.cleanUp();
         }
         if (destroyView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 5d48190..2fe98bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -1934,12 +1934,12 @@
     }
 
     @Override
-    public void onSwipingStarted(boolean isRightwardMotion) {
-        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? isRightwardMotion
-                : !isRightwardMotion;
-        if (!start) {
+    public void onSwipingStarted(boolean rightIcon) {
+        boolean camera = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? !rightIcon
+                : rightIcon;
+        if (camera) {
             mSecureCameraLaunchManager.onSwipingStarted();
-            mKeyguardBottomArea.prewarmCamera();
+            mKeyguardBottomArea.bindCameraPrewarmService();
         }
         requestDisallowInterceptTouchEvent(true);
         mOnlyAffordanceInThisMotion = true;
@@ -1948,7 +1948,7 @@
 
     @Override
     public void onSwipingAborted() {
-        mKeyguardBottomArea.maybeCooldownCamera();
+        mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 1e4aa61..e6089ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -1826,6 +1826,11 @@
     }
 
     @Override
+    public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
+        startActivityDismissingKeyguard(intent, false, dismissShade, callback);
+    }
+
+    @Override
     public void preventNextAnimation() {
         overrideActivityPendingAppTransition(true /* keyguardShowing */);
     }
@@ -2707,7 +2712,12 @@
     }
 
     public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
-            final boolean dismissShade) {
+            boolean dismissShade) {
+        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, null /* callback */);
+    }
+
+    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+            final boolean dismissShade, final Callback callback) {
         if (onlyProvisioned && !isDeviceProvisioned()) return;
 
         final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
@@ -2717,16 +2727,35 @@
             public void run() {
                 intent.setFlags(
                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-                mContext.startActivityAsUser(
-                        intent, new UserHandle(UserHandle.USER_CURRENT));
+                int result = ActivityManager.START_CANCELED;
+                try {
+                    result = ActivityManagerNative.getDefault().startActivityAsUser(
+                            null, mContext.getBasePackageName(),
+                            intent,
+                            intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                            null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, null,
+                            UserHandle.CURRENT.getIdentifier());
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Unable to start activity", e);
+                }
                 overrideActivityPendingAppTransition(
                         keyguardShowing && !afterKeyguardGone);
+                if (callback != null) {
+                    callback.onActivityStarted(result);
+                }
             }
         };
-        executeRunnableDismissingKeyguard(runnable, dismissShade, afterKeyguardGone);
+        Runnable cancelRunnable = new Runnable() {
+            @Override
+            public void run() {
+                callback.onActivityStarted(ActivityManager.START_CANCELED);
+            }
+        };
+        executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShade, afterKeyguardGone);
     }
 
     public void executeRunnableDismissingKeyguard(final Runnable runnable,
+            final Runnable cancelAction,
             final boolean dismissShade,
             final boolean afterKeyguardGone) {
         final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
@@ -2753,7 +2782,7 @@
                 }
                 return true;
             }
-        }, afterKeyguardGone);
+        }, cancelAction, afterKeyguardGone);
     }
 
     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -2813,10 +2842,15 @@
     }
 
     @Override
-    protected void dismissKeyguardThenExecute(final OnDismissAction action,
+    protected void dismissKeyguardThenExecute(OnDismissAction action, boolean afterKeyguardGone) {
+        dismissKeyguardThenExecute(action, null /* cancelRunnable */, afterKeyguardGone);
+    }
+
+    private void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction,
             boolean afterKeyguardGone) {
         if (mStatusBarKeyguardViewManager.isShowing()) {
-            mStatusBarKeyguardViewManager.dismissWithAction(action, afterKeyguardGone);
+            mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
+                    afterKeyguardGone);
         } else {
             action.onDismiss();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 0caf51a..6cb890a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -126,10 +126,11 @@
         updateStates();
     }
 
-    public void dismissWithAction(OnDismissAction r, boolean afterKeyguardGone) {
+    public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
+            boolean afterKeyguardGone) {
         if (mShowing) {
             if (!afterKeyguardGone) {
-                mBouncer.showWithDismissAction(r);
+                mBouncer.showWithDismissAction(r, cancelAction);
             } else {
                 mBouncer.show(false /* resetSecuritySelection */);
                 mAfterKeyguardGoneAction = r;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
index 4269c19..93d0ec3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
@@ -19,6 +19,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
@@ -138,14 +139,14 @@
 
     public static boolean wouldLaunchResolverActivity(Context ctx, Intent intent,
             int currentUserId) {
-        return getTargetPackage(ctx, intent, currentUserId) == null;
+        return getTargetActivityInfo(ctx, intent, currentUserId) == null;
     }
 
     /**
-     * @return the target package of the intent it resolves to a specific package or {@code null} if
-     *         it resolved to the resolver activity
+     * @return the target activity info of the intent it resolves to a specific package or
+     *         {@code null} if it resolved to the resolver activity
      */
-    public static String getTargetPackage(Context ctx, Intent intent,
+    public static ActivityInfo getTargetActivityInfo(Context ctx, Intent intent,
             int currentUserId) {
         PackageManager packageManager = ctx.getPackageManager();
         final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
@@ -158,7 +159,7 @@
         if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
             return null;
         } else {
-            return resolved.activityInfo.packageName;
+            return resolved.activityInfo;
         }
     }
 
diff --git a/tests/CameraPrewarmTest/AndroidManifest.xml b/tests/CameraPrewarmTest/AndroidManifest.xml
index eb40200..11b2686 100644
--- a/tests/CameraPrewarmTest/AndroidManifest.xml
+++ b/tests/CameraPrewarmTest/AndroidManifest.xml
@@ -22,13 +22,13 @@
         <activity android:name=".CameraActivity"
                 android:theme="@android:style/Theme.NoTitleBar">
             <intent-filter>
-                <action android:name="android.media.action.STILL_IMAGE_CAMERA_SECURE" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-            <intent-filter>
                 <action android:name="android.media.action.STILL_IMAGE_CAMERA" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
+            <meta-data
+                android:name="android.media.still_image_camera_preview_service"
+                android:value="com.google.android.test.cameraprewarm.PrewarmService">
+            </meta-data>
         </activity>
 
         <activity android:name=".SecureCameraActivity"
@@ -37,16 +37,15 @@
                 <action android:name="android.media.action.STILL_IMAGE_CAMERA_SECURE" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
+            <meta-data
+                android:name="android.media.still_image_camera_preview_service"
+                android:value="com.google.android.test.cameraprewarm.PrewarmService">
+            </meta-data>
         </activity>
 
-        <receiver android:name=".PrewarmReceiver" >
-            <intent-filter>
-                <action android:name="android.media.action.STILL_IMAGE_CAMERA_PREWARM" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.media.action.STILL_IMAGE_CAMERA_COOLDOWN" />
-            </intent-filter>
-        </receiver>
+        <service android:name=".PrewarmService"
+            android:exported="true">
+        </service>
 
     </application>
 </manifest>
diff --git a/tests/CameraPrewarmTest/res/values/strings.xml b/tests/CameraPrewarmTest/res/values/strings.xml
index 11f7ac7..fe39ac1 100644
--- a/tests/CameraPrewarmTest/res/values/strings.xml
+++ b/tests/CameraPrewarmTest/res/values/strings.xml
@@ -16,6 +16,6 @@
   -->
 
 <resources>
-    <string name="activity_title">Assistant</string>
+    <string name="activity_title">Camera Prewarm test</string>
     <string name="search_label">Orilla Search Engine</string>
 </resources>
diff --git a/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/CameraActivity.java b/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/CameraActivity.java
index 4d22234..0b43666 100644
--- a/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/CameraActivity.java
+++ b/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/CameraActivity.java
@@ -19,7 +19,6 @@
 import android.app.Activity;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.WindowManager;
 
 import com.google.android.test.cameraprewarm.R;
 
@@ -31,7 +30,6 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.camera_activity);
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
         Log.i(TAG, "Activity created");
     }
 }
diff --git a/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmReceiver.java b/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmReceiver.java
deleted file mode 100644
index d49f96d..0000000
--- a/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmReceiver.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2015 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.google.android.test.cameraprewarm;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.provider.MediaStore;
-import android.util.Log;
-
-public class PrewarmReceiver extends BroadcastReceiver {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (intent.getAction().equals(MediaStore.ACTION_STILL_IMAGE_CAMERA_PREWARM)) {
-            Log.i(CameraActivity.TAG, "Prewarm received");
-        } else if (intent.getAction().equals(MediaStore.ACTION_STILL_IMAGE_CAMERA_COOLDOWN)){
-            Log.i(CameraActivity.TAG, "Cooldown received");
-        }
-    }
-}
diff --git a/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmService.java b/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmService.java
new file mode 100644
index 0000000..d080b1a
--- /dev/null
+++ b/tests/CameraPrewarmTest/src/com/google/android/test/cameraprewarm/PrewarmService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.google.android.test.cameraprewarm;
+
+import android.service.media.CameraPrewarmService;
+import android.util.Log;
+
+public class PrewarmService extends CameraPrewarmService {
+
+    @Override
+    public void onPrewarm() {
+        Log.i("PrewarmService", "Warming up");
+    }
+
+    @Override
+    public void onCooldown(boolean cameraIntentFired) {
+        Log.i("PrewarmService", "Cooling down fired=" + cameraIntentFired);
+    }
+}