public final class

ExtensionWindowBackend

extends java.lang.Object

implements WindowBackend

 java.lang.Object

↳androidx.window.ExtensionWindowBackend

Overview

Default implementation of WindowBackend that uses a combination of platform APIs and device-dependent OEM extensions.

Summary

Methods
public DeviceStategetDeviceState()

public static ExtensionWindowBackendgetInstance(Context context)

Get the shared instance of the class.

public WindowLayoutInfogetWindowLayoutInfo(Context context)

public voidregisterDeviceStateChangeCallback(java.util.concurrent.Executor executor, Consumer<DeviceState> callback)

public voidregisterLayoutChangeCallback(Context context, java.util.concurrent.Executor executor, Consumer<WindowLayoutInfo> callback)

public voidunregisterDeviceStateChangeCallback(Consumer<DeviceState> callback)

public voidunregisterLayoutChangeCallback(Consumer<WindowLayoutInfo> callback)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static ExtensionWindowBackend getInstance(Context context)

Get the shared instance of the class.

public WindowLayoutInfo getWindowLayoutInfo(Context context)

public DeviceState getDeviceState()

public void registerLayoutChangeCallback(Context context, java.util.concurrent.Executor executor, Consumer<WindowLayoutInfo> callback)

public void unregisterLayoutChangeCallback(Consumer<WindowLayoutInfo> callback)

public void registerDeviceStateChangeCallback(java.util.concurrent.Executor executor, Consumer<DeviceState> callback)

public void unregisterDeviceStateChangeCallback(Consumer<DeviceState> callback)

Source

/*
 * 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.ExtensionHelper.DEBUG;
import static androidx.window.WindowManager.getActivityFromContext;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.
 */
public final class ExtensionWindowBackend implements WindowBackend {
    private static volatile ExtensionWindowBackend sInstance;
    private static final Object sLock = new Object();

    @GuardedBy("sLock")
    private 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.
     */
    private 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.
     */
    private final List<DeviceStateChangeCallbackWrapper> mDeviceStateChangeCallbacks =
            new CopyOnWriteArrayList<>();
    /** Device state that was last reported through callbacks, used to filter out duplicates. */
    @GuardedBy("sLock")
    private DeviceState mLastReportedDeviceState;
    /** Window layouts that were last reported through callbacks, used to filter out duplicates. */
    @GuardedBy("sLock")
    private final HashMap<IBinder, WindowLayoutInfo> mLastReportedWindowLayouts =
            new HashMap<>();

    private static final String TAG = "WindowServer";

    private ExtensionWindowBackend() {
        // Empty
    }

    /**
     * Get the shared instance of the class.
     */
    @NonNull
    public static ExtensionWindowBackend getInstance(@NonNull Context context) {
        if (sInstance == null) {
            synchronized (sLock) {
                if (sInstance == null) {
                    sInstance = new ExtensionWindowBackend();
                    sInstance.initExtension(context.getApplicationContext());
                }
            }
        }
        return sInstance;
    }

    /** Try to initialize Extension, returns early if it's not available. */
    @SuppressLint("SyntheticAccessor")
    @GuardedBy("sLock")
    private void initExtension(Context context) {
        mWindowExtension = ExtensionHelper.getExtensionImpl(context);
        if (mWindowExtension == null) {
            return;
        }
        mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
    }

    @NonNull
    @Override
    public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
        Activity activity = assertActivityContext(context);
        IBinder windowToken = getActivityWindowToken(activity);
        if (windowToken == null) {
            throw new IllegalStateException("Activity does not have a window attached.");
        }

