mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 23:36:47 +00:00
This also means Taskbar is transient by default in automated tests, instead of persistent. Updated some checks accordingly. Flag: LEGACY ENABLE_TRANSIENT_TASKBAR ENABLED Test: TaskbarExpandCollapse#hideShowTaskbar; TaplTestsTaskbar; TaplTestsTransientTaskbar; TaplTestsPersistentTaskbar Bug: 270395798 Change-Id: Ib6e592a31a55a912a7ea991a421a9c60bca51c80
511 lines
20 KiB
Java
511 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2019 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 com.android.launcher3.util;
|
|
|
|
import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
|
|
import static android.view.Display.DEFAULT_DISPLAY;
|
|
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
|
|
|
|
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
|
|
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
|
|
import static com.android.launcher3.Utilities.dpiFromPx;
|
|
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_PINNING;
|
|
import static com.android.launcher3.config.FeatureFlags.ENABLE_TRANSIENT_TASKBAR;
|
|
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
|
|
import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
|
|
import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.annotation.TargetApi;
|
|
import android.content.ComponentCallbacks;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.hardware.display.DisplayManager;
|
|
import android.os.Build;
|
|
import android.util.ArrayMap;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
import android.view.Display;
|
|
|
|
import androidx.annotation.AnyThread;
|
|
import androidx.annotation.UiThread;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import com.android.launcher3.LauncherPrefs;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.logging.FileLog;
|
|
import com.android.launcher3.util.window.CachedDisplayInfo;
|
|
import com.android.launcher3.util.window.WindowManagerProxy;
|
|
|
|
import java.io.PrintWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.StringJoiner;
|
|
|
|
/**
|
|
* Utility class to cache properties of default display to avoid a system RPC on every call.
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
public class DisplayController implements ComponentCallbacks, SafeCloseable {
|
|
|
|
private static final String TAG = "DisplayController";
|
|
private static final boolean DEBUG = false;
|
|
private static boolean sTransientTaskbarStatusForTests = true;
|
|
|
|
// TODO(b/254119092) remove all logs with this tag
|
|
public static final String TASKBAR_NOT_DESTROYED_TAG = "b/254119092";
|
|
|
|
public static final MainThreadInitializedObject<DisplayController> INSTANCE =
|
|
new MainThreadInitializedObject<>(DisplayController::new);
|
|
|
|
public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
|
|
public static final int CHANGE_ROTATION = 1 << 1;
|
|
public static final int CHANGE_DENSITY = 1 << 2;
|
|
public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3;
|
|
public static final int CHANGE_NAVIGATION_MODE = 1 << 4;
|
|
public static final int CHANGE_TASKBAR_PINNING = 1 << 5;
|
|
|
|
public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
|
|
| CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE
|
|
| CHANGE_TASKBAR_PINNING;
|
|
|
|
private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
|
|
private static final String TARGET_OVERLAY_PACKAGE = "android";
|
|
|
|
private final Context mContext;
|
|
private final DisplayManager mDM;
|
|
|
|
// Null for SDK < S
|
|
private final Context mWindowContext;
|
|
|
|
// The callback in this listener updates DeviceProfile, which other listeners might depend on
|
|
private DisplayInfoChangeListener mPriorityListener;
|
|
private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
|
|
|
|
private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent);
|
|
|
|
private Info mInfo;
|
|
private boolean mDestroyed = false;
|
|
|
|
private SharedPreferences.OnSharedPreferenceChangeListener
|
|
mTaskbarPinningPreferenceChangeListener;
|
|
|
|
@VisibleForTesting
|
|
protected DisplayController(Context context) {
|
|
mContext = context;
|
|
mDM = context.getSystemService(DisplayManager.class);
|
|
|
|
if (ENABLE_TASKBAR_PINNING.get()) {
|
|
attachTaskbarPinningSharedPreferenceChangeListener(mContext);
|
|
}
|
|
|
|
Display display = mDM.getDisplay(DEFAULT_DISPLAY);
|
|
if (Utilities.ATLEAST_S) {
|
|
mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
|
|
mWindowContext.registerComponentCallbacks(this);
|
|
} else {
|
|
mWindowContext = null;
|
|
mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
|
|
}
|
|
|
|
// Initialize navigation mode change listener
|
|
mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
|
|
|
|
WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
|
|
Context displayInfoContext = getDisplayInfoContext(display);
|
|
mInfo = new Info(displayInfoContext, wmProxy,
|
|
wmProxy.estimateInternalDisplayBounds(displayInfoContext));
|
|
FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
|
|
}
|
|
|
|
private void attachTaskbarPinningSharedPreferenceChangeListener(Context context) {
|
|
mTaskbarPinningPreferenceChangeListener =
|
|
(sharedPreferences, key) -> {
|
|
if (TASKBAR_PINNING_KEY.equals(key)
|
|
&& mInfo.mIsTaskbarPinned != LauncherPrefs.get(mContext).get(
|
|
TASKBAR_PINNING)
|
|
) {
|
|
handleInfoChange(mWindowContext.getDisplay());
|
|
}
|
|
};
|
|
|
|
LauncherPrefs.get(context).addListener(
|
|
mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
|
|
}
|
|
|
|
/**
|
|
* Returns the current navigation mode
|
|
*/
|
|
public static NavigationMode getNavigationMode(Context context) {
|
|
return INSTANCE.get(context).getInfo().navigationMode;
|
|
}
|
|
|
|
/**
|
|
* Returns whether taskbar is transient.
|
|
*/
|
|
public static boolean isTransientTaskbar(Context context) {
|
|
return INSTANCE.get(context).getInfo().isTransientTaskbar();
|
|
}
|
|
|
|
/**
|
|
* Enables transient taskbar status for tests.
|
|
*/
|
|
@VisibleForTesting
|
|
public static void enableTransientTaskbarForTests(boolean enable) {
|
|
sTransientTaskbarStatusForTests = enable;
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
mDestroyed = true;
|
|
if (ENABLE_TASKBAR_PINNING.get()) {
|
|
LauncherPrefs.get(mContext).removeListener(
|
|
mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
|
|
}
|
|
if (mWindowContext != null) {
|
|
mWindowContext.unregisterComponentCallbacks(this);
|
|
} else {
|
|
// TODO: unregister broadcast receiver
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface for listening for display changes
|
|
*/
|
|
public interface DisplayInfoChangeListener {
|
|
|
|
/**
|
|
* Invoked when display info has changed.
|
|
* @param context updated context associated with the display.
|
|
* @param info updated display information.
|
|
* @param flags bitmask indicating type of change.
|
|
*/
|
|
void onDisplayInfoChanged(Context context, Info info, int flags);
|
|
}
|
|
|
|
private void onIntent(Intent intent) {
|
|
if (mDestroyed) {
|
|
return;
|
|
}
|
|
boolean reconfigure = false;
|
|
if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
|
|
reconfigure = true;
|
|
} else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
|
|
Configuration config = mContext.getResources().getConfiguration();
|
|
reconfigure = mInfo.fontScale != config.fontScale
|
|
|| mInfo.densityDpi != config.densityDpi;
|
|
}
|
|
|
|
if (reconfigure) {
|
|
Log.d(TAG, "Configuration changed, notifying listeners");
|
|
Display display = mDM.getDisplay(DEFAULT_DISPLAY);
|
|
if (display != null) {
|
|
handleInfoChange(display);
|
|
}
|
|
}
|
|
}
|
|
|
|
@UiThread
|
|
@Override
|
|
@TargetApi(Build.VERSION_CODES.S)
|
|
public final void onConfigurationChanged(Configuration config) {
|
|
Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
|
|
Display display = mWindowContext.getDisplay();
|
|
if (config.densityDpi != mInfo.densityDpi
|
|
|| config.fontScale != mInfo.fontScale
|
|
|| display.getRotation() != mInfo.rotation
|
|
|| !mInfo.mScreenSizeDp.equals(
|
|
new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
|
|
handleInfoChange(display);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final void onLowMemory() { }
|
|
|
|
public void setPriorityListener(DisplayInfoChangeListener listener) {
|
|
mPriorityListener = listener;
|
|
}
|
|
|
|
public void addChangeListener(DisplayInfoChangeListener listener) {
|
|
mListeners.add(listener);
|
|
}
|
|
|
|
public void removeChangeListener(DisplayInfoChangeListener listener) {
|
|
mListeners.remove(listener);
|
|
}
|
|
|
|
public Info getInfo() {
|
|
return mInfo;
|
|
}
|
|
|
|
private Context getDisplayInfoContext(Display display) {
|
|
return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
|
|
}
|
|
|
|
@AnyThread
|
|
@VisibleForTesting
|
|
public void handleInfoChange(Display display) {
|
|
WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext);
|
|
Info oldInfo = mInfo;
|
|
|
|
Context displayInfoContext = getDisplayInfoContext(display);
|
|
Info newInfo = new Info(displayInfoContext, wmProxy, oldInfo.mPerDisplayBounds);
|
|
|
|
if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
|
|
|| newInfo.navigationMode != oldInfo.navigationMode) {
|
|
// Cache may not be valid anymore, recreate without cache
|
|
newInfo = new Info(displayInfoContext, wmProxy,
|
|
wmProxy.estimateInternalDisplayBounds(displayInfoContext));
|
|
}
|
|
|
|
int change = 0;
|
|
if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) {
|
|
change |= CHANGE_ACTIVE_SCREEN;
|
|
}
|
|
if (newInfo.rotation != oldInfo.rotation) {
|
|
change |= CHANGE_ROTATION;
|
|
}
|
|
if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
|
|
change |= CHANGE_DENSITY;
|
|
}
|
|
if (newInfo.navigationMode != oldInfo.navigationMode) {
|
|
change |= CHANGE_NAVIGATION_MODE;
|
|
}
|
|
if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)
|
|
|| !newInfo.mPerDisplayBounds.equals(oldInfo.mPerDisplayBounds)) {
|
|
change |= CHANGE_SUPPORTED_BOUNDS;
|
|
FileLog.w(TAG,
|
|
"(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds);
|
|
}
|
|
if (newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) {
|
|
change |= CHANGE_TASKBAR_PINNING;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
|
|
}
|
|
|
|
if (change != 0) {
|
|
mInfo = newInfo;
|
|
final int flags = change;
|
|
MAIN_EXECUTOR.execute(() -> notifyChange(displayInfoContext, flags));
|
|
}
|
|
}
|
|
|
|
private void notifyChange(Context context, int flags) {
|
|
if (mPriorityListener != null) {
|
|
mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
|
|
}
|
|
|
|
int count = mListeners.size();
|
|
for (int i = 0; i < count; i++) {
|
|
mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
|
|
}
|
|
}
|
|
|
|
public static class Info {
|
|
|
|
// Cached property
|
|
public final CachedDisplayInfo normalizedDisplayInfo;
|
|
public final int rotation;
|
|
public final Point currentSize;
|
|
public final Rect cutout;
|
|
|
|
// Configuration property
|
|
public final float fontScale;
|
|
private final int densityDpi;
|
|
public final NavigationMode navigationMode;
|
|
private final PortraitSize mScreenSizeDp;
|
|
|
|
// WindowBounds
|
|
public final WindowBounds realBounds;
|
|
public final Set<WindowBounds> supportedBounds = new ArraySet<>();
|
|
private final ArrayMap<CachedDisplayInfo, List<WindowBounds>> mPerDisplayBounds =
|
|
new ArrayMap<>();
|
|
|
|
private final boolean mIsTaskbarPinned;
|
|
|
|
public Info(Context displayInfoContext) {
|
|
/* don't need system overrides for external displays */
|
|
this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>());
|
|
}
|
|
|
|
// Used for testing
|
|
public Info(Context displayInfoContext,
|
|
WindowManagerProxy wmProxy,
|
|
Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache) {
|
|
CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext);
|
|
normalizedDisplayInfo = displayInfo.normalize();
|
|
rotation = displayInfo.rotation;
|
|
currentSize = displayInfo.size;
|
|
cutout = displayInfo.cutout;
|
|
|
|
Configuration config = displayInfoContext.getResources().getConfiguration();
|
|
fontScale = config.fontScale;
|
|
densityDpi = config.densityDpi;
|
|
mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp);
|
|
navigationMode = wmProxy.getNavigationMode(displayInfoContext);
|
|
|
|
mPerDisplayBounds.putAll(perDisplayBoundsCache);
|
|
List<WindowBounds> cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
|
|
|
|
realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo);
|
|
if (cachedValue == null) {
|
|
// Unexpected normalizedDisplayInfo is found, recreate the cache
|
|
FileLog.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache: "
|
|
+ normalizedDisplayInfo);
|
|
FileLog.e(TAG, "(Invalid Cache) perDisplayBounds : " + mPerDisplayBounds);
|
|
mPerDisplayBounds.clear();
|
|
mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext));
|
|
cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
|
|
if (cachedValue == null) {
|
|
FileLog.e(TAG, "normalizedDisplayInfo not found in estimation: "
|
|
+ normalizedDisplayInfo);
|
|
supportedBounds.add(realBounds);
|
|
}
|
|
}
|
|
|
|
if (cachedValue != null) {
|
|
// Verify that the real bounds are a match
|
|
WindowBounds expectedBounds = cachedValue.get(displayInfo.rotation);
|
|
if (!realBounds.equals(expectedBounds)) {
|
|
List<WindowBounds> clone = new ArrayList<>(cachedValue);
|
|
clone.set(displayInfo.rotation, realBounds);
|
|
mPerDisplayBounds.put(normalizedDisplayInfo, clone);
|
|
}
|
|
}
|
|
mPerDisplayBounds.values().forEach(supportedBounds::addAll);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "displayInfo: " + displayInfo);
|
|
Log.d(TAG, "realBounds: " + realBounds);
|
|
Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo);
|
|
Log.d(TAG, "perDisplayBounds: " + mPerDisplayBounds);
|
|
}
|
|
|
|
mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING);
|
|
}
|
|
|
|
/**
|
|
* Returns whether taskbar is transient.
|
|
*/
|
|
public boolean isTransientTaskbar() {
|
|
// TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
|
|
// sTransientTaskbarStatusForTests and update test to directly
|
|
// toggle shred preference to switch transient taskbar on/of
|
|
if (!Utilities.isRunningInTestHarness()
|
|
&& ENABLE_TASKBAR_PINNING.get()
|
|
&& mIsTaskbarPinned) {
|
|
return false;
|
|
}
|
|
return navigationMode == NavigationMode.NO_BUTTON
|
|
&& (Utilities.isRunningInTestHarness()
|
|
? sTransientTaskbarStatusForTests
|
|
: ENABLE_TRANSIENT_TASKBAR.get() && !mIsTaskbarPinned);
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the bounds represent a tablet.
|
|
*/
|
|
public boolean isTablet(WindowBounds bounds) {
|
|
return smallestSizeDp(bounds) >= MIN_TABLET_WIDTH;
|
|
}
|
|
|
|
/**
|
|
* Returns smallest size in dp for given bounds.
|
|
*/
|
|
public float smallestSizeDp(WindowBounds bounds) {
|
|
return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), densityDpi);
|
|
}
|
|
|
|
/**
|
|
* Returns all displays for the device
|
|
*/
|
|
public Set<CachedDisplayInfo> getAllDisplays() {
|
|
return Collections.unmodifiableSet(mPerDisplayBounds.keySet());
|
|
}
|
|
|
|
public int getDensityDpi() {
|
|
return densityDpi;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the given binary flags as a human-readable string.
|
|
* @see #CHANGE_ALL
|
|
*/
|
|
public String getChangeFlagsString(int change) {
|
|
StringJoiner result = new StringJoiner("|");
|
|
appendFlag(result, change, CHANGE_ACTIVE_SCREEN, "CHANGE_ACTIVE_SCREEN");
|
|
appendFlag(result, change, CHANGE_ROTATION, "CHANGE_ROTATION");
|
|
appendFlag(result, change, CHANGE_DENSITY, "CHANGE_DENSITY");
|
|
appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS");
|
|
appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE");
|
|
appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT");
|
|
return result.toString();
|
|
}
|
|
|
|
/**
|
|
* Dumps the current state information
|
|
*/
|
|
public void dump(PrintWriter pw) {
|
|
Info info = mInfo;
|
|
pw.println("DisplayController.Info:");
|
|
pw.println(" normalizedDisplayInfo=" + info.normalizedDisplayInfo);
|
|
pw.println(" rotation=" + info.rotation);
|
|
pw.println(" fontScale=" + info.fontScale);
|
|
pw.println(" densityDpi=" + info.densityDpi);
|
|
pw.println(" navigationMode=" + info.navigationMode.name());
|
|
pw.println(" isTaskbarPinned=" + info.mIsTaskbarPinned);
|
|
pw.println(" currentSize=" + info.currentSize);
|
|
info.mPerDisplayBounds.forEach((key, value) -> pw.println(
|
|
" perDisplayBounds - " + key + ": " + value));
|
|
}
|
|
|
|
/**
|
|
* Utility class to hold a size information in an orientation independent way
|
|
*/
|
|
public static class PortraitSize {
|
|
public final int width, height;
|
|
|
|
public PortraitSize(int w, int h) {
|
|
width = Math.min(w, h);
|
|
height = Math.max(w, h);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
PortraitSize that = (PortraitSize) o;
|
|
return width == that.width && height == that.height;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(width, height);
|
|
}
|
|
}
|
|
|
|
}
|