DO NOT MERGE Test sidecar OEM implementation for WM support library
This provides an example implementation of the Sidecar interface
for the Window support library, which allows mocking a foldable
device by reading its current configuration from settings.
This implementation also makes sure that the reported display
features are adjusted to the window position and coordinate space,
as required by the Sidecar interface contract.
The reported values can be updated by using the following shell
commands:
settings put global device_posture <posture>
settings put global display_features <type>-[<sX>,<sY>,<eX>,<eY>]
Bug: 146188055
Test: Manual, see instructions doc
Change-Id: I4764298d7633bc3ef574b5cbc2dee256e9cf3f03
(cherry picked from commit d66f77b0b762091464c850bb79744ea5d07e0053)
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp
new file mode 100644
index 0000000..308c1a5
--- /dev/null
+++ b/libs/WindowManager/Jetpack/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 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.
+
+android_library_import {
+ name: "window-extensions",
+ aars: ["window-extensions-release.aar"],
+ sdk_version: "current",
+}
+
+java_library {
+ name: "androidx.window.extensions",
+ srcs: ["src/**/*.java"],
+ static_libs: ["window-extensions"],
+ installable: true,
+ sdk_version: "core_platform",
+ vendor: true,
+ libs: ["framework", "androidx.annotation_annotation",],
+ required: ["androidx.window.extensions.xml",],
+}
+
+prebuilt_etc {
+ name: "androidx.window.extensions.xml",
+ vendor: true,
+ sub_dir: "permissions",
+ src: "androidx.window.extensions.xml",
+ filename_from_src: true,
+}
diff --git a/libs/WindowManager/Jetpack/androidx.window.extensions.xml b/libs/WindowManager/Jetpack/androidx.window.extensions.xml
new file mode 100644
index 0000000..1f0ff66
--- /dev/null
+++ b/libs/WindowManager/Jetpack/androidx.window.extensions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 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.
+ -->
+<permissions>
+ <library
+ name="androidx.window.extensions"
+ file="/vendor/framework/androidx.window.extensions.jar"/>
+</permissions>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionHelper.java
new file mode 100644
index 0000000..c4f11a0
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionHelper.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020 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 androidx.window.extensions;
+
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.IBinder;
+import android.view.DisplayInfo;
+import android.view.Surface;
+
+/**
+ * Toolkit class for calculation of the display feature bounds within the window.
+ * NOTE: This sample implementation only works for Activity windows, because there is no public APIs
+ * to obtain layout params or bounds for arbitrary windows.
+ */
+class ExtensionHelper {
+ /**
+ * Rotate the input rectangle specified in default display orientation to the current display
+ * rotation.
+ */
+ static void rotateRectToDisplayRotation(Rect inOutRect, int displayId) {
+ DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance();
+ DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId);
+ int rotation = displayInfo.rotation;
+
+ boolean isSideRotation = rotation == ROTATION_90 || rotation == ROTATION_270;
+ int displayWidth = isSideRotation ? displayInfo.logicalHeight : displayInfo.logicalWidth;
+ int displayHeight = isSideRotation ? displayInfo.logicalWidth : displayInfo.logicalHeight;
+
+ inOutRect.intersect(0, 0, displayWidth, displayHeight);
+
+ rotateBounds(inOutRect, displayWidth, displayHeight, rotation);
+ }
+
+ /**
+ * Rotate the input rectangle within parent bounds for a given delta.
+ */
+ private static void rotateBounds(Rect inOutRect, int parentWidth, int parentHeight,
+ @Surface.Rotation int delta) {
+ int origLeft = inOutRect.left;
+ switch (delta) {
+ case ROTATION_0:
+ return;
+ case ROTATION_90:
+ inOutRect.left = inOutRect.top;
+ inOutRect.top = parentWidth - inOutRect.right;
+ inOutRect.right = inOutRect.bottom;
+ inOutRect.bottom = parentWidth - origLeft;
+ return;
+ case ROTATION_180:
+ inOutRect.left = parentWidth - inOutRect.right;
+ inOutRect.right = parentWidth - origLeft;
+ return;
+ case ROTATION_270:
+ inOutRect.left = parentHeight - inOutRect.bottom;
+ inOutRect.bottom = inOutRect.right;
+ inOutRect.right = parentHeight - inOutRect.top;
+ inOutRect.top = origLeft;
+ return;
+ }
+ }
+
+ /** Transform rectangle from absolute coordinate space to the window coordinate space. */
+ static void transformToWindowSpaceRect(Rect inOutRect, IBinder windowToken) {
+ Rect windowRect = getWindowRect(windowToken);
+ if (windowRect == null) {
+ inOutRect.setEmpty();
+ return;
+ }
+ if (!Rect.intersects(inOutRect, windowRect)) {
+ inOutRect.setEmpty();
+ return;
+ }
+ inOutRect.intersect(windowRect);
+ inOutRect.offset(-windowRect.left, -windowRect.top);
+ }
+
+ /**
+ * Get the current window bounds in absolute coordinates.
+ * NOTE: Only works with Activity windows.
+ */
+ private static Rect getWindowRect(IBinder windowToken) {
+ Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+ final Rect windowRect = new Rect();
+ if (activity != null) {
+ activity.getWindow().getDecorView().getWindowDisplayFrame(windowRect);
+ }
+ return windowRect;
+ }
+
+ /**
+ * Check if this window is an Activity window that is in multi-window mode.
+ */
+ static boolean isInMultiWindow(IBinder windowToken) {
+ Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+ return activity != null && activity.isInMultiWindowMode();
+ }
+
+ /**
+ * Get the id of the parent display for the window.
+ * NOTE: Only works with Activity windows.
+ */
+ static int getWindowDisplay(IBinder windowToken) {
+ Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+ return activity != null
+ ? activity.getWindowManager().getDefaultDisplay().getDisplayId() : INVALID_DISPLAY;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
new file mode 100644
index 0000000..47349f1
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 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 androidx.window.extensions;
+
+import android.content.Context;
+
+/**
+ * Provider class that will instantiate the library implementation. It must be included in the
+ * vendor library, and the vendor implementation must match the signature of this class.
+ */
+public class ExtensionProvider {
+
+ /**
+ * The support library will instantiate the vendor implementation using this interface.
+ * @return An implementation of {@link ExtensionInterface}.
+ */
+ public static ExtensionInterface getExtensionImpl(Context context) {
+ return new SettingsExtensionImpl(context);
+ }
+
+ /**
+ * The support library will use this method to check API version compatibility.
+ * @return API version string in MAJOR.MINOR.PATCH-description format.
+ */
+ public static String getApiVersion() {
+ return "1.0.0-settings_sample";
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SettingsExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SettingsExtensionImpl.java
new file mode 100644
index 0000000..7a3fbf3
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SettingsExtensionImpl.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2020 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 androidx.window.extensions;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.window.extensions.ExtensionHelper.getWindowDisplay;
+import static androidx.window.extensions.ExtensionHelper.isInMultiWindow;
+import static androidx.window.extensions.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.extensions.ExtensionHelper.transformToWindowSpaceRect;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class SettingsExtensionImpl extends StubExtension {
+ private static final String TAG = "SettingsExtension";
+
+ private static final String DEVICE_POSTURE = "device_posture";
+ private static final String DISPLAY_FEATURES = "display_features";
+
+ private static final Pattern FEATURE_PATTERN =
+ Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]");
+
+ private static final String FEATURE_TYPE_FOLD = "fold";
+ private static final String FEATURE_TYPE_HINGE = "hinge";
+
+ private Context mContext;
+ private SettingsObserver mSettingsObserver;
+
+ final class SettingsObserver extends ContentObserver {
+ private final Uri mDevicePostureUri =
+ Settings.Global.getUriFor(DEVICE_POSTURE);
+ private final Uri mDisplayFeaturesUri =
+ Settings.Global.getUriFor(DISPLAY_FEATURES);
+ private final ContentResolver mResolver = mContext.getContentResolver();
+ private boolean mRegisteredObservers;
+
+
+ private SettingsObserver() {
+ super(new Handler(Looper.getMainLooper()));
+ }
+
+ private void registerObserversIfNeeded() {
+ if (mRegisteredObservers) {
+ return;
+ }
+ mRegisteredObservers = true;
+ mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendents */,
+ this /* ContentObserver */);
+ mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendents */,
+ this /* ContentObserver */);
+ }
+
+ private void unregisterObserversIfNeeded() {
+ if (!mRegisteredObservers) {
+ return;
+ }
+ mRegisteredObservers = false;
+ mResolver.unregisterContentObserver(this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (uri == null) {
+ return;
+ }
+
+ if (mDevicePostureUri.equals(uri)) {
+ updateDevicePosture();
+ return;
+ }
+ if (mDisplayFeaturesUri.equals(uri)) {
+ updateDisplayFeatures();
+ return;
+ }
+ }
+ }
+
+ SettingsExtensionImpl(Context context) {
+ mContext = context;
+ mSettingsObserver = new SettingsObserver();
+ }
+
+ private void updateDevicePosture() {
+ updateDeviceState(getDeviceState());
+ }
+
+ /** Update display features with values read from settings. */
+ private void updateDisplayFeatures() {
+ for (IBinder windowToken : getWindowsListeningForLayoutChanges()) {
+ ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken);
+ updateWindowLayout(windowToken, newLayout);
+ }
+ }
+
+ @NonNull
+ @Override
+ public ExtensionDeviceState getDeviceState() {
+ ContentResolver resolver = mContext.getContentResolver();
+ int posture = Settings.Global.getInt(resolver, DEVICE_POSTURE,
+ ExtensionDeviceState.POSTURE_UNKNOWN);
+ return new ExtensionDeviceState(posture);
+ }
+
+ @NonNull
+ @Override
+ public ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
+ List<ExtensionDisplayFeature> displayFeatures = readDisplayFeatures(windowToken);
+ return new ExtensionWindowLayoutInfo(displayFeatures);
+ }
+
+ private List<ExtensionDisplayFeature> readDisplayFeatures(IBinder windowToken) {
+ List<ExtensionDisplayFeature> features = new ArrayList<ExtensionDisplayFeature>();
+ int displayId = getWindowDisplay(windowToken);
+ if (displayId != DEFAULT_DISPLAY) {
+ Log.w(TAG, "This sample doesn't support display features on secondary displays");
+ return features;
+ }
+
+ ContentResolver resolver = mContext.getContentResolver();
+ final String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES);
+ if (isInMultiWindow(windowToken)) {
+ // It is recommended not to report any display features in multi-window mode, since it
+ // won't be possible to synchronize the display feature positions with window movement.
+ return features;
+ }
+ if (TextUtils.isEmpty(displayFeaturesString)) {
+ return features;
+ }
+
+ String[] featureStrings = displayFeaturesString.split(";");
+ for (String featureString : featureStrings) {
+ Matcher featureMatcher = FEATURE_PATTERN.matcher(featureString);
+ if (!featureMatcher.matches()) {
+ Log.e(TAG, "Malformed feature description format: " + featureString);
+ continue;
+ }
+ try {
+ String featureType = featureMatcher.group(1);
+ int type;
+ switch (featureType) {
+ case FEATURE_TYPE_FOLD:
+ type = ExtensionDisplayFeature.TYPE_FOLD;
+ break;
+ case FEATURE_TYPE_HINGE:
+ type = ExtensionDisplayFeature.TYPE_HINGE;
+ break;
+ default: {
+ Log.e(TAG, "Malformed feature type: " + featureType);
+ continue;
+ }
+ }
+
+ int left = Integer.parseInt(featureMatcher.group(2));
+ int top = Integer.parseInt(featureMatcher.group(3));
+ int right = Integer.parseInt(featureMatcher.group(4));
+ int bottom = Integer.parseInt(featureMatcher.group(5));
+ Rect featureRect = new Rect(left, top, right, bottom);
+ rotateRectToDisplayRotation(featureRect, displayId);
+ transformToWindowSpaceRect(featureRect, windowToken);
+ if (!featureRect.isEmpty()) {
+ ExtensionDisplayFeature feature =
+ new ExtensionDisplayFeature(featureRect, type);
+ features.add(feature);
+ } else {
+ Log.w(TAG, "Failed to adjust feature to window");
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Malformed feature description: " + featureString);
+ }
+ }
+ return features;
+ }
+
+ @Override
+ protected void onListenersChanged() {
+ if (mSettingsObserver == null) {
+ return;
+ }
+
+ if (hasListeners()) {
+ mSettingsObserver.registerObserversIfNeeded();
+ } else {
+ mSettingsObserver.unregisterObserversIfNeeded();
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
new file mode 100644
index 0000000..0ebbb86
--- /dev/null
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