Fixing SecondaryDisplay Launcher

> All apps layout broken in landscpae UI
> Crash when manager profile is added
> Wrong icon size on low resolution

Change-Id: If01dacf9f62a0384ebd8b31b62500178416d3ab4
This commit is contained in:
Sunny Goyal 2020-04-13 17:15:05 -07:00
parent 8451542a25
commit 0e7a338e8b
11 changed files with 398 additions and 117 deletions

View File

@ -17,7 +17,8 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/drag_layer" > android:id="@+id/drag_layer"
android:padding="@dimen/dynamic_grid_edge_margin">
<GridView <GridView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -119,8 +120,7 @@
android:singleLine="true" android:singleLine="true"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:textColorHint="@drawable/all_apps_search_hint" android:textColorHint="@drawable/all_apps_search_hint"
android:textSize="16sp" android:textSize="16sp" />
android:translationY="24dp" />
<include layout="@layout/all_apps_fast_scroller" /> <include layout="@layout/all_apps_fast_scroller" />
</com.android.launcher3.allapps.AllAppsContainerView> </com.android.launcher3.allapps.AllAppsContainerView>

View File

@ -1,5 +1,6 @@
sdk=29 sdk=29
shadows= \ shadows= \
com.android.launcher3.shadows.LShadowApplicationPackageManager \
com.android.launcher3.shadows.LShadowAppPredictionManager \ com.android.launcher3.shadows.LShadowAppPredictionManager \
com.android.launcher3.shadows.LShadowAppWidgetManager \ com.android.launcher3.shadows.LShadowAppWidgetManager \
com.android.launcher3.shadows.LShadowBackupManager \ com.android.launcher3.shadows.LShadowBackupManager \

View File

@ -0,0 +1,133 @@
/*
* Copyright (C) 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 com.android.launcher3.secondarydisplay;
import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
import static com.android.launcher3.util.LauncherUIHelper.doLayout;
import static com.android.launcher3.util.Preconditions.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import android.content.Context;
import android.os.UserManager;
import android.provider.Settings;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.allapps.AllAppsPagedView;
import com.android.launcher3.allapps.AllAppsRecyclerView;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.shadows.ShadowOverrides;
import com.android.launcher3.util.LauncherLayoutBuilder;
import com.android.launcher3.util.LauncherModelHelper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowUserManager;
/**
* Tests for {@link SecondaryDisplayLauncher} with work profile
*/
@RunWith(RobolectricTestRunner.class)
@LooperMode(Mode.PAUSED)
public class SDWorkModeTest {
private static final int SYSTEM_USER = 0;
private static final int FLAG_SYSTEM = 0x00000800;
private static final int WORK_PROFILE_ID = 10;
private static final int FLAG_PROFILE = 0x00001000;
private Context mTargetContext;
private InvariantDeviceProfile mIdp;
private LauncherModelHelper mModelHelper;
private LauncherLayoutBuilder mLayoutBuilder;
@Before
public void setup() throws Exception {
mModelHelper = new LauncherModelHelper();
mTargetContext = RuntimeEnvironment.application;
mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
ShadowOverrides.setProvider(UserEventDispatcher.class,
c -> mock(UserEventDispatcher.class));
Settings.Global.putFloat(mTargetContext.getContentResolver(),
Settings.Global.WINDOW_ANIMATION_SCALE, 0);
mModelHelper.installApp(TEST_PACKAGE);
mLayoutBuilder = new LauncherLayoutBuilder();
}
@Test
public void testAllAppsList_noWorkProfile() throws Exception {
SecondaryDisplayLauncher launcher = loadLauncher();
launcher.showAppDrawer(true);
doLayout(launcher);
verifyRecyclerViewCount(launcher.getAppsView().getActiveRecyclerView());
}
@Test
public void testAllAppsList_workProfile() throws Exception {
ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class));
sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
sum.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
SecondaryDisplayLauncher launcher = loadLauncher();
launcher.showAppDrawer(true);
doLayout(launcher);
AllAppsRecyclerView rv1 = launcher.getAppsView().getActiveRecyclerView();
verifyRecyclerViewCount(rv1);
assertNotNull(launcher.getAppsView().getWorkModeSwitch());
assertTrue(launcher.getAppsView().getRecyclerViewContainer() instanceof AllAppsPagedView);
AllAppsPagedView pagedView =
(AllAppsPagedView) launcher.getAppsView().getRecyclerViewContainer();
pagedView.snapToPageImmediately(1);
doLayout(launcher);
AllAppsRecyclerView rv2 = launcher.getAppsView().getActiveRecyclerView();
verifyRecyclerViewCount(rv2);
assertNotSame(rv1, rv2);
}
private SecondaryDisplayLauncher loadLauncher() throws Exception {
// Install 100 apps
for (int i = 0; i < 100; i++) {
mModelHelper.installApp(TEST_PACKAGE + i);
}
mModelHelper.setupDefaultLayoutProvider(new LauncherLayoutBuilder()).loadModelSync();
SecondaryDisplayLauncher launcher =
Robolectric.buildActivity(SecondaryDisplayLauncher.class).setup().get();
doLayout(launcher);
return launcher;
}
private void verifyRecyclerViewCount(AllAppsRecyclerView rv) {
int childCount = rv.getChildCount();
assertTrue(childCount > 0);
assertTrue(childCount < 100);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 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 com.android.launcher3.shadows;
import android.os.Process;
import android.os.UserHandle;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowApplicationPackageManager;
/**
* Shadow for {@link ShadowApplicationPackageManager} which create mock predictors
*/
@Implements(className = "android.app.ApplicationPackageManager")
public class LShadowApplicationPackageManager extends ShadowApplicationPackageManager {
@Implementation
public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
return Process.myUserHandle().equals(user) ? label : "Work " + label;
}
}

