diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalHostViewController.java b/packages/SystemUI/src/com/android/systemui/communal/CommunalHostViewController.java index 213d13bed4e22..5f4c4df30450e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalHostViewController.java @@ -67,6 +67,8 @@ public class CommunalHostViewController extends ViewController private static final int SHOW_COMMUNAL_VIEW_INVALID_STATES = STATE_DOZING | STATE_BOUNCER_SHOWING | STATE_KEYGUARD_OCCLUDED; + private ViewController mCommunalViewController; + private KeyguardUpdateMonitorCallback mKeyguardUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -220,7 +222,7 @@ public class CommunalHostViewController extends ViewController final Context context = mView.getContext(); - final ListenableFuture listenableFuture = + final ListenableFuture listenableFuture = currentSource.requestCommunalView(context); if (listenableFuture == null) { @@ -230,11 +232,14 @@ public class CommunalHostViewController extends ViewController listenableFuture.addListener(() -> { try { - final View view = listenableFuture.get(); - view.setLayoutParams(new ViewGroup.LayoutParams( + final CommunalSource.CommunalViewResult result = listenableFuture.get(); + result.view.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mView.addView(view); + mView.addView(result.view); + + mCommunalViewController = result.viewController; + mCommunalViewController.init(); } catch (Exception e) { Log.e(TAG, "could not obtain communal view through callback:" + e); } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSource.java b/packages/SystemUI/src/com/android/systemui/communal/CommunalSource.java index 742e4b16a90cb..99c311e37217d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSource.java +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSource.java @@ -19,6 +19,8 @@ package com.android.systemui.communal; import android.content.Context; import android.view.View; +import com.android.systemui.util.ViewController; + import com.google.common.util.concurrent.ListenableFuture; /** @@ -27,14 +29,39 @@ import com.google.common.util.concurrent.ListenableFuture; * Callbacks may also be registered to listen to state changes. */ public interface CommunalSource { + /** + * {@link CommunalViewResult} is handed back from {@link #requestCommunalView(Context)} and + * contains the view to be displayed and its associated controller. + */ + class CommunalViewResult { + /** + * The resulting communal view. + */ + public final View view; + /** + * The controller for the communal view. + */ + public final ViewController viewController; + + /** + * The default constructor for {@link CommunalViewResult}. + * @param view The communal view. + * @param viewController The communal view's controller. + */ + public CommunalViewResult(View view, ViewController viewController) { + this.view = view; + this.viewController = viewController; + } + } + /** * Requests a communal surface that can be displayed inside {@link CommunalHostView}. * * @param context The {@link View} {@link Context} to build the resulting view from - * @return A future that can be listened upon for the resulting {@link View}. The value will be - * {@code null} in case of a failure. + * @return A future that can be listened upon for the resulting {@link CommunalViewResult}. The + * value will be {@code null} in case of a failure. */ - ListenableFuture requestCommunalView(Context context); + ListenableFuture requestCommunalView(Context context); /** * Adds a {@link Callback} to receive future status updates regarding this diff --git a/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSourceImpl.java b/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSourceImpl.java index d94b8ac565ab3..8364369006109 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSourceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSourceImpl.java @@ -21,7 +21,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.view.SurfaceControlViewHost; -import android.view.View; +import android.view.SurfaceView; import androidx.concurrent.futures.CallbackToFutureAdapter; @@ -106,13 +106,15 @@ public class CommunalSourceImpl implements CommunalSource { } @Override - public ListenableFuture requestCommunalView(Context context) { + public ListenableFuture requestCommunalView(Context context) { if (DEBUG) { Log.d(TAG, "Received request for communal view"); } - ListenableFuture packageFuture = + ListenableFuture packageFuture = CallbackToFutureAdapter.getFuture(completer -> { - completer.set(new CommunalSurfaceView(context, mMainExecutor, this)); + final SurfaceView view = new SurfaceView(context); + completer.set(new CommunalViewResult(view, + new CommunalSurfaceViewController(view, mMainExecutor, this))); return "CommunalSourceImpl::requestCommunalSurface::getCommunalSurface"; }); diff --git a/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceView.java b/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceView.java deleted file mode 100644 index 67458a812f7e7..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceView.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2021 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.systemui.communal.service; - -import android.content.Context; -import android.util.Log; -import android.view.SurfaceControlViewHost; -import android.view.SurfaceHolder; -import android.view.SurfaceView; - -import androidx.annotation.NonNull; - -import com.google.common.util.concurrent.ListenableFuture; - -import java.util.concurrent.Executor; - -/** - * {@link CommunalSurfaceView} can be used to display remote surfaces returned by the - * {@link CommunalService}. The necessary information for the request are derived from the view's - * events from being attached to the parent container. - */ -public class CommunalSurfaceView extends SurfaceView { - private static final String TAG = "CommunalSurfaceView"; - private static final boolean DEBUG = false; - private final Executor mMainExecutor; - private final CommunalSourceImpl mSource; - - public CommunalSurfaceView(Context context, Executor executor, CommunalSourceImpl source) { - super(context); - mSource = source; - mMainExecutor = executor; - - getHolder().addCallback(new SurfaceHolder.Callback() { - @Override - public void surfaceCreated(@NonNull SurfaceHolder holder) { - onSurfaceCreated(); - } - - @Override - public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, - int height) { - - } - - @Override - public void surfaceDestroyed(@NonNull SurfaceHolder holder) { - - } - }); - } - - private void onSurfaceCreated() { - setWillNotDraw(false); - - final ListenableFuture surfaceFuture = - mSource.requestCommunalSurface(this.getHostToken(), - getDisplay().getDisplayId(), getMeasuredWidth(), getMeasuredHeight()); - - surfaceFuture.addListener(new Runnable() { - @Override - public void run() { - try { - SurfaceControlViewHost.SurfacePackage surfacePackage = surfaceFuture.get(); - - if (DEBUG) { - Log.d(TAG, "Received surface package:" + surfacePackage); - } - - if (surfacePackage != null) { - setChildSurfacePackage(surfacePackage); - setZOrderOnTop(true); - postInvalidate(); - } else { - Log.e(TAG, "couldn't get the surface package"); - } - } catch (Exception e) { - Log.e(TAG, "An error occurred retrieving the future result:" + e); - } - } - }, mMainExecutor); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceViewController.java b/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceViewController.java new file mode 100644 index 0000000000000..15cd7071d8d2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/service/CommunalSurfaceViewController.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 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.systemui.communal.service; + +import android.annotation.IntDef; +import android.util.Log; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; + +import com.android.systemui.util.ViewController; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.Executor; + +/** + * {@link CommunalSurfaceViewController} coordinates requesting communal surfaces to populate a + * {@link SurfaceView} with. + */ +public class CommunalSurfaceViewController extends ViewController { + private static final String TAG = "CommunalSurfaceViewCtlr"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Executor mMainExecutor; + private final CommunalSourceImpl mSource; + + @IntDef({STATE_SURFACE_CREATED, STATE_SURFACE_VIEW_ATTACHED}) + private @interface State {} + + private static final int STATE_SURFACE_CREATED = 1 << 0; + private static final int STATE_SURFACE_VIEW_ATTACHED = 1 << 1; + + private static final int STATE_CAN_SHOW_SURFACE = + STATE_SURFACE_CREATED | STATE_SURFACE_VIEW_ATTACHED; + + private int mCurrentState; + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + setState(STATE_SURFACE_CREATED, true); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, + int height) { + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + setState(STATE_SURFACE_CREATED, false); + } + }; + + protected CommunalSurfaceViewController(SurfaceView view, Executor executor, + CommunalSourceImpl source) { + super(view); + mSource = source; + mMainExecutor = executor; + } + + @Override + public void init() { + super.init(); + mView.getHolder().addCallback(mSurfaceHolderCallback); + } + + private void setState(@State int state, boolean enabled) { + if (DEBUG) { + Log.d(TAG, "setState. state:" + state + " enable:" + enabled); + } + + final int newState = enabled ? mCurrentState | state : mCurrentState & ~state; + + // no new state is available + if (newState == mCurrentState) { + return; + } + + if (DEBUG) { + Log.d(TAG, "setState. new state:" + mCurrentState); + } + + mCurrentState = newState; + + if (newState == STATE_CAN_SHOW_SURFACE) { + showSurface(); + } + } + + private void showSurface() { + mView.setWillNotDraw(false); + + final ListenableFuture surfaceFuture = + mSource.requestCommunalSurface(mView.getHostToken(), + mView.getDisplay().getDisplayId(), mView.getMeasuredWidth(), + mView.getMeasuredHeight()); + + surfaceFuture.addListener(new Runnable() { + @Override + public void run() { + try { + // If the request is received after detached, ignore. + if (!mView.isAttachedToWindow()) { + return; + } + + SurfaceControlViewHost.SurfacePackage surfacePackage = surfaceFuture.get(); + + if (DEBUG) { + Log.d(TAG, "Received surface package:" + surfacePackage); + } + + if (surfacePackage != null) { + mView.setChildSurfacePackage(surfacePackage); + mView.setZOrderOnTop(true); + mView.postInvalidate(); + } else { + Log.e(TAG, "couldn't get the surface package"); + } + } catch (Exception e) { + Log.e(TAG, "An error occurred retrieving the future result:" + e); + } + } + }, mMainExecutor); + } + + @Override + protected void onViewAttached() { + setState(STATE_SURFACE_VIEW_ATTACHED, true); + } + + @Override + protected void onViewDetached() { + setState(STATE_SURFACE_VIEW_ATTACHED, false); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/service/CommunalSurfaceViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/service/CommunalSurfaceViewControllerTest.java new file mode 100644 index 0000000000000..8a2f7361eb1ee --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/service/CommunalSurfaceViewControllerTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2021 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.systemui.communal.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.IBinder; +import android.view.Display; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import com.google.common.util.concurrent.SettableFuture; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class CommunalSurfaceViewControllerTest extends SysuiTestCase { + private static final int MEASURED_HEIGHT = 200; + private static final int MEASURED_WIDTH = 500; + private static final int DISPLAY_ID = 3; + + @Mock + private Display mDisplay; + + @Mock + private IBinder mHostToken; + + @Mock + private SurfaceView mSurfaceView; + + @Mock + private SurfaceHolder mSurfaceHolder; + + @Mock + private CommunalSourceImpl mCommunalSource; + + @Mock + private SurfaceControlViewHost.SurfacePackage mSurfacePackage; + + private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + + private SurfaceHolder.Callback mCallback; + + private CommunalSurfaceViewController mController; + + private SettableFuture mPackageFuture; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + final ArgumentCaptor callbackCapture = + ArgumentCaptor.forClass(SurfaceHolder.Callback.class); + when(mSurfaceView.getHolder()).thenReturn(mSurfaceHolder); + when(mSurfaceView.getDisplay()).thenReturn(mDisplay); + when(mDisplay.getDisplayId()).thenReturn(DISPLAY_ID); + when(mSurfaceView.getHostToken()).thenReturn(mHostToken); + when(mSurfaceView.getMeasuredWidth()).thenReturn(MEASURED_WIDTH); + when(mSurfaceView.getMeasuredHeight()).thenReturn(MEASURED_HEIGHT); + when(mSurfaceView.isAttachedToWindow()).thenReturn(false); + mController = new CommunalSurfaceViewController(mSurfaceView, mFakeExecutor, + mCommunalSource); + mController.init(); + verify(mSurfaceHolder).addCallback(callbackCapture.capture()); + mCallback = callbackCapture.getValue(); + + mPackageFuture = SettableFuture.create(); + + when(mCommunalSource.requestCommunalSurface(any(), anyInt(), anyInt(), anyInt())) + .thenReturn(mPackageFuture); + } + + @Test + public void testSetSurfacePackage() { + // There should be no requests without the proper state. + verify(mCommunalSource, times(0)) + .requestCommunalSurface(any(), anyInt(), anyInt(), anyInt()); + + // The full state must be present to make a request. + mController.onViewAttached(); + verify(mCommunalSource, times(0)) + .requestCommunalSurface(any(), anyInt(), anyInt(), anyInt()); + + // Request surface view once all conditions are met. + mCallback.surfaceCreated(mSurfaceHolder); + verify(mCommunalSource) + .requestCommunalSurface(mHostToken, DISPLAY_ID, MEASURED_WIDTH, MEASURED_HEIGHT); + + when(mSurfaceView.isAttachedToWindow()).thenReturn(true); + + // Respond to request. + mPackageFuture.set(mSurfacePackage); + mFakeExecutor.runAllReady(); + + + // Make sure SurfaceView is set. + verify(mSurfaceView).setChildSurfacePackage(mSurfacePackage); + verify(mSurfaceView).setZOrderOnTop(true); + verify(mSurfaceView).setWillNotDraw(false); + } +}