Add Media Playing API

These methods permit an activity to play or continue playing media
behind a translucent activity above it. Particularly the home
activity in Android TV. Methods exist to notify the upper activity
when playing starts or stops and for notifying the playing activity
when to stop playing and release its resources.

Methods are called when either activity's state changes or new
activities are launched.

Fixes bug 14469711.

Change-Id: I7ba10c5a4683504931cffa228488f5281e5bbf86
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 2914a61..5aa7027 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -743,10 +743,13 @@
             return Activity.this.findViewById(id);
         }
     };
-    
+
+    // Most recent call to setMediaPlaying().
+    boolean mMediaPlaying;
+
     ArrayMap<String, LoaderManagerImpl> mAllLoaderManagers;
     LoaderManagerImpl mLoaderManager;
-    
+
     private static final class ManagedCursor {
         ManagedCursor(Cursor cursor) {
             mCursor = cursor;
@@ -764,6 +767,7 @@
     // protected by synchronized (this) 
     int mResultCode = RESULT_CANCELED;
     Intent mResultData = null;
+
     private TranslucentConversionListener mTranslucentCallback;
     private boolean mChangeCanvasToTranslucent;
 
@@ -5325,6 +5329,101 @@
     }
 
     /**
+     * Activities that want to show media behind a translucent activity above them must call this
+     * method anytime before a return from {@link #onPause()}. If this call is successful
+     * then the activity should continue to play media when {@link #onPause()} is called, but must
+     * stop playing and release resources prior to or within the call to
+     * {@link #onStopMediaPlaying()}. If this call returns false the activity must stop
+     * playing and release resources immediately.
+     *
+     * <p>Only fullscreen opaque activities may make this call. I.e. this call is a nop
+     * for dialog and translucent activities.
+     *
+     * <p>False will be returned any time this method is call between the return of onPause and
+     *      the next call to onResume.
+     *
+     * @param playing true to notify the system that media is starting or continuing playing,
+     *                false to indicate that media has stopped or is stopping. Resources must
+     *                be released when passing false to this method.
+     * @return the resulting play state. If true the activity may continue playing media beyond
+     *      {@link #onPause()}, if false then the caller must stop playing and immediately
+     *      release all media resources. Returning false may occur in lieu of a call to
+     *      onReleaseMediaResources() so the return value must be checked.
+     *
+     * @see #isBackgroundMediaPlaying()
+     * @see #onStopMediaPlaying()
+     * @see #onBackgroundMediaPlayingChanged(boolean)
+     */
+    public boolean setMediaPlaying(boolean playing) {
+        if (!mResumed) {
+            // Do not permit paused or stopped activities to start playing.
+            playing = false;
+        }
+        try {
+            mMediaPlaying = ActivityManagerNative.getDefault().setMediaPlaying(mToken, playing) &&
+                    playing;
+        } catch (RemoteException e) {
+            mMediaPlaying = false;
+        }
+        return mMediaPlaying;
+    }
+
+    /**
+     * Called when a translucent activity over playing media is becoming opaque or another
+     * activity is being launched. Activities that call {@link #setMediaPlaying(boolean)}
+     * must implement this method to at the minimum call
+     * <code>super.onStopMediaPlayback()</code>.
+     *
+     * <p>When this method is called the activity has 500 msec to release the media resources.
+     * If the activity has not returned from this method in 500 msec the system will destroy
+     * the activity and kill the process in order to recover the media resources for another
+     * process. Otherwise {@link #onStop()} will be called following return.
+     *
+     * @see #setMediaPlaying(boolean)
+     * @see #isBackgroundMediaPlaying()
+     * @see #onBackgroundMediaPlayingChanged(boolean)
+     */
+    public void onStopMediaPlaying() {
+        mCalled = true;
+    }
+
+    /**
+     * Translucent activities may call this to determine if there is an activity below it that
+     * is playing media.
+     *
+     * @return true if media is playing according to the most recent call to
+     * {@link #setMediaPlaying(boolean)}, false otherwise.
+     *
+     * @see #setMediaPlaying(boolean)
+     * @see #onStopMediaPlaying()
+     * @see #onBackgroundMediaPlayingChanged(boolean)
+     * @hide
+     */
+    public boolean isBackgroundMediaPlaying() {
+        try {
+            return ActivityManagerNative.getDefault().isBackgroundMediaPlaying(mToken);
+        } catch (RemoteException e) {
+        }
+        return false;
+    }
+
+    /**
+     * The topmost foreground activity will receive this call when an activity below it either
+     * starts or stops playing media.
+     *
+     * This call may be a consequence of {@link #setMediaPlaying(boolean)} or might be
+     * due to a background activity finishing itself.
+     *
+     * @param playing true if media playback is starting, false if it is stopping.
+     *
+     * @see #setMediaPlaying(boolean)
+     * @see #isBackgroundMediaPlaying()
+     * @see #onStopMediaPlaying()
+     */
+    public void onBackgroundMediaPlayingChanged(boolean playing) {
+    }
+
+    /**
      * Adjust the current immersive mode setting.
      *
      * Note that changing this value will have no effect on the activity's
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index ce36fd8..4a70e15 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -2176,6 +2176,33 @@
             reply.writeNoException();
             return true;
         }
+
+        case SET_MEDIA_PLAYING_TRANSACTION: {
+            data.enforceInterface(IActivityManager.descriptor);
+            IBinder token = data.readStrongBinder();
+            boolean enable = data.readInt() > 0;
+            boolean success = setMediaPlaying(token, enable);
+            reply.writeNoException();
+            reply.writeInt(success ? 1 : 0);
+            return true;
+        }
+
+        case IS_BG_MEDIA_PLAYING_TRANSACTION: {
+            data.enforceInterface(IActivityManager.descriptor);
+            IBinder token = data.readStrongBinder();
+            final boolean enabled = isBackgroundMediaPlaying(token);
+            reply.writeNoException();
+            reply.writeInt(enabled ? 1 : 0);
+            return true;
+        }
+
+        case MEDIA_RESOURCES_RELEASED: {
+            data.enforceInterface(IActivityManager.descriptor);
+            IBinder token = data.readStrongBinder();
+            mediaResourcesReleased(token);
+            reply.writeNoException();
+            return true;
+        }
         }
 
         return super.onTransact(code, data, reply, flags);
@@ -5007,5 +5034,46 @@
         reply.recycle();
     }
 
+    @Override
+    public boolean setMediaPlaying(IBinder token, boolean playing) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        data.writeInterfaceToken(IActivityManager.descriptor);
+        data.writeStrongBinder(token);
+        data.writeInt(playing ? 1 : 0);
+        mRemote.transact(SET_MEDIA_PLAYING_TRANSACTION, data, reply, 0);
+        reply.readException();
+        boolean success = reply.readInt() > 0;
+        data.recycle();
+        reply.recycle();
+        return success;
+    }
+
+    @Override
+    public boolean isBackgroundMediaPlaying(IBinder token) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        data.writeInterfaceToken(IActivityManager.descriptor);
+        data.writeStrongBinder(token);
+        mRemote.transact(IS_BG_MEDIA_PLAYING_TRANSACTION, data, reply, 0);
+        reply.readException();
+        final boolean playing = reply.readInt() > 0;
+        data.recycle();
+        reply.recycle();
+        return playing;
+    }
+
+    @Override
+    public void mediaResourcesReleased(IBinder token) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        data.writeInterfaceToken(IActivityManager.descriptor);
+        data.writeStrongBinder(token);
+        mRemote.transact(MEDIA_RESOURCES_RELEASED, data, reply, IBinder.FLAG_ONEWAY);
+        reply.readException();
+        data.recycle();
+        reply.recycle();
+    }
+
     private IBinder mRemote;
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 7b48e1d..184e10c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1153,6 +1153,16 @@
         public final void updateTimePrefs(boolean is24Hour) {
             DateFormat.set24HourTimePref(is24Hour);
         }
+
+        @Override
+        public void scheduleStopMediaPlaying(IBinder token) {
+            sendMessage(H.STOP_MEDIA_PLAYING, token);
+        }
+
+        @Override
+        public void scheduleBackgroundMediaPlayingChanged(IBinder token, boolean playing) {
+            sendMessage(H.BACKGROUND_MEDIA_PLAYING_CHANGED, token, playing ? 1 : 0);
+        }
     }
 
     private class H extends Handler {
@@ -1203,6 +1213,8 @@
         public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144;
         public static final int INSTALL_PROVIDER        = 145;
         public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
+        public static final int STOP_MEDIA_PLAYING = 147;
+        public static final int BACKGROUND_MEDIA_PLAYING_CHANGED = 148;
 
         String codeToString(int code) {
             if (DEBUG_MESSAGES) {
@@ -1253,6 +1265,8 @@
                     case TRANSLUCENT_CONVERSION_COMPLETE: return "TRANSLUCENT_CONVERSION_COMPLETE";
                     case INSTALL_PROVIDER: return "INSTALL_PROVIDER";
                     case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS";
+                    case STOP_MEDIA_PLAYING: return "STOP_MEDIA_PLAYING";
+                    case BACKGROUND_MEDIA_PLAYING_CHANGED: return "BACKGROUND_MEDIA_PLAYING_CHANGED";
                 }
             }
             return Integer.toString(code);
@@ -1470,6 +1484,11 @@
                 case ON_NEW_ACTIVITY_OPTIONS:
                     Pair<IBinder, ActivityOptions> pair = (Pair<IBinder, ActivityOptions>) msg.obj;
                     onNewActivityOptions(pair.first, pair.second);
+                case STOP_MEDIA_PLAYING:
+                    handleStopMediaPlaying((IBinder) msg.obj);
+                    break;
+                case BACKGROUND_MEDIA_PLAYING_CHANGED:
+                    handleOnBackgroundMediaPlayingChanged((IBinder) msg.obj, msg.arg1 > 0);
                     break;
             }
             if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
@@ -2454,6 +2473,34 @@
         }
     }
 
+    public void handleStopMediaPlaying(IBinder token) {
+        ActivityClientRecord r = mActivities.get(token);
+        if (r != null) {
+            final Activity activity = r.activity;
+            if (activity.mMediaPlaying) {
+                activity.mCalled = false;
+                activity.onStopMediaPlaying();
+                // Tick, tick, tick. The activity has 500 msec to return or it will be destroyed.
+                if (!activity.mCalled) {
+                    throw new SuperNotCalledException("Activity " + activity.getLocalClassName() +
+                            " did not call through to super.onStopMediaPlayback()");
+                }
+                activity.mMediaPlaying = false;
+            }
+        }
+        try {
+            ActivityManagerNative.getDefault().mediaResourcesReleased(token);
+        } catch (RemoteException e) {
+        }
+    }
+
+    public void handleOnBackgroundMediaPlayingChanged(IBinder token, boolean playing) {
+        ActivityClientRecord r = mActivities.get(token);
+        if (r != null) {
+            r.activity.onBackgroundMediaPlayingChanged(playing);
+        }
+    }
+
     public void handleInstallProvider(ProviderInfo info) {
         installContentProviders(mInitialApplication, Lists.newArrayList(info));
     }
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index 6dead08..0b4510fe 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -647,6 +647,25 @@
             reply.writeNoException();
             return true;
         }
+
+        case STOP_MEDIA_PLAYING_TRANSACTION:
+        {
+            data.enforceInterface(IApplicationThread.descriptor);
+            IBinder token = data.readStrongBinder();
+            scheduleStopMediaPlaying(token);
+            reply.writeNoException();
+            return true;
+        }
+
+        case BACKGROUND_MEDIA_PLAYING_CHANGED_TRANSACTION:
+        {
+            data.enforceInterface(IApplicationThread.descriptor);
+            IBinder token = data.readStrongBinder();
+            boolean enabled = data.readInt() > 0;
+            scheduleBackgroundMediaPlayingChanged(token, enabled);
+            reply.writeNoException();
+            return true;
+        }
         }
 
         return super.onTransact(code, data, reply, flags);
@@ -1304,4 +1323,23 @@
         mRemote.transact(UPDATE_TIME_PREFS_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
         data.recycle();
     }
+
+    @Override
+    public void scheduleStopMediaPlaying(IBinder token) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        data.writeInterfaceToken(IApplicationThread.descriptor);
+        data.writeStrongBinder(token);
+        mRemote.transact(STOP_MEDIA_PLAYING_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+        data.recycle();
+    }
+
+    @Override
+    public void scheduleBackgroundMediaPlayingChanged(IBinder token, boolean enabled) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        data.writeInterfaceToken(IApplicationThread.descriptor);
+        data.writeStrongBinder(token);
+        data.writeInt(enabled ? 1 : 0);
+        mRemote.transact(BACKGROUND_MEDIA_PLAYING_CHANGED_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+        data.recycle();
+    }
 }
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 6e4192b..b3a8a8a 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -415,11 +415,9 @@
 
     public void performIdleMaintenance() throws RemoteException;
 
-    /** @hide */
     public IActivityContainer createActivityContainer(IBinder parentActivityToken,
             IActivityContainerCallback callback) throws RemoteException;
 
-    /** @hide */
     public void deleteActivityContainer(IActivityContainer container) throws RemoteException;
 
     public IActivityContainer getEnclosingActivityContainer(IBinder activityToken)
@@ -427,28 +425,25 @@
 
     public IBinder getHomeActivityToken() throws RemoteException;
 
-    /** @hide */
     public void startLockTaskModeOnCurrent() throws RemoteException;
 
-    /** @hide */
     public void startLockTaskMode(int taskId) throws RemoteException;
 
-    /** @hide */
     public void startLockTaskMode(IBinder token) throws RemoteException;
 
-    /** @hide */
     public void stopLockTaskMode() throws RemoteException;
 
-    /** @hide */
     public void stopLockTaskModeOnCurrent() throws RemoteException;
 
-    /** @hide */
     public boolean isInLockTaskMode() throws RemoteException;
 
-    /** @hide */
     public void setTaskDescription(IBinder token, ActivityManager.TaskDescription values)
             throws RemoteException;
 
+    public boolean setMediaPlaying(IBinder token, boolean playing) throws RemoteException;
+    public boolean isBackgroundMediaPlaying(IBinder token) throws RemoteException;
+    public void mediaResourcesReleased(IBinder token) throws RemoteException;
+
     /*
      * Private non-Binder interfaces
      */
@@ -753,4 +748,7 @@
     int STOP_LOCK_TASK_BY_CURRENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+222;
     int FINISH_VOICE_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+223;
     int IS_TOP_OF_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+224;
+    int SET_MEDIA_PLAYING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+225;
+    int IS_BG_MEDIA_PLAYING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+226;
+    int MEDIA_RESOURCES_RELEASED = IBinder.FIRST_CALL_TRANSACTION+227;
 }
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index d3c4854..18faf0e 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -145,6 +145,8 @@
     void setProcessState(int state) throws RemoteException;
     void scheduleInstallProvider(ProviderInfo provider) throws RemoteException;
     void updateTimePrefs(boolean is24Hour) throws RemoteException;
+    void scheduleStopMediaPlaying(IBinder token) throws RemoteException;
+    void scheduleBackgroundMediaPlayingChanged(IBinder token, boolean enabled) throws RemoteException;
 
     String descriptor = "android.app.IApplicationThread";
 
@@ -199,4 +201,6 @@
     int SET_PROCESS_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+49;
     int SCHEDULE_INSTALL_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+50;
     int UPDATE_TIME_PREFS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+51;
+    int STOP_MEDIA_PLAYING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+52;
+    int BACKGROUND_MEDIA_PLAYING_CHANGED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+53;
 }