From 336f9fdf8610c78a90e148c8e8ea5c73f80a252e Mon Sep 17 00:00:00 2001 From: Andrii Kulian Date: Wed, 23 Dec 2020 20:00:10 -0800 Subject: [PATCH] Add sample WM Jetpack Extensions impl Moving some of the common bits of Sidecar and Extensions into helper classes and adding both to the reference repo. Bug: 154172225 Test: m androidx.window.extensions Test: Manual, by including extensions in the emulator image Test: gradlew window:window:cAT Change-Id: I44ff06b06b2202a5d687216e017528098dc14d1d --- libs/WindowManager/Jetpack/Android.bp | 31 ++- .../Jetpack/androidx.window.extensions.xml | 21 ++ .../window/extensions/ExtensionProvider.java | 41 +++ .../extensions/SampleExtensionImpl.java | 109 ++++++++ .../window/extensions/StubExtension.java | 86 +++++++ .../window/sidecar/SampleSidecarImpl.java | 120 +++++++++ .../window/sidecar/SettingsSidecarImpl.java | 233 ------------------ .../window/sidecar/SidecarProvider.java | 2 +- .../window/util/BaseDisplayFeature.java | 52 ++++ .../ExtensionHelper.java} | 57 ++--- .../window/util/SettingsConfigProvider.java | 196 +++++++++++++++ .../Jetpack/window-extensions-release.aar | Bin 0 -> 9182 bytes 12 files changed, 680 insertions(+), 268 deletions(-) create mode 100644 libs/WindowManager/Jetpack/androidx.window.extensions.xml create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java delete mode 100644 libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java rename libs/WindowManager/Jetpack/src/androidx/window/{sidecar/SidecarHelper.java => util/ExtensionHelper.java} (62%) create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java create mode 100644 libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp index 7fbbb61e469d4..4612ba28d1db6 100644 --- a/libs/WindowManager/Jetpack/Android.bp +++ b/libs/WindowManager/Jetpack/Android.bp @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Sidecar android_library_import { name: "window-sidecar", aars: ["window-sidecar-release.aar"], @@ -20,7 +21,7 @@ android_library_import { java_library { name: "androidx.window.sidecar", - srcs: ["src/**/*.java"], + srcs: ["src/androidx/window/sidecar/**/*.java", "src/androidx/window/util/**/*.java"], static_libs: ["window-sidecar"], installable: true, sdk_version: "core_platform", @@ -36,3 +37,31 @@ prebuilt_etc { src: "androidx.window.sidecar.xml", filename_from_src: true, } + +// Extensions +// NOTE: This module is still under active development and must not +// be used in production. Use 'androidx.window.sidecar' instead. +android_library_import { + name: "window-extensions", + aars: ["window-extensions-release.aar"], + sdk_version: "current", +} + +java_library { + name: "androidx.window.extensions", + srcs: ["src/androidx/window/extensions/**/*.java", "src/androidx/window/util/**/*.java"], + static_libs: ["window-extensions"], + installable: true, + sdk_version: "core_platform", + system_ext_specific: true, + libs: ["framework", "androidx.annotation_annotation",], + required: ["androidx.window.extensions.xml",], +} + +prebuilt_etc { + name: "androidx.window.extensions.xml", + system_ext_specific: 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 0000000000000..34264aa3ade3f --- /dev/null +++ b/libs/WindowManager/Jetpack/androidx.window.extensions.xml @@ -0,0 +1,21 @@ + + + + + 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 0000000000000..b7a60392c5125 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java @@ -0,0 +1,41 @@ +/* + * 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 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 { + /** + * Provides a simple implementation of {@link ExtensionInterface} that can be replaced by + * an OEM by overriding this method. + */ + public static ExtensionInterface getExtensionImpl(Context context) { + return new SampleExtensionImpl(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/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java new file mode 100644 index 0000000000000..0bf6965beb5ff --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java @@ -0,0 +1,109 @@ +/* + * 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 androidx.window.extensions; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.window.util.BaseDisplayFeature; +import androidx.window.util.SettingsConfigProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Reference implementation of androidx.window.extensions OEM interface for use with + * WindowManager Jetpack. + * + * NOTE: This version is a work in progress and under active development. It MUST NOT be used in + * production builds since the interface can still change before reaching stable version. + * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead. + */ +class SampleExtensionImpl extends StubExtension implements + SettingsConfigProvider.StateChangeCallback { + private static final String TAG = "SampleExtension"; + + private final SettingsConfigProvider mConfigProvider; + + SampleExtensionImpl(Context context) { + mConfigProvider = new SettingsConfigProvider(context, this); + } + + @Override + public void onDevicePostureChanged() { + updateDeviceState(new ExtensionDeviceState(mConfigProvider.getDeviceState())); + } + + @Override + public void onDisplayFeaturesChanged() { + for (Activity activity : getActivitiesListeningForLayoutChanges()) { + ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity); + updateWindowLayout(activity, newLayout); + } + } + + @NonNull + private ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { + List displayFeatures = getDisplayFeatures(activity); + return new ExtensionWindowLayoutInfo(displayFeatures); + } + + private List getDisplayFeatures(@NonNull Activity activity) { + List features = new ArrayList<>(); + int displayId = activity.getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + Log.w(TAG, "This sample doesn't support display features on secondary displays"); + return features; + } + + if (activity.isInMultiWindowMode()) { + // 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; + } + + List storedFeatures = mConfigProvider.getDisplayFeatures(); + for (BaseDisplayFeature baseFeature : storedFeatures) { + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(), + baseFeature.getState())); + } + return features; + } + + @Override + protected void onListenersChanged() { + if (hasListeners()) { + mConfigProvider.registerObserversIfNeeded(); + } else { + mConfigProvider.unregisterObserversIfNeeded(); + } + + onDevicePostureChanged(); + onDisplayFeaturesChanged(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java new file mode 100644 index 0000000000000..b0895efc75a9e --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java @@ -0,0 +1,86 @@ +/* + * 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 androidx.window.extensions; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import java.util.HashSet; +import java.util.Set; + +/** + * Basic implementation of the {@link ExtensionInterface}. An OEM can choose to use it as the base + * class for their implementation. + */ +abstract class StubExtension implements ExtensionInterface { + + private ExtensionCallback mExtensionCallback; + private final Set mWindowLayoutChangeListenerActivities = new HashSet<>(); + private boolean mDeviceStateChangeListenerRegistered; + + StubExtension() { + } + + @Override + public void setExtensionCallback(@NonNull ExtensionCallback extensionCallback) { + this.mExtensionCallback = extensionCallback; + } + + @Override + public void onWindowLayoutChangeListenerAdded(@NonNull Activity activity) { + this.mWindowLayoutChangeListenerActivities.add(activity); + this.onListenersChanged(); + } + + @Override + public void onWindowLayoutChangeListenerRemoved(@NonNull Activity activity) { + this.mWindowLayoutChangeListenerActivities.remove(activity); + this.onListenersChanged(); + } + + @Override + public void onDeviceStateListenersChanged(boolean isEmpty) { + this.mDeviceStateChangeListenerRegistered = !isEmpty; + this.onListenersChanged(); + } + + void updateDeviceState(ExtensionDeviceState newState) { + if (this.mExtensionCallback != null) { + mExtensionCallback.onDeviceStateChanged(newState); + } + } + + void updateWindowLayout(@NonNull Activity activity, + @NonNull ExtensionWindowLayoutInfo newLayout) { + if (this.mExtensionCallback != null) { + mExtensionCallback.onWindowLayoutChanged(activity, newLayout); + } + } + + @NonNull + Set getActivitiesListeningForLayoutChanges() { + return mWindowLayoutChangeListenerActivities; + } + + protected boolean hasListeners() { + return !mWindowLayoutChangeListenerActivities.isEmpty() + || mDeviceStateChangeListenerRegistered; + } + + protected abstract void onListenersChanged(); +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java new file mode 100644 index 0000000000000..1094a0e2b4da9 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -0,0 +1,120 @@ +/* + * 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. + */ + +package androidx.window.sidecar; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; + +import android.app.Activity; +import android.app.ActivityThread; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.window.util.BaseDisplayFeature; +import androidx.window.util.SettingsConfigProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Reference implementation of androidx.window.sidecar OEM interface for use with + * WindowManager Jetpack. + */ +class SampleSidecarImpl extends StubSidecar implements + SettingsConfigProvider.StateChangeCallback { + private static final String TAG = "SampleSidecar"; + + private final SettingsConfigProvider mConfigProvider; + + SampleSidecarImpl(Context context) { + mConfigProvider = new SettingsConfigProvider(context, this); + } + + @Override + public void onDevicePostureChanged() { + updateDeviceState(getDeviceState()); + } + + @Override + public void onDisplayFeaturesChanged() { + for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { + SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); + updateWindowLayout(windowToken, newLayout); + } + } + + @NonNull + @Override + public SidecarDeviceState getDeviceState() { + SidecarDeviceState deviceState = new SidecarDeviceState(); + deviceState.posture = mConfigProvider.getDeviceState(); + return deviceState; + } + + @NonNull + @Override + public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { + Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); + SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); + if (activity == null) { + return windowLayoutInfo; + } + windowLayoutInfo.displayFeatures = getDisplayFeatures(activity); + return windowLayoutInfo; + } + + private List getDisplayFeatures(@NonNull Activity activity) { + List features = new ArrayList(); + int displayId = activity.getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + Log.w(TAG, "This sample doesn't support display features on secondary displays"); + return features; + } + + if (activity.isInMultiWindowMode()) { + // 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; + } + + List storedFeatures = mConfigProvider.getDisplayFeatures(); + for (BaseDisplayFeature baseFeature : storedFeatures) { + SidecarDisplayFeature feature = new SidecarDisplayFeature(); + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + feature.setRect(featureRect); + feature.setType(baseFeature.getType()); + features.add(feature); + } + return features; + } + + @Override + protected void onListenersChanged() { + if (hasListeners()) { + mConfigProvider.registerObserversIfNeeded(); + } else { + mConfigProvider.unregisterObserversIfNeeded(); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java deleted file mode 100644 index 5397302f6882a..0000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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. - */ - -package androidx.window.sidecar; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static androidx.window.sidecar.SidecarHelper.getWindowDisplay; -import static androidx.window.sidecar.SidecarHelper.isInMultiWindow; -import static androidx.window.sidecar.SidecarHelper.rotateRectToDisplayRotation; -import static androidx.window.sidecar.SidecarHelper.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 com.android.internal.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class SettingsSidecarImpl extends StubSidecar { - private static final String TAG = "SettingsSidecar"; - - 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; - } - } - } - - SettingsSidecarImpl(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()) { - SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); - updateWindowLayout(windowToken, newLayout); - } - } - - @NonNull - @Override - public SidecarDeviceState getDeviceState() { - ContentResolver resolver = mContext.getContentResolver(); - int posture = Settings.Global.getInt(resolver, DEVICE_POSTURE, - SidecarDeviceState.POSTURE_UNKNOWN); - SidecarDeviceState deviceState = new SidecarDeviceState(); - deviceState.posture = posture; - return deviceState; - } - - @NonNull - @Override - public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { - List displayFeatures = readDisplayFeatures(windowToken); - SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo(); - windowLayoutInfo.displayFeatures = displayFeatures; - return windowLayoutInfo; - } - - private List readDisplayFeatures(IBinder windowToken) { - List features = new ArrayList(); - int displayId = getWindowDisplay(windowToken); - if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); - return 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; - } - - ContentResolver resolver = mContext.getContentResolver(); - String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES); - if (TextUtils.isEmpty(displayFeaturesString)) { - displayFeaturesString = mContext.getResources().getString( - R.string.config_display_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 = SidecarDisplayFeature.TYPE_FOLD; - break; - case FEATURE_TYPE_HINGE: - type = SidecarDisplayFeature.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 (isNotZero(featureRect)) { - SidecarDisplayFeature feature = new SidecarDisplayFeature(); - feature.setRect(featureRect); - feature.setType(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; - } - - private static boolean isNotZero(Rect rect) { - return rect.height() > 0 || rect.width() > 0; - } - - @Override - protected void onListenersChanged() { - if (mSettingsObserver == null) { - return; - } - - if (hasListeners()) { - mSettingsObserver.registerObserversIfNeeded(); - } else { - mSettingsObserver.unregisterObserversIfNeeded(); - } - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java index 0b4915ed5dac3..e6f8388b031f2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java @@ -28,7 +28,7 @@ public class SidecarProvider { * an OEM by overriding this method. */ public static SidecarInterface getSidecarImpl(Context context) { - return new SettingsSidecarImpl(context); + return new SampleSidecarImpl(context); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java new file mode 100644 index 0000000000000..b74a2a4bd47d3 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDisplayFeature.java @@ -0,0 +1,52 @@ +/* + * 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 androidx.window.util; + +import android.graphics.Rect; + +import androidx.annotation.NonNull; + +/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */ +public class BaseDisplayFeature { + private final int mType; + private final int mState; + @NonNull + public final Rect mRect; + + public BaseDisplayFeature(int type, int state, @NonNull Rect rect) { + this.mType = type; + this.mState = state; + if (rect.width() == 0 && rect.height() == 0) { + throw new IllegalArgumentException( + "Display feature rectangle cannot have zero width and height simultaneously."); + } + this.mRect = rect; + } + + public int getType() { + return mType; + } + + public int getState() { + return mState; + } + + @NonNull + public Rect getRect() { + return mRect; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java similarity index 62% rename from libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java rename to libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index e5b6cff17b265..2a593f15a9de6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,30 +14,36 @@ * limitations under the License. */ -package androidx.window.sidecar; +package androidx.window.util; -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; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -class SidecarHelper { +/** + * Util class for both Sidecar and Extensions. + */ +public final class ExtensionHelper { + + private ExtensionHelper() { + // Util class, no instances should be created. + } + /** - * Rotate the input rectangle specified in default display orientation to the current display + * Rotates the input rectangle specified in default display orientation to the current display * rotation. */ - static void rotateRectToDisplayRotation(Rect inOutRect, int displayId) { + public static void rotateRectToDisplayRotation(int displayId, Rect inOutRect) { DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance(); DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId); int rotation = displayInfo.rotation; @@ -52,7 +58,7 @@ class SidecarHelper { } /** - * Rotate the input rectangle within parent bounds for a given delta. + * Rotates the input rectangle within parent bounds for a given delta. */ private static void rotateBounds(Rect inOutRect, int parentWidth, int parentHeight, @Surface.Rotation int delta) { @@ -79,9 +85,9 @@ class SidecarHelper { } } - /** Transform rectangle from absolute coordinate space to the window coordinate space. */ - static void transformToWindowSpaceRect(Rect inOutRect, IBinder windowToken) { - Rect windowRect = getWindowBounds(windowToken); + /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ + public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) { + Rect windowRect = getWindowBounds(activity); if (windowRect == null) { inOutRect.setEmpty(); return; @@ -95,32 +101,17 @@ class SidecarHelper { } /** - * Get the current window bounds in absolute coordinates. - * NOTE: Only works with Activity windows. + * Gets the current window bounds in absolute coordinates. */ @Nullable - private static Rect getWindowBounds(IBinder windowToken) { - Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken); - return activity != null - ? activity.getWindowManager().getCurrentWindowMetrics().getBounds() - : null; + private static Rect getWindowBounds(@NonNull Activity activity) { + return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); } /** - * Check if this window is an Activity window that is in multi-window mode. + * Checks if both dimensions of the given rect are zero at the same time. */ - 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; + public static boolean isZero(@NonNull Rect rect) { + return rect.height() == 0 && rect.width() == 0; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java new file mode 100644 index 0000000000000..6dd190ce25197 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/SettingsConfigProvider.java @@ -0,0 +1,196 @@ +/* + * 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 androidx.window.util; + +import static androidx.window.util.ExtensionHelper.isZero; + +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.Looper; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Device and display feature state provider that uses Settings as the source. + */ +public final class SettingsConfigProvider extends ContentObserver { + private static final String TAG = "SettingsConfigProvider"; + 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 final Uri mDevicePostureUri = + Settings.Global.getUriFor(DEVICE_POSTURE); + private final Uri mDisplayFeaturesUri = + Settings.Global.getUriFor(DISPLAY_FEATURES); + private final Context mContext; + private final ContentResolver mResolver; + private final StateChangeCallback mCallback; + private boolean mRegisteredObservers; + + public SettingsConfigProvider(@NonNull Context context, @NonNull StateChangeCallback callback) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mResolver = context.getContentResolver(); + mCallback = callback; + } + + /** + * Registers the content observers for Settings keys that store device state and display feature + * configurations. + */ + public void registerObserversIfNeeded() { + if (mRegisteredObservers) { + return; + } + mRegisteredObservers = true; + mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendants */, + this /* ContentObserver */); + mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendants */, + this /* ContentObserver */); + } + + /** + * Unregisters the content observers that are tracking the state changes. + * @see #registerObserversIfNeeded() + */ + public void unregisterObserversIfNeeded() { + if (!mRegisteredObservers) { + return; + } + mRegisteredObservers = false; + mResolver.unregisterContentObserver(this); + } + + /** + * Gets the device posture int stored in Settings. + */ + public int getDeviceState() { + return Settings.Global.getInt(mResolver, DEVICE_POSTURE, + 0 /* POSTURE_UNKNOWN */); + } + + /** + * Gets the list of all display feature configs stored in Settings. Uses a custom + * {@link BaseDisplayFeature} class to report the config to be translated for actual + * containers in Sidecar or Extensions. + */ + public List getDisplayFeatures() { + List features = new ArrayList<>(); + String displayFeaturesString = Settings.Global.getString(mResolver, DISPLAY_FEATURES); + if (TextUtils.isEmpty(displayFeaturesString)) { + displayFeaturesString = mContext.getResources().getString( + R.string.config_display_features); + } + if (TextUtils.isEmpty(displayFeaturesString)) { + return features; + } + String[] featureStrings = displayFeaturesString.split(";"); + + int deviceState = getDeviceState(); + + 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 = 1 /* TYPE_FOLD */; + break; + case FEATURE_TYPE_HINGE: + type = 2 /* 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); + if (!isZero(featureRect)) { + BaseDisplayFeature feature = new BaseDisplayFeature(type, deviceState, + featureRect); + features.add(feature); + } else { + Log.w(TAG, "Read empty feature"); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Malformed feature description: " + featureString); + } + } + return features; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri == null) { + return; + } + + if (mDevicePostureUri.equals(uri)) { + mCallback.onDevicePostureChanged(); + mCallback.onDisplayFeaturesChanged(); + return; + } + if (mDisplayFeaturesUri.equals(uri)) { + mCallback.onDisplayFeaturesChanged(); + } + } + + /** + * Callback that notifies about device or display feature state changes. + */ + public interface StateChangeCallback { + /** + * Notifies about the device state update. + */ + void onDevicePostureChanged(); + + /** + * Notifies about the display feature config update. + */ + void onDisplayFeaturesChanged(); + } +} diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar new file mode 100644 index 0000000000000000000000000000000000000000..7b306b00afd93b3c696b4229d613eb38111145d4 GIT binary patch literal 9182 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 literal 0 HcmV?d00001