Index: media/base/android/java/src/org/chromium/media/DialogSurfaceHolder.java |
diff --git a/media/base/android/java/src/org/chromium/media/DialogSurfaceHolder.java b/media/base/android/java/src/org/chromium/media/DialogSurfaceHolder.java |
index 6797ffe8aab11eb369cef875568d7428a0f79b19..0fd948a1ef127289d08bdd1b9161d8deb647a2cb 100644 |
--- a/media/base/android/java/src/org/chromium/media/DialogSurfaceHolder.java |
+++ b/media/base/android/java/src/org/chromium/media/DialogSurfaceHolder.java |
@@ -48,12 +48,41 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
* IMPORTANT: Do not call this with mLock held. Expect that the callback |
* may call us. |
*/ |
- private final IDialogSurfaceCallback mCallback; |
+ private final IDialogSurfaceCallback mClientCallback; |
private final Handler mHandler; |
private final Context mContext; |
private final int mRendererPid; |
private final int mRenderFrameId; |
+ private enum State { |
+ // We have no token and no dialog. We will transition to the state |
+ // SCHEDULE_CREATE_DIALOG when we get a token. |
+ WAITING_FOR_TOKEN, |
+ |
+ // We have a token, and have scheduled dialog creation. When we try |
+ // to create the dialog, we will either still have a token and |
+ // transition to WAITING_FOR_SURFACE, or we won't and will transition |
+ // back to WAITING_FOR_TOKEN . |
+ SCHEDULED_CREATE_DIALOG, |
+ |
+ // We have a dialog, and we're waiting for onSurfaceCreated. If we lose |
+ // the token, then we'll dismiss the dialog and transition to |
+ // WAITING_FOR_TOKEN. If we get the created message, then we'll |
+ // transition to HAVE_SURFACE. |
+ // If we dismiss the dialog, we will also ignore callbacks from it, |
+ // since the surface isn't in use by the application, and it will be |
+ // destroyed once the dismiss actually happens, anyway. |
+ WAITING_FOR_SURFACE, |
+ |
+ // We have a dialog and a surface. If we lose the token, then we'll |
+ // dismiss the dialog, notify the client of a synthetic DESTROY, and |
+ // start ignoring callbacks from the surface. We'll transition to |
+ // WAITING_FOR_TOKEN. If we get a SURFACE_DESTROYED while in |
+ // HAVE_SURFACE, then we'll transition back to WAITING_FOR_SURFACE, |
+ // figuring that the surface will come back, e.g., at resume. |
+ HAVE_SURFACE, |
+ } |
+ |
// Callback operations. |
// These must match dialog_surface.h . |
private static final int OP_CREATED = 0; |
@@ -61,13 +90,23 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
private DialogSurfaceManager mOwner; |
+ private class Layout { |
+ public int x; |
+ public int y; |
+ public int width; |
+ public int height; |
+ }; |
+ |
////// Either thread, protected by mLock |
private final Object mLock = new Object(); |
private Dialog mDialog; |
+ private Callbacks mDialogCallbacks; |
private boolean mReleased; |
private Surface mSurface; |
private Runnable mPendingRunnable; |
private IBinder mToken; |
+ private Layout mCurrentLayout; |
+ private State mState = State.WAITING_FOR_TOKEN; |
/** |
* Called from primary thread. |
@@ -91,19 +130,26 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
mContext = context; |
mOwner = owner; |
mHandler = handler; |
- mCallback = callback; |
+ mClientCallback = callback; |
mReleased = false; |
+ mCurrentLayout = new Layout(); |
+ mCurrentLayout.x = x; |
+ mCurrentLayout.y = y; |
+ mCurrentLayout.width = width; |
+ mCurrentLayout.height = height; |
+ |
try { |
- mOwner.getMapper().postWindowToken(rendererPid, renderFrameId, this); |
- } catch(RemoteException e) { |
+ mOwner.getMapper().registerHolder(rendererPid, renderFrameId, this); |
+ } catch (RemoteException e) { |
Log.e(TAG, "Unable to request window token", e); |
// We could notify the callback here. |
release(); |
return; |
} |
- scheduleCreateDialog(x, y, width, height); |
+ |
+ // When we get a token, we'll create the dialog. |
} |
// Note that the native wrapper should call release() anyway on destruction. |
@@ -123,6 +169,11 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
@Override |
public void release() { |
synchronized (mLock) { |
+ try { |
+ mOwner.getMapper().unregisterHolder(this); |
+ } catch (RemoteException e) { |
+ } |
+ |
if (!mReleased) mOwner.notifyReleased(); |
mOwner = null; |
@@ -169,17 +220,24 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
// somebody called scheduleLayoutSurface while a create was pending |
// before they got notification that the surface was ready. |
- if (dialog == null) return; |
- if (mToken == null) return; |
+ // Remember the most recent layout. |
+ mCurrentLayout.x = x; |
+ mCurrentLayout.y = y; |
+ mCurrentLayout.width = width; |
+ mCurrentLayout.height = height; |
- Runnable r = new Runnable() { |
- @Override |
- public void run() { |
- layoutDialog(dialog, x, y, width, height); |
- } |
- }; |
+ // Only schedule this if we currently have a dialog. Otherwise, |
+ // we'll use the most recent layout when we get one. |
+ if (mState == State.WAITING_FOR_SURFACE || mState == State.HAVE_SURFACE) { |
+ Runnable r = new Runnable() { |
+ @Override |
+ public void run() { |
+ layoutDialog(dialog); |
+ } |
+ }; |
- mHandler.post(r); |
+ mHandler.post(r); |
+ } |
} |
} |
@@ -188,35 +246,37 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
* These happen on the looper thread. |
*/ |
private class Callbacks implements SurfaceHolder.Callback2 { |
+ public boolean ignoreCallbacks = false; |
+ |
@Override |
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} |
@Override |
public void surfaceCreated(SurfaceHolder holder) { |
synchronized (mLock) { |
+ if (ignoreCallbacks) return; |
+ |
mSurface = holder.getSurface(); |
- } |
- try { |
- mCallback.onCallback(OP_CREATED); |
- } catch (Exception e) { |
- Log.e(TAG, "SurfaceCreated: callback failed: " + e); |
- release(); |
+ // assert mState == WAITING_FOR_SURFACE |
+ mState = State.HAVE_SURFACE; |
} |
+ |
+ notifyClient(OP_CREATED); |
} |
@Override |
public void surfaceDestroyed(SurfaceHolder holder) { |
synchronized (mLock) { |
+ if (ignoreCallbacks) return; |
+ |
mSurface = null; |
- } |
- try { |
- mCallback.onCallback(OP_DESTROYED); |
- } catch (Exception e) { |
- Log.e(TAG, "SurfaceDestroyed: callback failed: " + e); |
- release(); |
+ // assert mState == HAVE_SURFACE |
+ mState = State.WAITING_FOR_SURFACE; |
} |
+ |
+ notifyClient(OP_DESTROYED); |
} |
@Override |
@@ -224,23 +284,58 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
} |
/** |
+ * Notify the client about |operation|. |
+ */ |
+ private void notifyClient(int operation) { |
+ try { |
+ mClientCallback.onCallback(operation); |
+ } catch (Exception e) { |
+ Log.e(TAG, "notifyClient: callback failed: " + e); |
+ release(); |
+ } |
+ } |
+ |
+ /** |
* Schedule creation of the dialog on our looper thread. Sets mThread |
* asynchronously. If release() occurs before then, then the dialog will |
* not be created. |
*/ |
- private void scheduleCreateDialog(final int x, final int y, final int width, final int height) { |
+ private void scheduleCreateDialog() { |
synchronized (mLock) { |
+ // assert mState == WAITING_FOR_TOKEN, maybe with a token. |
+ |
+ // If we don't have a token right now, then skip it. |
+ if (mToken == null) return; |
+ |
mDialog = null; |
+ mDialogCallbacks = null; |
+ mState = State.SCHEDULED_CREATE_DIALOG; |
} |
Runnable r = new Runnable() { |
@Override |
public void run() { |
+ synchronized (mLock) { |
+ // assert !mDialog |
+ |
+ if (mToken == null) { |
+ // We lost the token, so just skip this and go back to |
+ // waiting for one. |
+ mState = State.WAITING_FOR_TOKEN; |
+ return; |
+ } |
+ |
+ // We're going to create the dialog, and are now waiting |
+ // for the surface. |
+ mState = State.WAITING_FOR_SURFACE; |
+ } |
+ |
Dialog dialog = new Dialog(mContext, android.R.style.Theme_NoDisplay); |
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |
dialog.setCancelable(false); |
- layoutDialog(dialog, x, y, width, height); |
- dialog.getWindow().takeSurface(new Callbacks()); |
+ layoutDialog(dialog); |
+ mDialogCallbacks = new Callbacks(); |
+ dialog.getWindow().takeSurface(mDialogCallbacks); |
synchronized (mLock) { |
// If we've been released in the interim, then stop here. |
if (mReleased) return; |
@@ -251,13 +346,9 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
} |
}; // new Runnable |
- // Post it to our dedicated thread when we have a window token. |
- synchronized (mLock) { |
- if (mToken != null) |
- mHandler.post(r); |
- else |
- mPendingRunnable = r; |
- } |
+ // Post it to our dedicated thread. Note that we might not still have |
+ // a token when we get there, but that's okay. |
+ mHandler.post(r); |
} |
/** |
@@ -265,17 +356,19 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
* looper thread. |
* Call scheduleLayoutDialog from anywhere else. |
*/ |
- private void layoutDialog(Dialog dialog, int x, int y, int width, int height) { |
+ private void layoutDialog(Dialog dialog) { |
// Rather than using getAttributes, we just create them from scratch. |
// The default dialog attributes aren't what we want. |
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); |
// TODO(liberato): adjust for CompositorView screen location here if we |
// want to support non-full screen use cases. |
- layoutParams.x = x; |
- layoutParams.y = y; |
- layoutParams.width = width; |
- layoutParams.height = height; |
+ synchronized (mLock) { |
+ layoutParams.x = mCurrentLayout.x; |
+ layoutParams.y = mCurrentLayout.y; |
+ layoutParams.width = mCurrentLayout.width; |
+ layoutParams.height = mCurrentLayout.height; |
+ } |
layoutParams.gravity = Gravity.TOP | Gravity.LEFT; |
// Use a media surface, which is what SurfaceView uses by default. For |
@@ -313,7 +406,7 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
} catch (ExceptionInInitializerError e) { |
} |
- synchronized(mLock) { |
+ synchronized (mLock) { |
layoutParams.token = mToken; |
} |
@@ -324,15 +417,99 @@ class DialogSurfaceHolder extends IDialogSurfaceHolder.Stub { |
public void onWindowToken(IBinder token) { |
synchronized (mLock) { |
mToken = token; |
- // TODO(liberato): is it useful to notify the callback if !mToken? |
- if (mToken != null && mPendingRunnable != null) { |
- Runnable pendingRunnable = mPendingRunnable; |
- mHandler.post(pendingRunnable); |
+ |
+ if (mToken == null) { |
+ lostTokenLocked(); |
+ } else { |
+ acquiredTokenLocked(); |
} |
- // Clear the pending runnable even without a token. If the token |
- // isn't available or changes, then we have to rebuild everything |
- // since the Dialog may have a token from a previous activity. |
- mPendingRunnable = null; |
} |
} |
+ |
+ // Helper function to send a synthetic DESTROYED method to the client, |
+ // dismiss the dialog, and ignore future callbacks from it. In other words, |
+ // this causes us to pretend that the surface is gone. |
+ private void syntheticDestroyAndDismiss() { |
+ notifyClient(OP_DESTROYED); |
+ if (mDialog != null) { |
+ mDialogCallbacks.ignoreCallbacks = true; |
+ mDialog.dismiss(); |
+ mDialog = null; |
+ mDialogCallbacks = null; |
+ } |
+ |
+ // Transition to WAITING_FOR_TOKEN. We might already have a token, |
+ // which is okay. |
+ mState = State.WAITING_FOR_TOKEN; |
+ |
+ // If we do have a token, then we're not allowed to stay in the |
+ // WAITING_FOR_TOKEN state. Create the dialog. |
+ if (mToken != null) scheduleCreateDialog(); |
+ } |
+ |
+ // Helper function when we are transitioning from "have token" to not. |
+ private void lostTokenLocked() { |
+ Log.d(TAG, "lostTokenLocked: state: " + mState); |
+ switch (mState) { |
+ case WAITING_FOR_TOKEN: |
+ break; |
+ |
+ case SCHEDULED_CREATE_DIALOG: |
+ // Take no action until the dialog is created. |
+ break; |
+ |
+ case WAITING_FOR_SURFACE: |
+ // Dismiss the dialog, ignoring any future callbacks from it since |
+ // the dialog has the wrong token and the application doesn't have |
+ // the surface anyway. |
+ syntheticDestroyAndDismiss(); |
+ break; |
+ |
+ case HAVE_SURFACE: |
+ // We've lost the oken that this dialog is using. Dismiss it, |
+ // notify the client, and wait for a new token. |
+ // Note that it would be nice if we found out about this before |
+ // actually losing the token. In the case of a window switch, |
+ // there's no reason why we can't. |
+ syntheticDestroyAndDismiss(); |
+ break; |
+ } |
+ Log.d(TAG, "lostTokenLocked: new state: " + mState); |
+ } |
+ |
+ // Helper function when we are transition from "no token" to "have token". |
+ private void acquiredTokenLocked() { |
+ Log.d(TAG, "acquiredTokenLocked: state: " + mState); |
+ switch (mState) { |
+ case WAITING_FOR_TOKEN: |
+ // Yay! |
+ scheduleCreateDialog(); |
+ break; |
+ |
+ case SCHEDULED_CREATE_DIALOG: |
+ // We've lost and re-acquired a token while waiting for the |
+ // dialog to show up. That's fine; we'll just use this token |
+ // when we finally create the dialog. |
+ break; |
+ |
+ case WAITING_FOR_SURFACE: |
+ // WAITING_FOR_SURFACE: |
+ // We're not supposed to be here. We had to have the token when |
+ // we created the dialog (SCHEDULED => WAITING), else we would |
+ // have transitioned to WAITING_FOR_TOKEN. If we lost the token |
+ // while waiting for the surface, then we'd transition back to |
+ // WAITING_FOR_TOKEN. The only way we get here is if we get |
+ // a token while we already have one. Panic. |
+ Log.e(TAG, "Acquired token while WAITING_FOR_SURFACE"); |
+ syntheticDestroyAndDismiss(); |
+ break; |
+ |
+ case HAVE_SURFACE: |
+ // Also shouldn't be here, by similar logic. Run away. |
+ Log.e(TAG, "Acquired token while HAVE_SURFACE"); |
+ syntheticDestroyAndDismiss(); |
+ break; |
+ } |
+ Log.d(TAG, "acquiredTokenLocked: new state: " + mState); |
+ } |
} |