View File

@ -30,7 +30,6 @@ import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.os.Process;
import android.os.UserHandle; import android.os.UserHandle;
import android.util.ArraySet; import android.util.ArraySet;
@ -80,14 +79,15 @@ public class LShadowLauncherApps extends ShadowLauncherApps {
protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) { protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
ResolveInfo ri = RuntimeEnvironment.application.getPackageManager() ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
.resolveActivity(intent, 0); .resolveActivity(intent, 0);
return ri == null ? null : getLauncherActivityInfo(ri.activityInfo); return ri == null ? null : getLauncherActivityInfo(ri.activityInfo, user);
} }
public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) { public LauncherActivityInfo getLauncherActivityInfo(
ActivityInfo activityInfo, UserHandle user) {
return callConstructor(LauncherActivityInfo.class, return callConstructor(LauncherActivityInfo.class,
ClassParameter.from(Context.class, RuntimeEnvironment.application), ClassParameter.from(Context.class, RuntimeEnvironment.application),
ClassParameter.from(ActivityInfo.class, activityInfo), ClassParameter.from(ActivityInfo.class, activityInfo),
ClassParameter.from(UserHandle.class, Process.myUserHandle())); ClassParameter.from(UserHandle.class, user));
} }
@Implementation @Implementation
@ -104,7 +104,7 @@ public class LShadowLauncherApps extends ShadowLauncherApps {
.setPackage(packageName); .setPackage(packageName);
return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0) return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
.stream() .stream()
.map(ri -> getLauncherActivityInfo(ri.activityInfo)) .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -130,7 +130,7 @@ public class LShadowLauncherApps extends ShadowLauncherApps {
Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName); Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName);
return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0) return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
.stream() .stream()
.map(ri -> getLauncherActivityInfo(ri.activityInfo)) .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }

View File

