diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c87ba657bf466..25b73ab401a8a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -673,6 +673,11 @@ android:name=".keyguard.KeyguardService" android:exported="true" /> + + true + + false + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index fe7911045dfcb..33f07c716f958 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java @@ -21,6 +21,7 @@ import android.app.Service; import com.android.systemui.ImageWallpaper; import com.android.systemui.SystemUIService; import com.android.systemui.doze.DozeService; +import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; import com.android.systemui.keyguard.KeyguardService; import com.android.systemui.screenrecord.RecordingService; @@ -53,6 +54,12 @@ public abstract class DefaultServiceBinder { @ClassKey(KeyguardService.class) public abstract Service bindKeyguardService(KeyguardService service); + /** */ + @Binds + @IntoMap + @ClassKey(DreamOverlayService.class) + public abstract Service bindDreamOverlayService(DreamOverlayService service); + /** */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 30844ccc877b5..11bee7ed0669c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -23,6 +23,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.accessibility.WindowMagnification; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.dreams.DreamOverlayRegistrant; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; @@ -188,4 +189,11 @@ public abstract class SystemUIBinder { @IntoMap @ClassKey(HomeSoundEffectController.class) public abstract SystemUI bindHomeSoundEffectController(HomeSoundEffectController sysui); + + /** Inject into DreamOverlay. */ + @Binds + @IntoMap + @ClassKey(DreamOverlayRegistrant.class) + public abstract SystemUI bindDreamOverlayRegistrant( + DreamOverlayRegistrant dreamOverlayRegistrant); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java new file mode 100644 index 0000000000000..20c46da14e637 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java @@ -0,0 +1,128 @@ +/* + * 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.dreams; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.PatternMatcher; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.service.dreams.DreamService; +import android.service.dreams.IDreamManager; +import android.util.Log; + +import com.android.systemui.R; +import com.android.systemui.SystemUI; +import com.android.systemui.dagger.qualifiers.Main; + +import javax.inject.Inject; + +/** + * {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be + * the designated dream overlay component. + */ +public class DreamOverlayRegistrant extends SystemUI { + private static final String TAG = "DreamOverlayRegistrant"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final IDreamManager mDreamManager; + private final ComponentName mOverlayServiceComponent; + private final Resources mResources; + private boolean mCurrentRegisteredState = false; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "package changed receiver - onReceive"); + } + + registerOverlayService(); + } + }; + + private void registerOverlayService() { + // Check to see if the service has been disabled by the user. In this case, we should not + // proceed modifying the enabled setting. + final PackageManager packageManager = mContext.getPackageManager(); + final int enabledState = + packageManager.getComponentEnabledSetting(mOverlayServiceComponent); + + + // TODO(b/204626521): We should not have to set the component enabled setting if the + // enabled config flag is properly applied based on the RRO. + if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + final int overlayState = mResources.getBoolean(R.bool.config_dreamOverlayServiceEnabled) + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + + if (overlayState != enabledState) { + packageManager + .setComponentEnabledSetting(mOverlayServiceComponent, overlayState, 0); + } + } + + // The overlay service is only registered when its component setting is enabled. + boolean register = packageManager.getComponentEnabledSetting(mOverlayServiceComponent) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + + if (mCurrentRegisteredState == register) { + return; + } + + mCurrentRegisteredState = register; + + try { + if (DEBUG) { + Log.d(TAG, mCurrentRegisteredState + ? "registering dream overlay service:" + mOverlayServiceComponent + : "clearing dream overlay service"); + } + + mDreamManager.registerDreamOverlayService( + mCurrentRegisteredState ? mOverlayServiceComponent : null); + } catch (RemoteException e) { + Log.e(TAG, "could not register dream overlay service:" + e); + } + } + + @Inject + public DreamOverlayRegistrant(Context context, @Main Resources resources) { + super(context); + mResources = resources; + mDreamManager = IDreamManager.Stub.asInterface( + ServiceManager.getService(DreamService.DREAM_SERVICE)); + mOverlayServiceComponent = new ComponentName(mContext, DreamOverlayService.class); + } + + @Override + public void start() { + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + filter.addDataSchemeSpecificPart(mOverlayServiceComponent.getPackageName(), + PatternMatcher.PATTERN_LITERAL); + // Note that we directly register the receiver here as data schemes are not supported by + // BroadcastDispatcher. + mContext.registerReceiver(mReceiver, filter); + + registerOverlayService(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java new file mode 100644 index 0000000000000..d37914a63a390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -0,0 +1,165 @@ +/* + * 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.dreams; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.ColorDrawable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.dagger.qualifiers.Main; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * The {@link DreamOverlayService} is responsible for placing overlays on top of a dream. The + * dream reaches directly out to the service with a Window reference (via LayoutParams), which the + * service uses to insert its own child Window into the dream's parent Window. + */ +public class DreamOverlayService extends android.service.dreams.DreamOverlayService { + private static final String TAG = "DreamOverlayService"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // The Context is used to construct the hosting constraint layout and child overlay views. + private final Context mContext; + // The Executor ensures actions and ui updates happen on the same thread. + private final Executor mExecutor; + + // The window is populated once the dream informs the service it has begun dreaming. + private Window mWindow; + private ConstraintLayout mLayout; + + // The service listens to view changes in order to declare that input occurring in areas outside + // the overlay should be passed through to the dream underneath. + private View.OnAttachStateChangeListener mRootViewAttachListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.getViewTreeObserver() + .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); + } + + @Override + public void onViewDetachedFromWindow(View v) { + v.getViewTreeObserver() + .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); + } + }; + + // A hook into the internal inset calculation where we declare the overlays as the only + // touchable regions. + private ViewTreeObserver.OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + if (mLayout != null) { + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + final Region region = new Region(); + for (int i = 0; i < mLayout.getChildCount(); i++) { + View child = mLayout.getChildAt(i); + final Rect rect = new Rect(); + child.getGlobalVisibleRect(rect); + region.op(rect, Region.Op.UNION); + } + + inoutInfo.touchableRegion.set(region); + } + } + }; + + @Override + public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { + mExecutor.execute(() -> addOverlayWindowLocked(layoutParams)); + } + + /** + * Inserts {@link Window} to host dream overlays into the dream's parent window. Must be called + * from the main executing thread. The window attributes closely mirror those that are set by + * the {@link android.service.dreams.DreamService} on the dream Window. + * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting + * into the dream window. + */ + private void addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) { + mWindow = new PhoneWindow(mContext); + mWindow.setAttributes(layoutParams); + mWindow.setWindowManager(null, layoutParams.token, "DreamOverlay", true); + + mWindow.setBackgroundDrawable(new ColorDrawable(0)); + + mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + // Hide all insets when the dream is showing + mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); + mWindow.setDecorFitsSystemWindows(false); + + if (DEBUG) { + Log.d(TAG, "adding overlay window to dream"); + } + + mLayout = new ConstraintLayout(mContext); + mLayout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mLayout.addOnAttachStateChangeListener(mRootViewAttachListener); + mWindow.setContentView(mLayout); + + final WindowManager windowManager = mContext.getSystemService(WindowManager.class); + windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); + } + + @VisibleForTesting + protected void addOverlay(OverlayProvider provider) { + provider.onCreateOverlay(mContext, + (view, layoutParams) -> { + // Always move UI related work to the main thread. + mExecutor.execute(() -> { + if (mLayout == null) { + return; + } + + mLayout.addView(view, layoutParams); + }); + }, + () -> { + // The Callback is set on the main thread. + mExecutor.execute(() -> { + requestExit(); + }); + }); + } + + @Inject + public DreamOverlayService(Context context, @Main Executor executor) { + mContext = context; + mExecutor = executor; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java new file mode 100644 index 0000000000000..08f0f3507e3e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java @@ -0,0 +1,48 @@ +/* + * 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.dreams; + +import android.view.View; + +/** + * A collection of interfaces related to hosting an overlay. + */ +public abstract class OverlayHost { + /** + * An interface for the callback from the overlay provider to indicate when the overlay is + * ready. + */ + public interface CreationCallback { + /** + * Called to inform the overlay view is ready to be placed within the visual space. + * @param view The view representing the overlay. + * @param layoutParams The parameters to create the view with. + */ + void onCreated(View view, OverlayHostView.LayoutParams layoutParams); + } + + /** + * An interface for the callback from the overlay provider to signal interactions in the + * overlay. + */ + public interface InteractionCallback { + /** + * Called to signal the calling overlay would like to exit the dream. + */ + void onExit(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java new file mode 100644 index 0000000000000..7870426c78f1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java @@ -0,0 +1,43 @@ +/* + * 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.dreams; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +/** + * {@link OverlayHostView} is the container view for housing overlays ontop of a dream. + */ +public class OverlayHostView extends ConstraintLayout { + public OverlayHostView(Context context) { + super(context, null); + } + + public OverlayHostView(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + public OverlayHostView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr, 0); + } + + public OverlayHostView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java new file mode 100644 index 0000000000000..f20802527d739 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java @@ -0,0 +1,36 @@ +/* + * 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.dreams; + +import android.content.Context; + +/** + * {@link OverlayProvider} is an interface for defining entities that can supply overlays to show + * over a dream. Presentation components such as the {@link DreamOverlayService} supply + * implementations with the necessary context for constructing such overlays. + */ +public interface OverlayProvider { + /** + * Called when the {@link OverlayHost} requests the associated overlay be produced. + * + * @param context The {@link Context} used to construct the view. + * @param creationCallback The callback to inform when the overlay has been created. + * @param interactionCallback The callback to inform when the overlay has been interacted with. + */ + void onCreateOverlay(Context context, OverlayHost.CreationCallback creationCallback, + OverlayHost.InteractionCallback interactionCallback); +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java new file mode 100644 index 0000000000000..6223048f0ce5e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -0,0 +1,120 @@ +/* + * 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.dreams; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; + +import android.content.Intent; +import android.os.IBinder; +import android.service.dreams.IDreamOverlay; +import android.service.dreams.IDreamOverlayCallback; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowManagerImpl; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.utils.leaks.LeakCheckedTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DreamOverlayServiceTest extends SysuiTestCase { + private FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + + @Rule + public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); + + @Rule + public SysuiTestableContext mContext = new SysuiTestableContext( + InstrumentationRegistry.getContext(), mLeakCheck); + + WindowManager.LayoutParams mWindowParams = new WindowManager.LayoutParams(); + + @Mock + IDreamOverlayCallback mDreamOverlayCallback; + + @Mock + WindowManagerImpl mWindowManager; + + @Mock + OverlayProvider mProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(WindowManager.class, mWindowManager); + } + + @Test + public void testInteraction() throws Exception { + final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor); + final IBinder proxy = service.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + clearInvocations(mWindowManager); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback); + mMainExecutor.runAllReady(); + verify(mWindowManager).addView(any(), any()); + + // Add overlay. + service.addOverlay(mProvider); + mMainExecutor.runAllReady(); + + final ArgumentCaptor creationCallbackCapture = + ArgumentCaptor.forClass(OverlayHost.CreationCallback.class); + final ArgumentCaptor interactionCallbackCapture = + ArgumentCaptor.forClass(OverlayHost.InteractionCallback.class); + + // Ensure overlay provider is asked to create view. + verify(mProvider).onCreateOverlay(any(), creationCallbackCapture.capture(), + interactionCallbackCapture.capture()); + mMainExecutor.runAllReady(); + + // Inform service of overlay view creation. + final View view = new View(mContext); + creationCallbackCapture.getValue().onCreated(view, new ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + )); + + // Ask service to exit. + interactionCallbackCapture.getValue().onExit(); + mMainExecutor.runAllReady(); + + // Ensure service informs dream host of exit. + verify(mDreamOverlayCallback).onExitRequested(); + } +}