c6d625b8db
This CL allows user to long press on Taskbar divider view to bring up divider popup view. It also included functionality of allowing user to turn on always show taskbar from the divider popup view. Test: Manual Bug: 265436055 Bug: 265434718 Bug: 265434902 Bug: 265434705 Flag: ENABLE_TASKBAR_PINNING Change-Id: Ied54d718483a9b06b053d68988e5c294a786002a
470 lines
18 KiB
Java
470 lines
18 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.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.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.util.window.CachedDisplayInfo;
|
|
import com.android.launcher3.util.window.WindowManagerProxy;
|
|
|
|
import java.io.PrintWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
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;
|
|
|
|
// 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_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
|
|
| CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE;
|
|
|
|
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 final LauncherPrefs mPrefs;
|
|
|
|
private DisplayController(Context context) {
|
|
mContext = context;
|
|
mDM = context.getSystemService(DisplayManager.class);
|
|
mPrefs = LauncherPrefs.get(context);
|
|
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* 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).isTransientTaskbar();
|
|
}
|
|
|
|
/**
|
|
* Returns whether taskbar is transient.
|
|
*/
|
|
public boolean isTransientTaskbar() {
|
|
// TODO(b/258604917): When running in test harness, use !sTransientTaskbarStatusForTests
|
|
// once tests are updated to expect new persistent behavior such as not allowing long press
|
|
// to stash.
|
|
if (!Utilities.isRunningInTestHarness()
|
|
&& ENABLE_TASKBAR_PINNING.get()
|
|
&& mPrefs.get(TASKBAR_PINNING)) {
|
|
return false;
|
|
}
|
|
return getInfo().navigationMode == NavigationMode.NO_BUTTON
|
|
&& (Utilities.isRunningInTestHarness()
|
|
? sTransientTaskbarStatusForTests
|
|
: ENABLE_TRANSIENT_TASKBAR.get());
|
|
}
|
|
|
|
/**
|
|
* Enables transient taskbar status for tests.
|
|
*/
|
|
@VisibleForTesting
|
|
public static void enableTransientTaskbarForTests(boolean enable) {
|
|
sTransientTaskbarStatusForTests = enable;
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
mDestroyed = true;
|
|
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
|
|
private 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;
|
|
}
|
|
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, WindowBounds[]> mPerDisplayBounds =
|
|
new ArrayMap<>();
|
|
|
|
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, 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);
|
|
WindowBounds[] cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
|
|
|
|
realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo);
|
|
if (cachedValue == null) {
|
|
// Unexpected normalizedDisplayInfo is found, recreate the cache
|
|
Log.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache");
|
|
mPerDisplayBounds.clear();
|
|
mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext));
|
|
cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
|
|
if (cachedValue == null) {
|
|
Log.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[displayInfo.rotation];
|
|
if (!realBounds.equals(expectedBounds)) {
|
|
WindowBounds[] clone = new WindowBounds[4];
|
|
System.arraycopy(cachedValue, 0, clone, 0, 4);
|
|
clone[displayInfo.rotation] = realBounds;
|
|
mPerDisplayBounds.put(normalizedDisplayInfo, clone);
|
|
}
|
|
}
|
|
mPerDisplayBounds.values().forEach(
|
|
windowBounds -> Collections.addAll(supportedBounds, windowBounds));
|
|
if (DEBUG) {
|
|
Log.d(TAG, "displayInfo: " + displayInfo);
|
|
Log.d(TAG, "realBounds: " + realBounds);
|
|
Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo);
|
|
mPerDisplayBounds.forEach((key, value) -> Log.d(TAG,
|
|
"perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
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(" currentSize=" + info.currentSize);
|
|
info.mPerDisplayBounds.forEach((key, value) -> pw.println(
|
|
" perDisplayBounds - " + key + ": " + Arrays.deepToString(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);
|
|
}
|
|
}
|
|
|
|
}
|