@ -22,7 +22,6 @@ import android.content.res.Resources;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.Surface; import android.view.Surface;
import com.android.launcher3.CellLayout.ContainerType; import com.android.launcher3.CellLayout.ContainerType;
@ -34,6 +33,7 @@ import com.android.launcher3.util.DefaultDisplay;
public class DeviceProfile { public class DeviceProfile {
public final InvariantDeviceProfile inv; public final InvariantDeviceProfile inv;
private final DefaultDisplay.Info mInfo;
// Device properties // Device properties
public final boolean isTablet; public final boolean isTablet;
@ -133,9 +133,9 @@ public class DeviceProfile {
public DotRenderer mDotRendererWorkSpace; public DotRenderer mDotRendererWorkSpace;
public DotRenderer mDotRendererAllApps; public DotRenderer mDotRendererAllApps;
public DeviceProfile(Context context, InvariantDeviceProfile inv, public DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
Point minSize, Point maxSize, Point minSize, Point maxSize, int width, int height, boolean isLandscape,
int width, int height, boolean isLandscape, boolean isMultiWindowMode) { boolean isMultiWindowMode, boolean transposeLayoutWithOrientation) {
this.inv = inv; this.inv = inv;
this.isLandscape = isLandscape; this.isLandscape = isLandscape;
@ -152,8 +152,8 @@ public class DeviceProfile {
availableHeightPx = maxSize.y; availableHeightPx = maxSize.y;
} }
mInfo = info;
Resources res = context.getResources(); Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
// Constants from resources // Constants from resources
isTablet = res.getBoolean(R.bool.is_tablet); isTablet = res.getBoolean(R.bool.is_tablet);
@ -163,8 +163,7 @@ public class DeviceProfile {
boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0; boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
// Some more constants // Some more constants
transposeLayoutWithOrientation = this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
context = getContext(context, isVerticalBarLayout() context = getContext(context, isVerticalBarLayout()
? Configuration.ORIENTATION_LANDSCAPE ? Configuration.ORIENTATION_LANDSCAPE
@ -207,13 +206,14 @@ public class DeviceProfile {
res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding); res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
// Add a bit of space between nav bar and hotseat in vertical bar layout. // Add a bit of space between nav bar and hotseat in vertical bar layout.
hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0; hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, dm) + (isVerticalBarLayout() hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+ (isVerticalBarLayout()
? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx) ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
: (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size) : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
+ hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx)); + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
// Calculate all of the remaining variables. // Calculate all of the remaining variables.
updateAvailableDimensions(dm, res); updateAvailableDimensions(res);
// Now that we have all of the variables calculated, we can tune certain sizes. // Now that we have all of the variables calculated, we can tune certain sizes.
if (!isVerticalBarLayout() && isPhone && isTallDevice) { if (!isVerticalBarLayout() && isPhone && isTallDevice) {
@ -227,7 +227,7 @@ public class DeviceProfile {
hotseatBarBottomPaddingPx += extraSpace; hotseatBarBottomPaddingPx += extraSpace;
// Recalculate the available dimensions using the new hotseat size. // Recalculate the available dimensions using the new hotseat size.
updateAvailableDimensions(dm, res); updateAvailableDimensions(res);
} }
updateWorkspacePadding(); updateWorkspacePadding();
@ -239,12 +239,21 @@ public class DeviceProfile {
IconShape.DEFAULT_PATH_SIZE); IconShape.DEFAULT_PATH_SIZE);
} }
public DeviceProfile copy(Context context) { public Builder toBuilder(Context context) {
Point size = new Point(availableWidthPx, availableHeightPx); Point size = new Point(availableWidthPx, availableHeightPx);
return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape, return new Builder(context, inv, mInfo)
isMultiWindowMode); .setSizeRange(size, size)
.setSize(widthPx, heightPx)
.setMultiWindowMode(isMultiWindowMode);
} }
public DeviceProfile copy(Context context) {
return toBuilder(context).build();
}
/**
* TODO: Move this to the builder as part of setMultiWindowMode
*/
public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) { public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
// We take the minimum sizes of this profile and it's multi-window variant to ensure that // We take the minimum sizes of this profile and it's multi-window variant to ensure that
// the system decor is always excluded. // the system decor is always excluded.
@ -253,8 +262,11 @@ public class DeviceProfile {
// In multi-window mode, we can have widthPx = availableWidthPx // In multi-window mode, we can have widthPx = availableWidthPx
// and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles' // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
// widthPx and heightPx values where it's needed. // widthPx and heightPx values where it's needed.
DeviceProfile profile = new DeviceProfile(context, inv, mwSize, mwSize, mwSize.x, mwSize.y, DeviceProfile profile = toBuilder(context)
isLandscape, true); .setSizeRange(mwSize, mwSize)
.setSize(mwSize.x, mwSize.y)
.setMultiWindowMode(true)
.build();
// If there isn't enough vertical cell padding with the labels displayed, hide the labels. // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@ -289,27 +301,30 @@ public class DeviceProfile {
iconTextSizePx = 0; iconTextSizePx = 0;
iconDrawablePaddingPx = 0; iconDrawablePaddingPx = 0;
cellHeightPx = iconSizePx; cellHeightPx = iconSizePx;
autoResizeAllAppsCells();
}
// In normal cases, All Apps cell height should equal the Workspace cell height. /**
// Since we are removing labels from the Workspace, we need to manually compute the * Re-computes the all-apps cell size to be independent of workspace
// All Apps cell height. */
public void autoResizeAllAppsCells() {
int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1); int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1);
allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
+ Utilities.calculateTextHeight(allAppsIconTextSizePx) + Utilities.calculateTextHeight(allAppsIconTextSizePx)
+ topBottomPadding * 2; + topBottomPadding * 2;
} }
private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { private void updateAvailableDimensions(Resources res) {
updateIconSize(1f, res, dm); updateIconSize(1f, res);
// Check to see if the icons fit within the available height. If not, then scale down. // Check to see if the icons fit within the available height. If not, then scale down.
float usedHeight = (cellHeightPx * inv.numRows); float usedHeight = (cellHeightPx * inv.numRows);
int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y); int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
if (usedHeight > maxHeight) { if (usedHeight > maxHeight) {
float scale = maxHeight / usedHeight; float scale = maxHeight / usedHeight;
updateIconSize(scale, res, dm); updateIconSize(scale, res);
} }
updateAvailableFolderCellDimensions(dm, res); updateAvailableFolderCellDimensions(res);
} }
/** /**
@ -317,12 +332,13 @@ public class DeviceProfile {
* iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants, * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
* hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx. * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
*/ */
private void updateIconSize(float scale, Resources res, DisplayMetrics dm) { private void updateIconSize(float scale, Resources res) {
// Workspace // Workspace
final boolean isVerticalLayout = isVerticalBarLayout(); final boolean isVerticalLayout = isVerticalBarLayout();
float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize; float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, dm) * scale)); iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale); * scale));
iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale); iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
cellHeightPx = iconSizePx + iconDrawablePaddingPx cellHeightPx = iconSizePx + iconDrawablePaddingPx
@ -340,8 +356,8 @@ public class DeviceProfile {
// All apps // All apps
if (allAppsHasDifferentNumColumns()) { if (allAppsHasDifferentNumColumns()) {
allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, dm); allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, dm); allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y; allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y;
allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx; allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
} else { } else {
@ -381,12 +397,12 @@ public class DeviceProfile {
folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2; folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
} }
private void updateAvailableFolderCellDimensions(DisplayMetrics dm, Resources res) { private void updateAvailableFolderCellDimensions(Resources res) {
int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top) int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
+ res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom) + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
+ Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size)); + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
updateFolderCellSize(1f, dm, res); updateFolderCellSize(1f, res);
// Don't let the folder get too close to the edges of the screen. // Don't let the folder get too close to the edges of the screen.
int folderMargin = edgeMarginPx * 2; int folderMargin = edgeMarginPx * 2;
@ -405,12 +421,12 @@ public class DeviceProfile {
float scale = Math.min(scaleX, scaleY); float scale = Math.min(scaleX, scaleY);
if (scale < 1f) { if (scale < 1f) {
updateFolderCellSize(scale, dm, res); updateFolderCellSize(scale, res);
} }
} }
private void updateFolderCellSize(float scale, DisplayMetrics dm, Resources res) { private void updateFolderCellSize(float scale, Resources res) {
folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, dm) * scale); folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
folderChildTextSizePx = folderChildTextSizePx =
(int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale); (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
@ -627,4 +643,55 @@ public class DeviceProfile {
*/ */
void onDeviceProfileChanged(DeviceProfile dp); void onDeviceProfileChanged(DeviceProfile dp);
} }
public static class Builder {
private Context mContext;
private InvariantDeviceProfile mInv;
private DefaultDisplay.Info mInfo;
private Point mMinSize, mMaxSize;
private int mWidth, mHeight;
private boolean mIsLandscape;
private boolean mIsMultiWindowMode = false;
private boolean mTransposeLayoutWithOrientation;
public Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info) {
mContext = context;
mInv = inv;
mInfo = info;
mTransposeLayoutWithOrientation = context.getResources()
.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
}
public Builder setSizeRange(Point minSize, Point maxSize) {
mMinSize = minSize;
mMaxSize = maxSize;
return this;
}
public Builder setSize(int width, int height) {
mWidth = width;
mHeight = height;
mIsLandscape = mWidth > mHeight;
return this;
}
public Builder setMultiWindowMode(boolean isMultiWindowMode) {
mIsMultiWindowMode = isMultiWindowMode;
return this;
}
public Builder setTransposeLayoutWithOrientation(boolean transposeLayoutWithOrientation) {
mTransposeLayoutWithOrientation = transposeLayoutWithOrientation;
return this;
}
public DeviceProfile build() {
return new DeviceProfile(mContext, mInv, mInfo, mMinSize, mMaxSize,
mWidth, mHeight, mIsLandscape, mIsMultiWindowMode,
mTransposeLayoutWithOrientation);
}
}
} }

