Merge changes from topic "jumbo-sidecar-posture" into sc-dev

* changes:
  Update sample Sidecar/Extension implementation to allow mapping system device state to posture via a config.
  Seperate SettingsConfigProvider into producers.
This commit is contained in:
Darryl Johnson
2021-03-20 03:44:01 +00:00
committed by Android (Google) Code Review
15 changed files with 810 additions and 272 deletions

View File

@@ -4649,6 +4649,10 @@
<!-- WindowsManager JetPack display features --> <!-- WindowsManager JetPack display features -->
<string name="config_display_features" translatable="false" /> <string name="config_display_features" translatable="false" />
<!-- Map of System DeviceState supplied by DeviceStateManager to WM Jetpack posture. Must be in
the format [System DeviceState]:[WM Jetpack Posture], for example: "0:1". -->
<string-array name="config_device_state_postures" translatable="false" />
<!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored. <!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored.
Note: Activity min/max aspect ratio restrictions will still be respected. Note: Activity min/max aspect ratio restrictions will still be respected.
Therefore this override can control the maximum screen area that can be occupied by Therefore this override can control the maximum screen area that can be occupied by

View File

@@ -4182,6 +4182,7 @@
<java-symbol type="dimen" name="default_background_blur_radius" /> <java-symbol type="dimen" name="default_background_blur_radius" />
<java-symbol type="array" name="config_keep_warming_services" /> <java-symbol type="array" name="config_keep_warming_services" />
<java-symbol type="string" name="config_display_features" /> <java-symbol type="string" name="config_display_features" />
<java-symbol type="array" name="config_device_state_postures" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_height" /> <java-symbol type="dimen" name="controls_thumbnail_image_max_height" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_width" /> <java-symbol type="dimen" name="controls_thumbnail_image_max_width" />

View File