        synchronized (sLock) {
            WindowLayoutInfo extensionWindowLayoutInfo = mWindowExtension != null
                    ? mWindowExtension.getWindowLayoutInfo(windowToken)
                    : new WindowLayoutInfo(new ArrayList<>());
            mLastReportedWindowLayouts.put(windowToken, extensionWindowLayoutInfo);
            return extensionWindowLayoutInfo;
        }
    }

    @NonNull
    @Override
    public DeviceState getDeviceState() {
        synchronized (sLock) {
            return mWindowExtension != null ? mWindowExtension.getDeviceState() :
                    new DeviceState(DeviceState.POSTURE_UNKNOWN);
        }
    }

    @Override
    public void registerLayoutChangeCallback(@NonNull Context context,
            @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
        synchronized (sLock) {
            if (mWindowExtension == null) {
                if (DEBUG) {
                    Log.v(TAG, "Extension not loaded, skipping callback registration.");
                }
                return;
            }

            Activity activity = assertActivityContext(context);
            IBinder windowToken = getActivityWindowToken(activity);
            if (windowToken == null) {
                throw new IllegalStateException("Activity does not have a window attached.");
            }

            // Check if the token was already registered, in case we need to report tracking of a
            // new token to the extension.
            boolean registeredToken = false;
            for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
                if (callbackWrapper.mToken.equals(windowToken)) {
                    registeredToken = true;
                    break;
                }
            }

            final WindowLayoutChangeCallbackWrapper callbackWrapper =
                    new WindowLayoutChangeCallbackWrapper(windowToken, executor, callback);
            mWindowLayoutChangeCallbacks.add(callbackWrapper);
            if (!registeredToken) {
                // Added the first callback for the token.
                mWindowExtension.onWindowLayoutChangeListenerAdded(windowToken);
            }
        }
    }

    @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) {
                callbackRemovedForToken(callbackWrapper.mToken);
            }
        }
    }

    /**
     * Check if there are no more registered callbacks left for the token and inform extension if
     * needed.
     */
    @GuardedBy("sLock")
    private void callbackRemovedForToken(IBinder token) {
        for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
            if (callbackWrapper.mToken.equals(token)) {
                // Found a registered callback for token.
                return;
            }
        }
        // No registered callbacks left for token - report to extension.
        mWindowExtension.onWindowLayoutChangeListenerRemoved(token);
    }

    @Override
    public void registerDeviceStateChangeCallback(@NonNull Executor executor,
            @NonNull Consumer<DeviceState> callback) {
        synchronized (sLock) {
            if (mWindowExtension == null) {
                if (DEBUG) {
                    Log.d(TAG, "Extension not loaded, skipping callback registration.");
                }
                return;
            }

            if (mDeviceStateChangeCallbacks.isEmpty()) {
                mWindowExtension.onDeviceStateListenersChanged(false /* isEmpty */);
            }

            final DeviceStateChangeCallbackWrapper callbackWrapper =
                    new DeviceStateChangeCallbackWrapper(executor, callback);
            mDeviceStateChangeCallbacks.add(callbackWrapper);
        }
    }

    @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 */);
                    }
                    return;
                }
            }
        }
    }

    private 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.mExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callbackWrapper.mCallback.accept(newDeviceState);
                    }
                });
            }
        }

        @Override
        @SuppressLint("SyntheticAccessor")
        public void onWindowLayoutChanged(@NonNull IBinder windowToken,
                @NonNull WindowLayoutInfo newLayout) {
            synchronized (sLock) {
                WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(windowToken);
                if (newLayout.equals(lastReportedValue)) {
                    // Skipping, value already reported
                    if (DEBUG) {
                        Log.w(TAG, "Extension reported an old layout value");
                    }
                    return;
                }
                mLastReportedWindowLayouts.put(windowToken, newLayout);
            }

            for (WindowLayoutChangeCallbackWrapper callbackWrapper : mWindowLayoutChangeCallbacks) {
                if (!callbackWrapper.mToken.equals(windowToken)) {
                    continue;
                }

                callbackWrapper.mExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callbackWrapper.mCallback.accept(newLayout);
                    }
                });
            }
        }
    }

    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;
    }

    private IBinder getActivityWindowToken(Activity activity) {
        return activity.getWindow().getAttributes().token;
    }

    /**
     * Wrapper around {@link Consumer<WindowLayoutInfo>} that also includes the {@link Executor}
     * on which the callback should run and the associated token.
     */
    private static class WindowLayoutChangeCallbackWrapper {
        final Executor mExecutor;
        final Consumer<WindowLayoutInfo> mCallback;
        final IBinder mToken;

        WindowLayoutChangeCallbackWrapper(@NonNull IBinder token, @NonNull Executor executor,
                @NonNull Consumer<WindowLayoutInfo> callback) {
            mToken = token;
            mExecutor = executor;
            mCallback = callback;
        }
    }

    /**
     * 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;
        }
    }
}