View File

@ -28,6 +28,7 @@ import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
@ -159,10 +160,12 @@ public class InvariantDeviceProfile {
@TargetApi(23) @TargetApi(23)
private InvariantDeviceProfile(Context context) { private InvariantDeviceProfile(Context context) {
String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false) String gridName = getCurrentGridName(context);
? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) String newGridName = initGrid(context, gridName);
: null; if (!newGridName.equals(gridName)) {
initGrid(context, gridName); Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply();
}
mConfigMonitor = new ConfigMonitor(context, mConfigMonitor = new ConfigMonitor(context,
APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess); APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
mOverlayMonitor = new OverlayMonitor(context); mOverlayMonitor = new OverlayMonitor(context);
@ -178,17 +181,36 @@ public class InvariantDeviceProfile {
} }
} }
/** /**
* This constructor should NOT have any monitors by design. * This constructor should NOT have any monitors by design.
*/ */
public InvariantDeviceProfile(Context context, Display display) { public InvariantDeviceProfile(Context context, Display display) {
initGrid(context, null, new Info(display)); // Ensure that the main device profile is initialized
InvariantDeviceProfile originalProfile = INSTANCE.get(context);
String gridName = getCurrentGridName(context);
// Get the display info based on default display and interpolate it to existing display
DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
DefaultDisplay.INSTANCE.get(context).getInfo(),
getPredefinedDeviceProfiles(context, gridName));
Info myInfo = new Info(display);
DisplayOption myDisplayOption = invDistWeightedInterpolate(
myInfo, getPredefinedDeviceProfiles(context, gridName));
DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
.add(myDisplayOption);
result.iconSize = defaultDisplayOption.iconSize;
result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
result.allAppsIconSize = Math.min(
defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
initGrid(context, myInfo, result);
} }
public static String getCurrentGridName(Context context) { public static String getCurrentGridName(Context context) {
return Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false) SharedPreferences prefs = Utilities.getPrefs(context);
? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) return prefs.getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
: null; ? prefs.getString(KEY_IDP_GRID_NAME, null) : null;
} }
/** /**
@ -203,27 +225,17 @@ public class InvariantDeviceProfile {
} }
private String initGrid(Context context, String gridName) { private String initGrid(Context context, String gridName) {
return initGrid(context, gridName, DefaultDisplay.INSTANCE.get(context).getInfo()); DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
initGrid(context, displayInfo, displayOption);
return displayOption.grid.name;
} }
private String initGrid(Context context, String gridName, DefaultDisplay.Info displayInfo) { private void initGrid(
Point smallestSize = new Point(displayInfo.smallestSize); Context context, DefaultDisplay.Info displayInfo, DisplayOption displayOption) {
Point largestSize = new Point(displayInfo.largestSize); GridOption closestProfile = displayOption.grid;
ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
// This guarantees that width < height
float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
displayInfo.metrics);
float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
displayInfo.metrics);
// Sort the profiles based on the closeness to the device size
Collections.sort(allOptions, (a, b) ->
Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
DisplayOption interpolatedDisplayOption =
invDistWeightedInterpolate(minWidthDps, minHeightDps, allOptions);
GridOption closestProfile = allOptions.get(0).grid;
numRows = closestProfile.numRows; numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns; numColumns = closestProfile.numColumns;
numHotseatIcons = closestProfile.numHotseatIcons; numHotseatIcons = closestProfile.numHotseatIcons;
@ -236,21 +248,16 @@ public class InvariantDeviceProfile {
mExtraAttrs = closestProfile.extraAttrs; mExtraAttrs = closestProfile.extraAttrs;
if (!closestProfile.name.equals(gridName)) { iconSize = displayOption.iconSize;
Utilities.getPrefs(context).edit()
.putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
}
iconSize = interpolatedDisplayOption.iconSize;
iconShapePath = getIconShapePath(context); iconShapePath = getIconShapePath(context);
landscapeIconSize = interpolatedDisplayOption.landscapeIconSize; landscapeIconSize = displayOption.landscapeIconSize;
iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics); iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
iconTextSize = interpolatedDisplayOption.iconTextSize; iconTextSize = displayOption.iconTextSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize); fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
if (Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) { if (Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) {
allAppsIconSize = interpolatedDisplayOption.allAppsIconSize; allAppsIconSize = displayOption.allAppsIconSize;
allAppsIconTextSize = interpolatedDisplayOption.allAppsIconTextSize; allAppsIconTextSize = displayOption.allAppsIconTextSize;
} else { } else {
allAppsIconSize = iconSize; allAppsIconSize = iconSize;
allAppsIconTextSize = iconTextSize; allAppsIconTextSize = iconTextSize;
@ -266,10 +273,12 @@ public class InvariantDeviceProfile {
int smallSide = Math.min(realSize.x, realSize.y); int smallSide = Math.min(realSize.x, realSize.y);
int largeSide = Math.max(realSize.x, realSize.y); int largeSide = Math.max(realSize.x, realSize.y);
landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize, DeviceProfile.Builder builder = new DeviceProfile.Builder(context, this, displayInfo)
largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */); .setSizeRange(new Point(displayInfo.smallestSize),
portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize, new Point(displayInfo.largestSize));
smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);
landscapeProfile = builder.setSize(largeSide, smallSide).build();
portraitProfile = builder.setSize(smallSide, largeSide).build();
// We need to ensure that there is enough extra space in the wallpaper // We need to ensure that there is enough extra space in the wallpaper
// for the intended parallax effects // for the intended parallax effects
@ -283,8 +292,6 @@ public class InvariantDeviceProfile {
ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName()); ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null); defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
return closestProfile.name;
} }
@Nullable @Nullable
@ -453,6 +460,41 @@ public class InvariantDeviceProfile {
return (float) Math.hypot(x1 - x0, y1 - y0); return (float) Math.hypot(x1 - x0, y1 - y0);
} }
@VisibleForTesting
static DisplayOption invDistWeightedInterpolate(
DefaultDisplay.Info displayInfo, ArrayList<DisplayOption> points) {
Point smallestSize = new Point(displayInfo.smallestSize);
Point largestSize = new Point(displayInfo.largestSize);
// This guarantees that width < height
float width = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
displayInfo.metrics);
float height = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
displayInfo.metrics);
// Sort the profiles based on the closeness to the device size
Collections.sort(points, (a, b) ->
Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps)));
GridOption closestOption = points.get(0).grid;
float weights = 0;
DisplayOption p = points.get(0);
if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
return p;
}
DisplayOption out = new DisplayOption(closestOption);
for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
p = points.get(i);
float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
weights += w;
out.add(new DisplayOption().add(p).multiply(w));
}
return out.multiply(1.0f / weights);
}
@VisibleForTesting @VisibleForTesting
static DisplayOption invDistWeightedInterpolate(float width, float height, static DisplayOption invDistWeightedInterpolate(float width, float height,
ArrayList<DisplayOption> points) { ArrayList<DisplayOption> points) {
@ -573,7 +615,6 @@ public class InvariantDeviceProfile {
private static final class DisplayOption { private static final class DisplayOption {
private final GridOption grid; private final GridOption grid;
private final String name;
private final float minWidthDps; private final float minWidthDps;
private final float minHeightDps; private final float minHeightDps;
private final boolean canBeDefault; private final boolean canBeDefault;
@ -590,7 +631,6 @@ public class InvariantDeviceProfile {
TypedArray a = context.obtainStyledAttributes( TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ProfileDisplayOption); attrs, R.styleable.ProfileDisplayOption);
name = a.getString(R.styleable.ProfileDisplayOption_name);
minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0); minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0);
minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0); minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0);
canBeDefault = a.getBoolean( canBeDefault = a.getBoolean(
@ -609,8 +649,11 @@ public class InvariantDeviceProfile {
} }
DisplayOption() { DisplayOption() {
grid = null; this(null);
name = null; }
DisplayOption(GridOption grid) {
this.grid = grid;
minWidthDps = 0; minWidthDps = 0;
minHeightDps = 0; minHeightDps = 0;
canBeDefault = false; canBeDefault = false;

View File

@ -18,6 +18,7 @@ package com.android.launcher3.allapps;
import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.AsyncTask; import android.os.AsyncTask;
@ -30,7 +31,6 @@ import android.view.ViewConfiguration;
import android.widget.Switch; import android.widget.Switch;
import com.android.launcher3.Insettable; import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.Utilities; import com.android.launcher3.Utilities;
import com.android.launcher3.pm.UserCache; import com.android.launcher3.pm.UserCache;
@ -79,9 +79,8 @@ public class WorkModeSwitch extends Switch implements Insettable {
@Override @Override
public void toggle() { public void toggle() {
Launcher launcher = Launcher.getLauncher(getContext());
// don't show tip if user uses toggle // don't show tip if user uses toggle
launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply(); Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
trySetQuietModeEnabledToAllProfilesAsync(isChecked()); trySetQuietModeEnabledToAllProfilesAsync(isChecked());
} }
@ -203,9 +202,8 @@ public class WorkModeSwitch extends Switch implements Insettable {
} }
private boolean shouldShowWorkSwitch() { private boolean shouldShowWorkSwitch() {
Launcher launcher = Launcher.getLauncher(getContext()); return Utilities.ATLEAST_P && (hasShortcutsPermission(getContext())
return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher) || getContext().checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
|| launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
== PackageManager.PERMISSION_GRANTED); == PackageManager.PERMISSION_GRANTED);
} }
@ -213,12 +211,14 @@ public class WorkModeSwitch extends Switch implements Insettable {
* Shows a work tip on the Nth work tab open * Shows a work tip on the Nth work tab open
*/ */
public void showTipifNeeded() { public void showTipifNeeded() {
Launcher launcher = Launcher.getLauncher(getContext()); Context context = getContext();
int tipCounter = launcher.getSharedPrefs().getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD); SharedPreferences prefs = Utilities.getPrefs(context);
int tipCounter = prefs.getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
if (tipCounter < 0) return; if (tipCounter < 0) return;
if (tipCounter == 0) { if (tipCounter == 0) {
new ArrowTipView(launcher).show(launcher.getString(R.string.work_switch_tip), getTop()); new ArrowTipView(context)
.show(context.getString(R.string.work_switch_tip), getTop());
} }
launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply(); prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
} }
} }