@@ -30,13 +30,20 @@ android_library_import {
java_library { java_library {
name: "androidx.window.sidecar", name: "androidx.window.sidecar",
srcs: ["src/androidx/window/sidecar/**/*.java", "src/androidx/window/util/**/*.java"], srcs: [
"src/androidx/window/sidecar/**/*.java",
"src/androidx/window/util/**/*.java",
"src/androidx/window/common/**/*.java",
],
static_libs: ["window-sidecar"], static_libs: ["window-sidecar"],
installable: true, installable: true,
sdk_version: "core_platform", sdk_version: "core_platform",
system_ext_specific: true, system_ext_specific: true,
libs: ["framework", "androidx.annotation_annotation",], libs: [
required: ["androidx.window.sidecar.xml",], "framework",
"androidx.annotation_annotation",
],
required: ["androidx.window.sidecar.xml"],
} }
prebuilt_etc { prebuilt_etc {
@@ -58,13 +65,20 @@ android_library_import {
java_library { java_library {
name: "androidx.window.extensions", name: "androidx.window.extensions",
srcs: ["src/androidx/window/extensions/**/*.java", "src/androidx/window/util/**/*.java"], srcs: [
"src/androidx/window/extensions/**/*.java",
"src/androidx/window/util/**/*.java",
"src/androidx/window/common/**/*.java",
],
static_libs: ["window-extensions"], static_libs: ["window-extensions"],
installable: true, installable: true,
sdk_version: "core_platform", sdk_version: "core_platform",
system_ext_specific: true, system_ext_specific: true,
libs: ["framework", "androidx.annotation_annotation",], libs: [
required: ["androidx.window.extensions.xml",], "framework",
"androidx.annotation_annotation",
],
required: ["androidx.window.extensions.xml"],
} }
prebuilt_etc { prebuilt_etc {

View File

@@ -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 androidx.window.common;
import static androidx.window.util.ExtensionHelper.isZero;
import android.annotation.Nullable;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */
final class CommonDisplayFeature implements DisplayFeature {
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";
// TODO(b/183049815): Support feature strings that include the state of the feature.
/**
* Parses a display feature from a string.
*
* @throws IllegalArgumentException if the provided string is improperly formatted or could not
* otherwise be parsed.
*
* @see #FEATURE_PATTERN
*/
@NonNull
static CommonDisplayFeature parseFromString(@NonNull String string) {
Matcher featureMatcher = FEATURE_PATTERN.matcher(string);
if (!featureMatcher.matches()) {
throw new IllegalArgumentException("Malformed feature description format: " + string);
}
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: {
throw new IllegalArgumentException("Malformed feature type: " + featureType);
}
}
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)) {
throw new IllegalArgumentException("Feature has empty bounds: " + string);
}
return new CommonDisplayFeature(type, null, featureRect);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Malformed feature description: " + string, e);
}
}
private final int mType;
@Nullable
private final Integer mState;
@NonNull
private final Rect mRect;
CommonDisplayFeature(int type, @Nullable Integer 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;
}
/** Returns the state of the feature, or {@code null} if the feature has no state. */
@Nullable
public Integer getState() {
return mState;
}
@NonNull
public Rect getRect() {
return mRect;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CommonDisplayFeature that = (CommonDisplayFeature) o;
return mType == that.mType
&& Objects.equals(mState, that.mState)
&& mRect.equals(that.mRect);
}
@Override
public int hashCode() {
return Objects.hash(mType, mState, mRect);
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.common;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.window.util.BaseDataProducer;
import com.android.internal.R;
import java.util.Optional;
/**
* An implementation of {@link androidx.window.util.DataProducer} that returns the device's posture
* by mapping the state returned from {@link DeviceStateManager} to values provided in the resources
* config at {@link R.array#config_device_state_postures}.
*/
public final class DeviceStateManagerPostureProducer extends BaseDataProducer<Integer> {
private static final String TAG = "ConfigDevicePostureProducer";
private static final boolean DEBUG = false;
private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
private final DeviceStateCallback mDeviceStateCallback = (state) -> {
mCurrentDeviceState = state;
notifyDataChanged();
};
public DeviceStateManagerPostureProducer(@NonNull Context context) {
String[] deviceStatePosturePairs = context.getResources()
.getStringArray(R.array.config_device_state_postures);
for (String deviceStatePosturePair : deviceStatePosturePairs) {
String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
if (deviceStatePostureMapping.length != 2) {
if (DEBUG) {
Log.e(TAG, "Malformed device state posture pair: " + deviceStatePosturePair);
}
continue;
}
int deviceState;
int posture;
try {
deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
posture = Integer.parseInt(deviceStatePostureMapping[1]);
} catch (NumberFormatException e) {
if (DEBUG) {
Log.e(TAG, "Failed to parse device state or posture: " + deviceStatePosturePair,
e);
}
continue;
}
mDeviceStateToPostureMap.put(deviceState, posture);
}
if (mDeviceStateToPostureMap.size() > 0) {
context.getSystemService(DeviceStateManager.class)
.registerCallback(context.getMainExecutor(), mDeviceStateCallback);
}
}
@Override
@Nullable
public Optional<Integer> getData() {
final int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, -1);
return posture != -1 ? Optional.of(posture) : Optional.empty();
}
}

View File

@@ -14,39 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package androidx.window.util; package androidx.window.common;
import android.annotation.Nullable;
import android.graphics.Rect; import android.graphics.Rect;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */ /** Wrapper for both Extension and Sidecar versions of DisplayFeature. */
public class BaseDisplayFeature { public interface DisplayFeature {
private final int mType; /** Returns the type of the feature. */
private final int mState; int getType();
/** Returns the state of the feature, or {@code null} if the feature has no state. */
@Nullable
Integer getState();
/** Returns the bounds of the feature. */
@NonNull @NonNull
public final Rect mRect; Rect getRect();
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;
}
} }

View File

@@ -0,0 +1,74 @@
/*
* 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.common;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.window.util.BaseDataProducer;
import com.android.internal.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Implementation of {@link androidx.window.util.DataProducer} that produces
* {@link CommonDisplayFeature} parsed from a string stored in the resources config at
* {@link R.string#config_display_features}.
*/
public final class ResourceConfigDisplayFeatureProducer extends
BaseDataProducer<List<DisplayFeature>> {
private static final boolean DEBUG = false;
private static final String TAG = "ResourceConfigDisplayFeatureProducer";
private final Context mContext;
public ResourceConfigDisplayFeatureProducer(@NonNull Context context) {
mContext = context;
}
@Override
@Nullable
public Optional<List<DisplayFeature>> getData() {
String displayFeaturesString = mContext.getResources().getString(
R.string.config_display_features);
if (TextUtils.isEmpty(displayFeaturesString)) {
return Optional.empty();
}
List<DisplayFeature> features = new ArrayList<>();
String[] featureStrings = displayFeaturesString.split(";");
for (String featureString : featureStrings) {
CommonDisplayFeature feature;
try {
feature = CommonDisplayFeature.parseFromString(featureString);
} catch (IllegalArgumentException e) {
if (DEBUG) {
Log.w(TAG, "Failed to parse display feature: " + featureString, e);
}
continue;
}
features.add(feature);
}
return Optional.of(features);
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.common;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import androidx.window.util.BaseDataProducer;
import java.util.Optional;
/**
* Implementation of {@link androidx.window.util.DataProducer} that provides the device posture
* as an {@link Integer} from a value stored in {@link Settings}.
*/
public final class SettingsDevicePostureProducer extends BaseDataProducer<Integer> {
private static final String DEVICE_POSTURE = "device_posture";
private final Uri mDevicePostureUri =
Settings.Global.getUriFor(DEVICE_POSTURE);
private final ContentResolver mResolver;
private final ContentObserver mObserver;
private boolean mRegisteredObservers;
public SettingsDevicePostureProducer(@NonNull Context context) {
mResolver = context.getContentResolver();
mObserver = new SettingsObserver();
}
@Override
@Nullable
public Optional<Integer> getData() {
int posture = Settings.Global.getInt(mResolver, DEVICE_POSTURE, -1);
return posture == -1 ? Optional.empty() : Optional.of(posture);
}
/**
* Registers settings observers, if needed. When settings observers are registered for this
* producer callbacks for changes in data will be triggered.
*/
public void registerObserversIfNeeded() {
if (mRegisteredObservers) {
return;
}
mRegisteredObservers = true;
mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendants */,
mObserver /* ContentObserver */);
}
/**
* Unregisters settings observers, if needed. When settings observers are unregistered for this
* producer callbacks for changes in data will not be triggered.
*/
public void unregisterObserversIfNeeded() {
if (!mRegisteredObservers) {
return;
}
mRegisteredObservers = false;
mResolver.unregisterContentObserver(mObserver);
}
private final class SettingsObserver extends ContentObserver {
SettingsObserver() {
super(new Handler(Looper.getMainLooper()));
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (mDevicePostureUri.equals(uri)) {
notifyDataChanged();
}
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.common;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
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.window.util.BaseDataProducer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Implementation of {@link androidx.window.util.DataProducer} that produces
* {@link CommonDisplayFeature} parsed from a string stored in {@link Settings}.
*/
public final class SettingsDisplayFeatureProducer
extends BaseDataProducer<List<DisplayFeature>> {
private static final boolean DEBUG = false;
private static final String TAG = "SettingsDisplayFeatureProducer";
private static final String DISPLAY_FEATURES = "display_features";
private final Uri mDisplayFeaturesUri =
Settings.Global.getUriFor(DISPLAY_FEATURES);
private final ContentResolver mResolver;
private final ContentObserver mObserver;
private boolean mRegisteredObservers;
public SettingsDisplayFeatureProducer(@NonNull Context context) {
mResolver = context.getContentResolver();
mObserver = new SettingsObserver();
}
@Override
@Nullable
public Optional<List<DisplayFeature>> getData() {
String displayFeaturesString = Settings.Global.getString(mResolver, DISPLAY_FEATURES);
if (displayFeaturesString == null) {
return Optional.empty();
}
List<DisplayFeature> features = new ArrayList<>();
if (TextUtils.isEmpty(displayFeaturesString)) {
return Optional.of(features);
}
String[] featureStrings = displayFeaturesString.split(";");
for (String featureString : featureStrings) {
CommonDisplayFeature feature;
try {
feature = CommonDisplayFeature.parseFromString(featureString);
} catch (IllegalArgumentException e) {
if (DEBUG) {
Log.w(TAG, "Failed to parse display feature: " + featureString, e);
}
continue;
}
features.add(feature);
}
return Optional.of(features);
}
/**
* Registers settings observers, if needed. When settings observers are registered for this
* producer callbacks for changes in data will be triggered.
*/
public void registerObserversIfNeeded() {
if (mRegisteredObservers) {
return;
}
mRegisteredObservers = true;
mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendants */,
mObserver /* ContentObserver */);
}
/**
* Unregisters settings observers, if needed. When settings observers are unregistered for this
* producer callbacks for changes in data will not be triggered.
*/
public void unregisterObserversIfNeeded() {
if (!mRegisteredObservers) {
return;
}
mRegisteredObservers = false;
mResolver.unregisterContentObserver(mObserver);
}
private final class SettingsObserver extends ContentObserver {
SettingsObserver() {
super(new Handler(Looper.getMainLooper()));
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (mDisplayFeaturesUri.equals(uri)) {
notifyDataChanged();
}
}
}
}

View File

@@ -27,11 +27,17 @@ import android.graphics.Rect;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.window.util.BaseDisplayFeature; import androidx.window.common.DeviceStateManagerPostureProducer;
import androidx.window.util.SettingsConfigProvider; import androidx.window.common.DisplayFeature;
import androidx.window.common.ResourceConfigDisplayFeatureProducer;
import androidx.window.common.SettingsDevicePostureProducer;
import androidx.window.common.SettingsDisplayFeatureProducer;
import androidx.window.util.DataProducer;
import androidx.window.util.PriorityDataProducer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Reference implementation of androidx.window.extensions OEM interface for use with * Reference implementation of androidx.window.extensions OEM interface for use with
@@ -41,23 +47,46 @@ import java.util.List;
* production builds since the interface can still change before reaching stable version. * production builds since the interface can still change before reaching stable version.
* Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead. * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead.
*/ */
class SampleExtensionImpl extends StubExtension implements class SampleExtensionImpl extends StubExtension {
SettingsConfigProvider.StateChangeCallback {
private static final String TAG = "SampleExtension"; private static final String TAG = "SampleExtension";
private final SettingsConfigProvider mConfigProvider; private final SettingsDevicePostureProducer mSettingsDevicePostureProducer;
private final DataProducer<Integer> mDevicePostureProducer;
private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer;
private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer;
SampleExtensionImpl(Context context) { SampleExtensionImpl(Context context) {
mConfigProvider = new SettingsConfigProvider(context, this); mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context);
mDevicePostureProducer = new PriorityDataProducer<>(List.of(
mSettingsDevicePostureProducer,
new DeviceStateManagerPostureProducer(context)
));
mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context);
mDisplayFeatureProducer = new PriorityDataProducer<>(List.of(
mSettingsDisplayFeatureProducer,
new ResourceConfigDisplayFeatureProducer(context)
));
mDevicePostureProducer.addDataChangedCallback(this::onDevicePostureChanged);
mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
} }
@Override private void onDevicePostureChanged() {
public void onDevicePostureChanged() { updateDeviceState(new ExtensionDeviceState(getDevicePosture()));
updateDeviceState(new ExtensionDeviceState(mConfigProvider.getDeviceState()));
// Trigger a change in display features as the posture will be used in place of the feature
// state if the state is left unset by the producer.
onDisplayFeaturesChanged();
} }
@Override private int getDevicePosture() {
public void onDisplayFeaturesChanged() { Optional<Integer> posture = mDevicePostureProducer.getData();
return posture.orElse(ExtensionDeviceState.POSTURE_UNKNOWN);
}
private void onDisplayFeaturesChanged() {
for (Activity activity : getActivitiesListeningForLayoutChanges()) { for (Activity activity : getActivitiesListeningForLayoutChanges()) {
ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity); ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
updateWindowLayout(activity, newLayout); updateWindowLayout(activity, newLayout);
@@ -84,13 +113,20 @@ class SampleExtensionImpl extends StubExtension implements
return features; return features;
} }
List<BaseDisplayFeature> storedFeatures = mConfigProvider.getDisplayFeatures(); Optional<List<DisplayFeature>> storedFeatures = mDisplayFeatureProducer.getData();
for (BaseDisplayFeature baseFeature : storedFeatures) { if (storedFeatures.isPresent()) {
Rect featureRect = baseFeature.getRect(); int posture = getDevicePosture();
rotateRectToDisplayRotation(displayId, featureRect);
transformToWindowSpaceRect(activity, featureRect); for (DisplayFeature baseFeature : storedFeatures.get()) {
features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(), Rect featureRect = baseFeature.getRect();
baseFeature.getState())); rotateRectToDisplayRotation(displayId, featureRect);
transformToWindowSpaceRect(activity, featureRect);
Integer featureState = baseFeature.getState();
features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(),
featureState == null ? posture : featureState));
}
} }
return features; return features;
} }
@@ -98,9 +134,11 @@ class SampleExtensionImpl extends StubExtension implements
@Override @Override
protected void onListenersChanged() { protected void onListenersChanged() {
if (hasListeners()) { if (hasListeners()) {
mConfigProvider.registerObserversIfNeeded(); mSettingsDevicePostureProducer.registerObserversIfNeeded();
mSettingsDisplayFeatureProducer.registerObserversIfNeeded();
} else { } else {
mConfigProvider.unregisterObserversIfNeeded(); mSettingsDevicePostureProducer.unregisterObserversIfNeeded();
mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded();
} }
onDevicePostureChanged(); onDevicePostureChanged();

View File

@@ -29,33 +29,53 @@ import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.window.util.BaseDisplayFeature; import androidx.window.common.DeviceStateManagerPostureProducer;
import androidx.window.util.SettingsConfigProvider; import androidx.window.common.DisplayFeature;
import androidx.window.common.ResourceConfigDisplayFeatureProducer;
import androidx.window.common.SettingsDevicePostureProducer;
import androidx.window.common.SettingsDisplayFeatureProducer;
import androidx.window.util.DataProducer;
import androidx.window.util.PriorityDataProducer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Reference implementation of androidx.window.sidecar OEM interface for use with * Reference implementation of androidx.window.sidecar OEM interface for use with
* WindowManager Jetpack. * WindowManager Jetpack.
*/ */
class SampleSidecarImpl extends StubSidecar implements class SampleSidecarImpl extends StubSidecar {
SettingsConfigProvider.StateChangeCallback {
private static final String TAG = "SampleSidecar"; private static final String TAG = "SampleSidecar";
private final SettingsConfigProvider mConfigProvider; private final SettingsDevicePostureProducer mSettingsDevicePostureProducer;
private final DataProducer<Integer> mDevicePostureProducer;
private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer;
private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer;
SampleSidecarImpl(Context context) { SampleSidecarImpl(Context context) {
mConfigProvider = new SettingsConfigProvider(context, this); mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context);
mDevicePostureProducer = new PriorityDataProducer<>(List.of(
mSettingsDevicePostureProducer,
new DeviceStateManagerPostureProducer(context)
));
mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context);
mDisplayFeatureProducer = new PriorityDataProducer<>(List.of(
mSettingsDisplayFeatureProducer,
new ResourceConfigDisplayFeatureProducer(context)
));
mDevicePostureProducer.addDataChangedCallback(this::onDevicePostureChanged);
mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
} }
@Override private void onDevicePostureChanged() {
public void onDevicePostureChanged() {
updateDeviceState(getDeviceState()); updateDeviceState(getDeviceState());
} }
@Override private void onDisplayFeaturesChanged() {
public void onDisplayFeaturesChanged() {
for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { for (IBinder windowToken : getWindowsListeningForLayoutChanges()) {
SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken);
updateWindowLayout(windowToken, newLayout); updateWindowLayout(windowToken, newLayout);
@@ -65,8 +85,10 @@ class SampleSidecarImpl extends StubSidecar implements
@NonNull @NonNull
@Override @Override
public SidecarDeviceState getDeviceState() { public SidecarDeviceState getDeviceState() {
Optional<Integer> posture = mDevicePostureProducer.getData();
SidecarDeviceState deviceState = new SidecarDeviceState(); SidecarDeviceState deviceState = new SidecarDeviceState();
deviceState.posture = mConfigProvider.getDeviceState(); deviceState.posture = posture.orElse(SidecarDeviceState.POSTURE_UNKNOWN);
return deviceState; return deviceState;
} }
@@ -96,15 +118,17 @@ class SampleSidecarImpl extends StubSidecar implements
return features; return features;
} }
List<BaseDisplayFeature> storedFeatures = mConfigProvider.getDisplayFeatures(); Optional<List<DisplayFeature>> storedFeatures = mDisplayFeatureProducer.getData();
for (BaseDisplayFeature baseFeature : storedFeatures) { if (storedFeatures.isPresent()) {
SidecarDisplayFeature feature = new SidecarDisplayFeature(); for (DisplayFeature baseFeature : storedFeatures.get()) {
Rect featureRect = baseFeature.getRect(); SidecarDisplayFeature feature = new SidecarDisplayFeature();
rotateRectToDisplayRotation(displayId, featureRect); Rect featureRect = baseFeature.getRect();
transformToWindowSpaceRect(activity, featureRect); rotateRectToDisplayRotation(displayId, featureRect);
feature.setRect(featureRect); transformToWindowSpaceRect(activity, featureRect);
feature.setType(baseFeature.getType()); feature.setRect(featureRect);
features.add(feature); feature.setType(baseFeature.getType());
features.add(feature);
}
} }
return features; return features;
} }
@@ -112,9 +136,11 @@ class SampleSidecarImpl extends StubSidecar implements
@Override @Override
protected void onListenersChanged() { protected void onListenersChanged() {
if (hasListeners()) { if (hasListeners()) {
mConfigProvider.registerObserversIfNeeded(); mSettingsDevicePostureProducer.registerObserversIfNeeded();
mSettingsDisplayFeatureProducer.registerObserversIfNeeded();
} else { } else {
mConfigProvider.unregisterObserversIfNeeded(); mSettingsDevicePostureProducer.unregisterObserversIfNeeded();
mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded();
} }
} }
} }

