Files
lawnchair/src/com/android/launcher3/util/DisplayController.java
Tony Wickham eccf070bae Change TAPL to swipe to stash taskbar (instead of deprecated long press)
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
2023-11-01 23:16:10 +00:00

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