View File

@ -89,17 +89,16 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity
if (mDragLayer != null) { if (mDragLayer != null) {
return; return;
} }
InvariantDeviceProfile mainIdp = LauncherAppState.getIDP(this);
InvariantDeviceProfile currentDisplayIdp = InvariantDeviceProfile currentDisplayIdp =
new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay()); new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay());
// Pick the device profile with the smaller icon size so that the cached icons are // Disable transpose layout and use multi-window mode so that the icons are scaled properly
// shown properly mDeviceProfile = currentDisplayIdp.getDeviceProfile(this)
if (mainIdp.iconBitmapSize <= currentDisplayIdp.iconBitmapSize) { .toBuilder(this)
mDeviceProfile = mainIdp.getDeviceProfile(this).copy(this); .setMultiWindowMode(true)
} else { .setTransposeLayoutWithOrientation(false)
mDeviceProfile = currentDisplayIdp.getDeviceProfile(this); .build();
} mDeviceProfile.autoResizeAllAppsCells();
setContentView(R.layout.secondary_launcher); setContentView(R.layout.secondary_launcher);
mDragLayer = findViewById(R.id.drag_layer); mDragLayer = findViewById(R.id.drag_layer);

View File

@ -117,10 +117,12 @@ public class SecondaryDragLayer extends BaseDragLayer<SecondaryDisplayLauncher>
if (child == mAppsView) { if (child == mAppsView) {
int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx
+ grid.cellLayoutPaddingLeftRightPx); + grid.cellLayoutPaddingLeftRightPx);
int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
int appsWidth = Math.min(width, maxWidth); int appsWidth = Math.min(width, maxWidth);
int appsHeight = Math.round(appsWidth * (float) height / (float) width);
int maxHeight = grid.allAppsCellHeightPx * idp.numAllAppsColumns + padding;
int appsHeight = Math.min(height, maxHeight);
mAppsView.measure( mAppsView.measure(
makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY)); makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY));