View File

@@ -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 androidx.annotation.NonNull;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Base class that provides the implementation for the callback mechanism of the
* {@link DataProducer} API.
*
* @param <T> The type of data this producer returns through {@link #getData()}.
*/
public abstract class BaseDataProducer<T> implements DataProducer<T> {
private final Set<Runnable> mCallbacks = new LinkedHashSet<>();
@Override
public final void addDataChangedCallback(@NonNull Runnable callback) {
mCallbacks.add(callback);
}
@Override
public final void removeDataChangedCallback(@NonNull Runnable callback) {
mCallbacks.remove(callback);
}
/**
* Called to notify all registered callbacks that the data provided by {@link #getData()} has
* changed.
*/
protected void notifyDataChanged() {
for (Runnable callback : mCallbacks) {
callback.run();
}
}
}

View File

@@ -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 androidx.window.util;
import android.annotation.NonNull;
import java.util.Optional;
/**
* Produces data through {@link #getData()} and provides a mechanism for receiving a callback when
* the data managed by the produces has changed.
*
* @param <T> The type of data this producer returns through {@link #getData()}.
*/
public interface DataProducer<T> {
/**
* Returns the data currently stored in the provider, or {@link Optional#empty()} if the
* provider has no data.
*/
Optional<T> getData();
/**
* Adds a callback to be notified when the data returned from {@link #getData()} has changed.
*/
void addDataChangedCallback(@NonNull Runnable callback);
/** Removes a callback previously added with {@link #addDataChangedCallback(Runnable)}. */
void removeDataChangedCallback(@NonNull Runnable callback);
}

View File

@@ -0,0 +1,56 @@
/*
* 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.annotation.Nullable;
import java.util.List;
import java.util.Optional;
/**
* Implementation of {@link DataProducer} that delegates calls to {@link #getData()} to the list of
* provided child producers.
* <p>
* The value returned is based on the precedence of the supplied children where the producer with
* index 0 has a higher precedence than producers that come later in the list. When a producer with
* a higher precedence has a non-empty value returned from {@link #getData()}, its value will be
* returned from an instance of this class, ignoring all other producers with lower precedence.
*
* @param <T> The type of data this producer returns through {@link #getData()}.
*/
public final class PriorityDataProducer<T> extends BaseDataProducer<T> {
private final List<DataProducer<T>> mChildProducers;
public PriorityDataProducer(List<DataProducer<T>> childProducers) {
mChildProducers = childProducers;
for (DataProducer<T> childProducer : mChildProducers) {
childProducer.addDataChangedCallback(this::notifyDataChanged);
}
}
@Nullable
@Override
public Optional<T> getData() {
for (DataProducer<T> childProducer : mChildProducers) {
final Optional<T> data = childProducer.getData();
if (data.isPresent()) {
return data;
}
}
return Optional.empty();
}
}

View File

@@ -1,196 +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 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<BaseDisplayFeature> getDisplayFeatures() {
List<BaseDisplayFeature> 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();
}
}