ExtensionWindowBackend.java
/*
* 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;
import static androidx.window.ExtensionCompat.DEBUG;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Consumer;
import androidx.window.extensions.ExtensionInterface;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
/**
* Default implementation of {@link WindowBackend} that uses a combination of platform APIs and
* device-dependent OEM extensions.
*/
final class ExtensionWindowBackend implements WindowBackend {
private static volatile ExtensionWindowBackend sInstance;
private static final Object sLock = new Object();
@GuardedBy("sLock")
@VisibleForTesting
ExtensionInterfaceCompat mWindowExtension;
/**
* List of all registered callbacks for window layout info. Not protected by {@link #sLock} to
* allow iterating and callback execution without holding the global lock.
*/
@VisibleForTesting
final List<WindowLayoutChangeCallbackWrapper> mWindowLayoutChangeCallbacks =
new CopyOnWriteArrayList<>();
/**
* List of all registered callbacks for window layout info. Not protected by {@link #sLock} to
* allow iterating and callback execution without holding the global lock.
*/
@VisibleForTesting
final List<DeviceStateChangeCallbackWrapper> mDeviceStateChangeCallbacks =
new CopyOnWriteArrayList<>();
/** Device state that was last reported through callbacks, used to filter out duplicates. */
@GuardedBy("sLock")
@VisibleForTesting
DeviceState mLastReportedDeviceState;
/** Window layouts that were last reported through callbacks, used to filter out duplicates. */
@GuardedBy("sLock")
@VisibleForTesting
final Map<Activity, WindowLayoutInfo> mLastReportedWindowLayouts = new WeakHashMap<>();
private static final String TAG = "WindowServer";
@VisibleForTesting
ExtensionWindowBackend(@Nullable ExtensionInterfaceCompat windowExtension) {
mWindowExtension = windowExtension;
if (mWindowExtension != null) {
mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
}
}
/**
* Gets the shared instance of the class.
*/
@NonNull
public static ExtensionWindowBackend getInstance(@NonNull Context context) {
if (sInstance == null) {
synchronized (sLock) {
if (sInstance == null) {
ExtensionInterfaceCompat windowExtension = initAndVerifyExtension(context);
sInstance = new ExtensionWindowBackend(windowExtension);
}
}
}
return sInstance;
}
/**
* @deprecated will be removed in the next alpha.
* @return {@link DeviceState} when Sidecar is present and an unknown {@link DeviceState}
* otherwise.
*/
@Override
@NonNull
@Deprecated
public DeviceState getDeviceState() {
synchronized (sLock) {
if (mWindowExtension instanceof SidecarCompat) {
SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
return sidecarCompat.getDeviceState();
}
return new DeviceState(DeviceState.POSTURE_UNKNOWN);
}
}
/**
* @deprecated will be removed in the next alpha.
* @param activity that is running.
* @return {@link WindowLayoutInfo} for the window containing the {@link Activity} when
* Sidecar is present and an empty info otherwise
*/
@NonNull
@Override
@Deprecated
public WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
synchronized (sLock) {
if (mWindowExtension instanceof SidecarCompat) {
SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
return sidecarCompat.getWindowLayoutInfo(activity);
}
return new WindowLayoutInfo(Collections.emptyList());
}
}
/**
*
* @param context with an associated {@link Activity}
* @return the {@link WindowLayoutInfo}
* @deprecated use an {@link Activity} instead of {@link Context}
*/
@NonNull
@Override
@Deprecated
public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
return getWindowLayoutInfo(assertActivityContext(context));
}
@Override
public void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
@NonNull Consumer<WindowLayoutInfo> callback) {
registerLayoutChangeCallback(assertActivityContext(context), executor, callback);
}
/**
* Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
* @return Base {@link Activity} context or {@code null} if not available.
* @deprecated added temporarily to make migration easier. Will be removed in next relesae.
*/
@Nullable
@Deprecated // TODO(b/173739071) remove
private static Activity getActivityFromContext(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
/**
* @deprecated added temporarily to make migration easier. Will be removed in next release.
* @param context any {@link Context}
* @return {@link Activity} if associated with {@link Context} throw
* {@link IllegalArgumentException} otherwise.
*/
@Deprecated
private Activity assertActivityContext(Context context) {
Activity activity = getActivityFromContext(context);
if (activity == null) {
throw new IllegalArgumentException("Used non-visual Context with WindowManager. "
+ "Please use an Activity or a ContextWrapper around an Activity instead.");
}
return activity;
}
@Override
public void registerLayoutChangeCallback(@NonNull Activity activity,
@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
synchronized (sLock) {
if (mWindowExtension == null) {
if (DEBUG) {
Log.v(TAG, "Extension not loaded, skipping callback registration.");
}
callback.accept(new WindowLayoutInfo(new ArrayList<>()));
return;
}
// Check if the activity was already registered, in case we need to report tracking of a
// new activity to the extension.
boolean isActivityRegistered = isActivityRegistered(activity);
WindowLayoutChangeCallbackWrapper callbackWrapper =
new WindowLayoutChangeCallbackWrapper(activity, executor, callback);
mWindowLayoutChangeCallbacks.add(callbackWrapper);
// Read value before registering in case the extension updates synchronously.
// A synchronous update would result in two values emitted.
WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
if (!isActivityRegistered) {
mWindowExtension.onWindowLayoutChangeListenerAdded(activity);
}
if (lastReportedValue != null) {
callbackWrapper.accept(lastReportedValue);
}
}
}
private boolean isActivityRegistered(@NonNull Activity activity) {
for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
if (callbackWrapper.mActivity.equals(activity)) {
return true;
}
}
return false;
}
@Override
public void unregisterLayoutChangeCallback(@NonNull Consumer<WindowLayoutInfo> callback) {
synchronized (sLock) {
if (mWindowExtension == null) {
if (DEBUG) {
Log.v(TAG, "Extension not loaded, skipping callback un-registration.");
}
return;
}
// The same callback may be registered for multiple different window tokens, and
// vice-versa. First collect all items to be removed.
List<WindowLayoutChangeCallbackWrapper> itemsToRemove = new ArrayList<>();
for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
Consumer<WindowLayoutInfo> registeredCallback = callbackWrapper.mCallback;
if (registeredCallback == callback) {
itemsToRemove.add(callbackWrapper);
}
}
// Remove the items from the list and notify extension if needed.
mWindowLayoutChangeCallbacks.removeAll(itemsToRemove);
for (WindowLayoutChangeCallbackWrapper callbackWrapper : itemsToRemove) {
callbackRemovedForActivity(callbackWrapper.mActivity);
}
}
}
/**
* Checks if there are no more registered callbacks left for the activity and inform
* extension if needed.
*/
@GuardedBy("sLock")
private void callbackRemovedForActivity(Activity activity) {
for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
if (callbackWrapper.mActivity.equals(activity)) {
// Found a registered callback for token.
return;
}
}
// No registered callbacks left for the activity - report to extension.
mWindowExtension.onWindowLayoutChangeListenerRemoved(activity);
}
@Override
public void registerDeviceStateChangeCallback(@NonNull Executor executor,
@NonNull Consumer<DeviceState> callback) {
synchronized (sLock) {
final DeviceStateChangeCallbackWrapper callbackWrapper =
new DeviceStateChangeCallbackWrapper(executor, callback);
if (mWindowExtension == null) {
if (DEBUG) {
Log.d(TAG, "Extension not loaded, skipping callback registration.");
}
callback.accept(new DeviceState(DeviceState.POSTURE_UNKNOWN));
return;
}
if (mDeviceStateChangeCallbacks.isEmpty()) {
mWindowExtension.onDeviceStateListenersChanged(false /* isEmpty */);
}
mDeviceStateChangeCallbacks.add(callbackWrapper);
if (mLastReportedDeviceState != null) {
callbackWrapper.accept(mLastReportedDeviceState);
}
}
}
@Override
public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
synchronized (sLock) {
if (mWindowExtension == null) {
if (DEBUG) {
Log.d(TAG, "Extension not loaded, skipping callback un-registration.");
}
return;
}
for (DeviceStateChangeCallbackWrapper callbackWrapper : mDeviceStateChangeCallbacks) {
if (callbackWrapper.mCallback.equals(callback)) {
mDeviceStateChangeCallbacks.remove(callbackWrapper);
if (mDeviceStateChangeCallbacks.isEmpty()) {
mWindowExtension.onDeviceStateListenersChanged(true /* isEmpty */);
// Clear device state so we do not replay stale data.
mLastReportedDeviceState = null;
}
return;
}
}
}
}
@VisibleForTesting
class ExtensionListenerImpl implements ExtensionInterfaceCompat.ExtensionCallbackInterface {
@Override
@SuppressLint("SyntheticAccessor")
public void onDeviceStateChanged(@NonNull DeviceState newDeviceState) {
synchronized (sLock) {
if (newDeviceState.equals(mLastReportedDeviceState)) {
// Skipping, value already reported
if (DEBUG) {
Log.w(TAG, "Extension reported old layout value");
}
return;
}
mLastReportedDeviceState = newDeviceState;
}
for (DeviceStateChangeCallbackWrapper callbackWrapper : mDeviceStateChangeCallbacks) {
callbackWrapper.accept(newDeviceState);
}
}
@Override
@SuppressLint("SyntheticAccessor")
public void onWindowLayoutChanged(@NonNull Activity activity,
@NonNull WindowLayoutInfo newLayout) {
synchronized (sLock) {
WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
if (newLayout.equals(lastReportedValue)) {
// Skipping, value already reported
if (DEBUG) {
Log.w(TAG, "Extension reported an old layout value");
}
return;
}
mLastReportedWindowLayouts.put(activity, newLayout);
}
for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
if (!callbackWrapper.mActivity.equals(activity)) {
continue;
}
callbackWrapper.accept(newLayout);
}
}
}
/**
* Wrapper around {@link Consumer<WindowLayoutInfo>} that also includes the {@link Executor}
* on which the callback should run and the {@link Activity}.
*/
private static class WindowLayoutChangeCallbackWrapper {
final Executor mExecutor;
final Consumer<WindowLayoutInfo> mCallback;
final Activity mActivity;
WindowLayoutChangeCallbackWrapper(@NonNull Activity activity, @NonNull Executor executor,
@NonNull Consumer<WindowLayoutInfo> callback) {
mActivity = activity;
mExecutor = executor;
mCallback = callback;
}
void accept(WindowLayoutInfo layoutInfo) {
mExecutor.execute(() -> mCallback.accept(layoutInfo));
}
}
/**
* Wrapper around {@link Consumer<DeviceState>} that also includes the {@link Executor} on
* which the callback should run.
*/
private static class DeviceStateChangeCallbackWrapper {
final Executor mExecutor;
final Consumer<DeviceState> mCallback;
DeviceStateChangeCallbackWrapper(@NonNull Executor executor,
@NonNull Consumer<DeviceState> callback) {
mExecutor = executor;
mCallback = callback;
}
void accept(DeviceState state) {
mExecutor.execute(() -> mCallback.accept(state));
}
}
/**
* Loads an instance of {@link ExtensionInterface} implemented by OEM if available on this
* device. This also verifies if the loaded implementation conforms to the declared API version.
*/
@Nullable
static ExtensionInterfaceCompat initAndVerifyExtension(Context context) {
ExtensionInterfaceCompat impl = null;
try {
if (isExtensionVersionSupported(ExtensionCompat.getExtensionVersion())) {
impl = new ExtensionCompat(context);
if (!impl.validateExtensionInterface()) {
if (DEBUG) {
Log.d(TAG, "Loaded extension doesn't match the interface version");
}
impl = null;
}
}
} catch (Throwable t) {
if (DEBUG) {
Log.d(TAG, "Failed to load extension: " + t);
}
impl = null;
}
if (impl == null) {
// Falling back to Sidecar
try {
if (isExtensionVersionSupported(SidecarCompat.getSidecarVersion())) {
impl = new SidecarCompat(context);
if (!impl.validateExtensionInterface()) {
if (DEBUG) {
Log.d(TAG, "Loaded Sidecar doesn't match the interface version");
}
impl = null;
}
}
} catch (Throwable t) {
if (DEBUG) {
Log.d(TAG, "Failed to load sidecar: " + t);
}
impl = null;
}
}
if (impl == null) {
if (DEBUG) {
Log.d(TAG, "No supported extension or sidecar found");
}
}
return impl;
}
/**
* Checks if the Extension version provided on this device is supported by the current version
* of the library.
*/
@VisibleForTesting
static boolean isExtensionVersionSupported(@Nullable Version extensionVersion) {
if (extensionVersion == null) {
return false;
}
if (extensionVersion.getMajor() == 1) {
// Disable androidx.window.extensions support in release builds of the library until the
// extensions API is finalized.
return DEBUG;
}
return Version.CURRENT.getMajor() >= extensionVersion.getMajor();
}
/**
* Test-only affordance to forget the existing instance.
*/
@VisibleForTesting
static void resetInstance() {
sInstance = null;
}
}