View File

@ -32,7 +32,7 @@ import android.widget.TextView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher; import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragLayer;
@ -48,13 +48,13 @@ public class ArrowTipView extends AbstractFloatingView {
private static final long SHOW_DURATION_MS = 300; private static final long SHOW_DURATION_MS = 300;
private static final long HIDE_DURATION_MS = 100; private static final long HIDE_DURATION_MS = 100;
protected final Launcher mLauncher; protected final BaseDraggingActivity mActivity;
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();
private Runnable mOnClosed; private Runnable mOnClosed;
public ArrowTipView(Context context) { public ArrowTipView(Context context) {
super(context, null, 0); super(context, null, 0);
mLauncher = Launcher.getLauncher(context); mActivity = BaseDraggingActivity.fromContext(context);
init(context); init(context);
} }
@ -75,11 +75,11 @@ public class ArrowTipView extends AbstractFloatingView {
.setStartDelay(0) .setStartDelay(0)
.setDuration(HIDE_DURATION_MS) .setDuration(HIDE_DURATION_MS)
.setInterpolator(Interpolators.ACCEL) .setInterpolator(Interpolators.ACCEL)
.withEndAction(() -> mLauncher.getDragLayer().removeView(this)) .withEndAction(() -> mActivity.getDragLayer().removeView(this))
.start(); .start();
} else { } else {
animate().cancel(); animate().cancel();
mLauncher.getDragLayer().removeView(this); mActivity.getDragLayer().removeView(this);
} }
if (mOnClosed != null) mOnClosed.run(); if (mOnClosed != null) mOnClosed.run();
mIsOpen = false; mIsOpen = false;
@ -126,12 +126,12 @@ public class ArrowTipView extends AbstractFloatingView {
*/ */
public ArrowTipView show(String text, int top) { public ArrowTipView show(String text, int top) {
((TextView) findViewById(R.id.text)).setText(text); ((TextView) findViewById(R.id.text)).setText(text);
mLauncher.getDragLayer().addView(this); mActivity.getDragLayer().addView(this);
DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams(); DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
params.gravity = Gravity.CENTER_HORIZONTAL; params.gravity = Gravity.CENTER_HORIZONTAL;
params.leftMargin = mLauncher.getDeviceProfile().workspacePadding.left; params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left;
params.rightMargin = mLauncher.getDeviceProfile().workspacePadding.right; params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right;
post(() -> setY(top - getHeight())); post(() -> setY(top - getHeight()));
setAlpha(0); setAlpha(0);
animate() animate()