Snap for 7475038 from 3fcc02b8dc68d05e9acbca532e762e691efdfd0c to sc-v2-release
Change-Id: I26df7eb88d0c53c154a772569b784498fc837f6d
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 872633c..4eb2d53 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -132,7 +132,7 @@
unbindService(mTisBinderConnection);
if (mTaskbarManager != null) {
- mTaskbarManager.setLauncher(null);
+ mTaskbarManager.clearLauncher(this);
}
super.onDestroy();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 7d0afe1..12ac0f5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -98,11 +98,11 @@
@Override
protected void onDestroy() {
+ onLauncherResumedOrPaused(false);
mIconAlignmentForResumedState.finishAnimation();
mIconAlignmentForGestureState.finishAnimation();
mHotseatController.cleanup();
- setTaskbarViewVisible(true);
mLauncher.getHotseat().setIconsAlpha(1f);
mLauncher.setTaskbarUIController(null);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 2facd44..4e85eb4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -30,6 +30,7 @@
import android.animation.ObjectAnimator;
import android.annotation.DrawableRes;
+import android.annotation.IdRes;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.Region.Op;
@@ -123,7 +124,8 @@
flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
// Rotation button
- RotationButton rotationButton = new RotationButtonImpl(addButton(mEndContainer));
+ RotationButton rotationButton = new RotationButtonImpl(
+ addButton(mEndContainer, R.id.rotate_suggestion));
rotationButton.hide();
mControllers.rotationButtonController.setRotationButton(rotationButton);
} else {
@@ -138,7 +140,7 @@
TaskbarNavButtonController navButtonController) {
View backButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
- startContainer, navButtonController);
+ startContainer, navButtonController, R.id.back);
// Rotate when Ime visible
mPropertyHolders.add(new StatePropertyHolder(backButton,
flags -> (flags & FLAG_IME_VISIBLE) == 0, View.ROTATION, 0,
@@ -149,19 +151,19 @@
// home and recents buttons
View homeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, startContainer,
- navButtonController);
+ navButtonController, R.id.home);
mPropertyHolders.add(new StatePropertyHolder(homeButton,
flags -> (flags & FLAG_IME_VISIBLE) == 0 &&
(flags & FLAG_KEYGUARD_VISIBLE) == 0));
View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS,
- startContainer, navButtonController);
+ startContainer, navButtonController, R.id.recent_apps);
mPropertyHolders.add(new StatePropertyHolder(recentsButton,
flags -> (flags & FLAG_IME_VISIBLE) == 0 &&
(flags & FLAG_KEYGUARD_VISIBLE) == 0));
// IME switcher
View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
- endContainer, navButtonController);
+ endContainer, navButtonController, R.id.ime_switcher);
mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton,
flags -> ((flags & MASK_IME_SWITCHER_VISIBLE) == MASK_IME_SWITCHER_VISIBLE)
&& ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)
@@ -169,7 +171,7 @@
// A11y button
mA11yButton = addButton(R.drawable.ic_sysbar_accessibility_button, BUTTON_A11Y,
- endContainer, navButtonController);
+ endContainer, navButtonController, R.id.accessibility_button);
mPropertyHolders.add(new StatePropertyHolder(mA11yButton,
flags -> (flags & FLAG_A11Y_VISIBLE) != 0
&& (flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0));
@@ -251,16 +253,17 @@
}
private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType,
- ViewGroup parent, TaskbarNavButtonController navButtonController) {
- ImageView buttonView = addButton(parent);
+ ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) {
+ ImageView buttonView = addButton(parent, id);
buttonView.setImageResource(drawableId);
buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType));
return buttonView;
}
- private ImageView addButton(ViewGroup parent) {
+ private ImageView addButton(ViewGroup parent, int id) {
ImageView buttonView = (ImageView) mContext.getLayoutInflater()
.inflate(R.layout.taskbar_nav_button, parent, false);
+ buttonView.setId(id);
parent.addView(buttonView);
mAllButtons.add(buttonView);
return buttonView;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 9f5ea50..788a36b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -26,7 +26,7 @@
import android.hardware.display.DisplayManager;
import android.view.Display;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.DeviceProfile;
@@ -103,14 +103,25 @@
}
/**
- * Sets or clears a launcher to act as taskbar callback
+ * Sets a launcher to act as taskbar callback
*/
- public void setLauncher(@Nullable BaseQuickstepLauncher launcher) {
+ public void setLauncher(@NonNull BaseQuickstepLauncher launcher) {
mLauncher = launcher;
if (mTaskbarActivityContext != null) {
- mTaskbarActivityContext.setUIController(mLauncher == null
- ? TaskbarUIController.DEFAULT
- : new LauncherTaskbarUIController(launcher, mTaskbarActivityContext));
+ mTaskbarActivityContext.setUIController(
+ new LauncherTaskbarUIController(launcher, mTaskbarActivityContext));
+ }
+ }
+
+ /**
+ * Clears a previously set Launcher
+ */
+ public void clearLauncher(@NonNull BaseQuickstepLauncher launcher) {
+ if (mLauncher == launcher) {
+ mLauncher = null;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 8549ca2..dd7c403 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -23,6 +23,8 @@
import androidx.annotation.IntDef;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TouchInteractionService;
@@ -94,10 +96,8 @@
}
private void navigateToOverview() {
- int commandType = mService.getOverviewCommandHelper().isOverviewVisible() ?
- OverviewCommandHelper.TYPE_TOGGLE :
- OverviewCommandHelper.TYPE_SHOW;
- mService.getOverviewCommandHelper().addCommand(commandType);
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
+ mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
}
private void executeBack() {
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 728fec2..2beef0a 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -136,11 +136,6 @@
}
}
- public boolean isOverviewVisible() {
- BaseActivityInterface activityInterface =
- mOverviewComponentObserver.getActivityInterface();
- return activityInterface.getVisibleRecentsView() != null;
- }
/**
* Executes the task and returns true if next task can be executed. If false, then the next
* task is deferred until {@link #scheduleNextTask} is called
diff --git a/res/drawable/notification_circle.xml b/res/drawable/notification_circle.xml
new file mode 100644
index 0000000..65fbaea
--- /dev/null
+++ b/res/drawable/notification_circle.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <solid android:color="?attr/popupNotificationDotColor"/>
+
+ <size
+ android:width="@dimen/notification_circle_icon_size"
+ android:height="@dimen/notification_circle_icon_size"/>
+</shape>
\ No newline at end of file
diff --git a/res/layout/notification_content.xml b/res/layout/notification_content.xml
index 147aa30..84822a6 100644
--- a/res/layout/notification_content.xml
+++ b/res/layout/notification_content.xml
@@ -23,35 +23,36 @@
<FrameLayout
android:id="@+id/header"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_header_height"
- android:paddingEnd="@dimen/notification_padding_end"
- android:paddingStart="@dimen/notification_padding_start">
+ android:layout_height="wrap_content"
+ android:paddingEnd="@dimen/notification_padding"
+ android:paddingStart="@dimen/notification_padding">
<TextView
android:id="@+id/notification_text"
+ android:paddingTop="@dimen/notification_padding"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="start"
- android:gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|start"
android:text="@string/notifications_header"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_header_text_size" />
+ android:textSize="@dimen/notification_header_text_size"
+ style="@style/TextHeadline"/>
<TextView
android:id="@+id/notification_count"
- android:layout_width="@dimen/notification_icon_size"
- android:layout_height="match_parent"
- android:layout_gravity="end"
- android:fontFamily="sans-serif-medium"
+ android:layout_width="@dimen/notification_circle_icon_size"
+ android:layout_height="@dimen/notification_circle_icon_size"
+ android:background="@drawable/notification_circle"
+ android:layout_gravity="bottom|end"
android:gravity="center"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_header_count_text_size" />
+ android:textSize="@dimen/notification_header_count_text_size"
+ style="@style/TextHeadline"/>
</FrameLayout>
<!-- Main view -->
<com.android.launcher3.notification.NotificationMainView
android:id="@+id/main_view"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_main_height"
- android:background="@drawable/bg_notification_content"
+ android:layout_height="wrap_content"
android:focusable="true" >
<LinearLayout
@@ -61,28 +62,28 @@
android:background="?attr/popupColorPrimary"
android:gravity="center_vertical"
android:orientation="vertical"
- android:paddingBottom="14dp"
- android:paddingEnd="@dimen/notification_main_text_padding_end"
- android:paddingStart="@dimen/notification_padding_start">
+ android:paddingTop="@dimen/notification_padding"
+ android:paddingBottom="@dimen/notification_padding"
+ android:paddingEnd="@dimen/notification_padding"
+ android:paddingStart="@dimen/notification_main_text_padding_start">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
- android:fontFamily="sans-serif"
android:lines="1"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_main_title_size" />
+ android:textSize="@dimen/notification_main_title_size"
+ style="@style/TextHeadline" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
- android:fontFamily="sans-serif"
android:lines="1"
- android:textColor="?android:attr/textColorSecondary"
+ android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/notification_main_text_size" />
</LinearLayout>
@@ -90,37 +91,9 @@
android:id="@+id/popup_item_icon"
android:layout_width="@dimen/notification_icon_size"
android:layout_height="@dimen/notification_icon_size"
- android:layout_gravity="center_vertical|end"
- android:layout_marginBottom="7dp"
- android:layout_marginEnd="@dimen/notification_padding_end" />
+ android:layout_gravity="start"
+ android:layout_marginTop="@dimen/notification_padding"
+ android:layout_marginStart="@dimen/notification_icon_padding" />
</com.android.launcher3.notification.NotificationMainView>
-
- <!-- Footer -->
- <com.android.launcher3.notification.NotificationFooterLayout
- android:id="@+id/footer"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_footer_height"
- android:layout_gravity="center_vertical"
- android:clipChildren="false">
-
- <LinearLayout
- android:id="@+id/icon_row"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:gravity="end|center_vertical"
- android:orientation="horizontal"
- android:padding="@dimen/notification_footer_icon_row_padding"/>
-
- <View
- android:id="@+id/overflow"
- android:layout_width="@dimen/horizontal_ellipsis_size"
- android:layout_height="@dimen/horizontal_ellipsis_size"
- android:layout_gravity="start|center_vertical"
- android:layout_marginStart="@dimen/horizontal_ellipsis_offset"
- android:background="@drawable/horizontal_ellipsis" />
-
- </com.android.launcher3.notification.NotificationFooterLayout>
</merge>
\ No newline at end of file
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index dc0c7a0..5aae1c7 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -25,6 +25,9 @@
<color name="popup_color_secondary_dark">@android:color/system_neutral1_900</color>
<color name="popup_color_tertiary_dark">@android:color/system_neutral2_700</color>
+ <color name="popup_notification_dot_light">@android:color/system_accent1_100</color>
+ <color name="popup_notification_dot_dark">@android:color/system_accent2_600</color>
+
<color name="workspace_text_color_light">@android:color/system_neutral1_0</color>
<color name="workspace_text_color_dark">@android:color/system_neutral1_1000</color>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 92deb68..1fadc88 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -36,6 +36,7 @@
<attr name="iconOnlyShortcutColor" format="color" />
<attr name="eduHalfSheetBGColor" format="color" />
<attr name="overviewScrimColor" format="color" />
+ <attr name="popupNotificationDotColor" format="color" />
<attr name="folderDotColor" format="color" />
<attr name="folderFillColor" format="color" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index dac12ed..01f5364 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -53,6 +53,9 @@
<color name="popup_color_secondary_dark">#202124</color>
<color name="popup_color_tertiary_dark">#757575</color> <!-- Gray 600 -->
+ <color name="popup_notification_dot_light">#FFF</color>
+ <color name="popup_notification_dot_dark">#757575</color>
+
<color name="workspace_text_color_light">#FFF</color>
<color name="workspace_text_color_dark">#FF000000</color>
diff --git a/res/values/config.xml b/res/values/config.xml
index 299c80e..04c359e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -76,6 +76,9 @@
<!-- View IDs to store item highlight information -->
<item type="id" name="view_unhighlight_background" />
+ <!-- view ID used to restore work tab state -->
+ <item type="id" name="work_tab_state_id" />
+
<!-- Menu id for feature flags -->
<item type="id" name="menu_apply_flags" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 7fda1e5..f4a0a3d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -259,28 +259,19 @@
<!-- Notifications -->
<dimen name="bg_round_rect_radius">8dp</dimen>
- <dimen name="notification_padding_start">16dp</dimen>
- <dimen name="notification_padding_end">12dp</dimen>
- <!-- notification_padding_end + (icon_size - footer_icon_size) / 2 -->
- <dimen name="notification_footer_icon_row_padding">15dp</dimen>
- <dimen name="notification_header_height">36dp</dimen>
- <dimen name="notification_main_height">84dp</dimen>
- <dimen name="notification_footer_height">32dp</dimen>
- <!-- How much space to keep as padding for the last notification when the footer collapses -->
- <dimen name="notification_empty_footer_height">6dp</dimen>
- <dimen name="notification_header_text_size">13sp</dimen>
+ <dimen name="notification_padding">16dp</dimen>
+ <dimen name="notification_padding_top">18dp</dimen>
+ <dimen name="notification_header_text_size">14sp</dimen>
<dimen name="notification_header_count_text_size">12sp</dimen>
- <dimen name="notification_main_title_size">16sp</dimen>
+ <dimen name="notification_main_title_size">14sp</dimen>
<dimen name="notification_main_text_size">14sp</dimen>
- <dimen name="notification_icon_size">24dp</dimen>
- <dimen name="notification_footer_icon_size">18dp</dimen>
- <!-- notification_icon_size + notification_padding_end + 16dp padding between icon and text -->
- <dimen name="notification_main_text_padding_end">52dp</dimen>
+ <dimen name="notification_circle_icon_size">24dp</dimen>
+ <dimen name="notification_icon_size">32dp</dimen>
+ <!-- Space between edge and icon and icon and text -->
+ <dimen name="notification_icon_padding">12dp</dimen>
+ <!-- notification_icon_padding + notification_icon_size + notification_icon_padding -->
+ <dimen name="notification_main_text_padding_start">56dp</dimen>
<dimen name="horizontal_ellipsis_size">18dp</dimen>
- <!-- arrow_horizontal_offset_start - (ellipsis_size - arrow_width) / 2 -->
- <dimen name="horizontal_ellipsis_offset">19dp</dimen>
- <dimen name="popup_item_divider_height">0.5dp</dimen>
- <dimen name="swipe_helper_falsing_threshold">70dp</dimen>
<!-- Overview -->
<dimen name="options_menu_icon_size">24dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
index 1bd40ce..d941716 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -19,4 +19,13 @@
<item type="id" name="view_type_widgets_list" />
<item type="id" name="view_type_widgets_header" />
<item type="id" name="view_type_widgets_search_header" />
+
+ <!-- Do not change, must be kept in sync with sysui navbar button IDs for tests! -->
+ <item type="id" name="home" />
+ <item type="id" name="recent_apps" />
+ <item type="id" name="back" />
+ <item type="id" name="ime_switcher" />
+ <item type="id" name="accessibility_button" />
+ <item type="id" name="rotate_suggestion" />
+ <!-- /Do not change, must be kept in sync with sysui navbar button IDs for tests! -->
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 8072d0c..a5c796b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -39,6 +39,7 @@
<item name="popupColorPrimary">@color/popup_color_primary_light</item>
<item name="popupColorSecondary">@color/popup_color_secondary_light</item>
<item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
+ <item name="popupNotificationDotColor">@color/popup_notification_dot_light</item>
<item name="isMainColorDark">false</item>
<item name="isWorkspaceDarkText">false</item>
<item name="workspaceTextColor">@color/workspace_text_color_light</item>
@@ -107,6 +108,7 @@
<item name="popupColorPrimary">@color/popup_color_primary_dark</item>
<item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
<item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
+ <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
<item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
<item name="folderDotColor">@color/folder_dot_color</item>
<item name="folderFillColor">@color/folder_background_dark</item>
diff --git a/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
new file mode 100644
index 0000000..c18e26c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+import android.util.Size;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class CachingWidgetPreviewLoaderTest {
+ private static final Size SIZE_10_10 = new Size(10, 10);
+ private static final Size SIZE_20_20 = new Size(20, 20);
+ private static final String TEST_PACKAGE = "com.example.test";
+ private static final ComponentName TEST_PROVIDER =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider");
+ private static final ComponentName TEST_PROVIDER2 =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider2");
+ private static final Bitmap BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+ private static final Bitmap BITMAP2 = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888);
+
+
+ @Mock private CancellationSignal mCancellationSignal;
+ @Mock private WidgetPreviewLoader mDelegate;
+ @Mock private IconCache mIconCache;
+ @Mock private DeviceProfile mDeviceProfile;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo2;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback2;
+ @Captor private ArgumentCaptor<WidgetPreviewLoadedCallback> mCallbackCaptor;
+
+ private TestActivity mTestActivity;
+ private CachingWidgetPreviewLoader mLoader;
+ private WidgetItem mWidgetItem;
+ private WidgetItem mWidgetItem2;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLoader = new CachingWidgetPreviewLoader(mDelegate);
+
+ mTestActivity = Robolectric.buildActivity(TestActivity.class).setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ when(mDelegate.loadPreview(any(), any(), any(), any())).thenReturn(mCancellationSignal);
+
+ mProviderInfo.provider = TEST_PROVIDER;
+ when(mProviderInfo.getProfile()).thenReturn(new UserHandle(0));
+
+ mProviderInfo2.provider = TEST_PROVIDER2;
+ when(mProviderInfo2.getProfile()).thenReturn(new UserHandle(0));
+
+ InvariantDeviceProfile testProfile = new InvariantDeviceProfile();
+ testProfile.numRows = 5;
+ testProfile.numColumns = 5;
+
+ mWidgetItem = new WidgetItem(mProviderInfo, testProfile, mIconCache);
+ mWidgetItem2 = new WidgetItem(mProviderInfo2, testProfile, mIconCache);
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldReturnNull() {
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldNotCallDelegate() {
+ mLoader.getPreview(mWidgetItem, SIZE_10_10);
+
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void getPreview_inCache_shouldReturnCachedBitmap() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void getPreview_otherSizeInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isNull();
+ }
+
+ @Test
+ public void getPreview_otherItemInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem2, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_shouldStoreMultipleSizesPerItem() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP2);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isEqualTo(BITMAP2);
+ }
+
+ @Test
+ public void loadPreview_notInCache_shouldStartLoading() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mDelegate).loadPreview(eq(mTestActivity), eq(mWidgetItem), eq(SIZE_10_10), any());
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_shouldCallBack() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ cancellationSignal.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_shouldNotCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+
+ verifyZeroInteractions(mCancellationSignal);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_loaded_shouldCallBackToNonCancelled() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ cancellationSignal1.cancel();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_bothCallsCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ CancellationSignal cancellationSignal2 =
+ mLoader.loadPreview(
+ mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+ cancellationSignal2.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verifyZeroInteractions(mPreviewLoadedCallback2);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldOnlyCallDelegateOnce() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), any());
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldForwardResultToEachCallback() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_inCache_shouldCallBackImmediately() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ reset(mDelegate);
+
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_thenCancelled_shouldNotRemovePreviewFromCache() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ cancellationSignal.cancel();
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void isPreviewLoaded_notLoaded_shouldReturnFalse() {
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherSizeLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherItemLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_loaded_shouldReturnTrue() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_notInCache_shouldBeNoOp() {
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldOnlyRemoveSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldRemoveAllSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Arrays.asList(mWidgetItem, mWidgetItem2));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ @Test
+ public void clearAll_cacheEmpty_shouldBeNoOp() {
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleItems_shouldRemoveAll() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearAll();
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ private void loadPreviewIntoCache(WidgetItem widgetItem, Size size, Bitmap bitmap) {
+ reset(mDelegate);
+ mLoader.loadPreview(mTestActivity, widgetItem, size, ignored -> {});
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(bitmap);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
index cc36f63..c946c72 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -207,10 +207,9 @@
// GIVEN the current list has app headers [A, B, E content].
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
List.of(mHeaderA, mHeaderB, mContentE));
- // GIVEN the new list has app headers [A, B, E content].
- List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderB, mContentE);
- // GIVEN the user has interacted with B.
- mHeaderB.setIsWidgetListShown(true);
+ // GIVEN the new list has app headers [A, B, E content] and the user has interacted with B.
+ List<WidgetsListBaseEntry> newList =
+ List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE);
// WHEN computing the list difference.
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
index e1214ff..c730fc0 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -33,13 +33,13 @@
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -64,7 +64,7 @@
private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
@Mock private LayoutInflater mMockLayoutInflater;
- @Mock private WidgetPreviewLoader mMockWidgetCache;
+ @Mock private DatabaseWidgetPreviewLoader mMockWidgetCache;
@Mock private RecyclerView.AdapterDataObserver mListener;
@Mock private IconCache mIconCache;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index 4e2a508..81b0c5f 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
@@ -42,6 +41,7 @@
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
@@ -79,7 +79,7 @@
@Mock
private DeviceProfile mDeviceProfile;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private OnHeaderClickListener mOnHeaderClickListener;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
index d6aea55..a0ba7c3 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
@@ -42,6 +41,7 @@
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
@@ -79,7 +79,7 @@
@Mock
private DeviceProfile mDeviceProfile;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private OnHeaderClickListener mOnHeaderClickListener;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 2f1326f..8f9d132 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -38,13 +38,14 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -85,7 +86,7 @@
@Mock
private IconCache mIconCache;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private DeviceProfile mDeviceProfile;
@@ -113,11 +114,10 @@
/* iconClickListener= */ view -> {},
/* iconLongClickListener= */ view -> false);
mViewHolderBinder = new WidgetsListTableViewHolderBinder(
- mContext,
LayoutInflater.from(mTestActivity),
mOnIconClickListener,
mOnLongClickListener,
- mWidgetPreviewLoader,
+ new CachingWidgetPreviewLoader(mWidgetPreviewLoader),
new WidgetsListDrawableFactory(mTestActivity),
widgetsListAdapter);
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index b3d096c..3d6be69 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -48,6 +48,7 @@
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState {
@@ -63,7 +64,7 @@
private final LauncherModel mModel;
private final IconProvider mIconProvider;
private final IconCache mIconCache;
- private final WidgetPreviewLoader mWidgetCache;
+ private final DatabaseWidgetPreviewLoader mWidgetCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
private final RunnableList mOnTerminateCallback = new RunnableList();
@@ -138,7 +139,7 @@
mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context));
mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
iconCacheFileName, mIconProvider);
- mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
+ mWidgetCache = new DatabaseWidgetPreviewLoader(mContext, mIconCache);
mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
mOnTerminateCallback.add(mIconCache::close);
}
@@ -180,7 +181,7 @@
return mModel;
}
- public WidgetPreviewLoader getWidgetCache() {
+ public DatabaseWidgetPreviewLoader getWidgetCache() {
return mWidgetCache;
}
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 8076b27..67f2a9e 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -31,6 +31,7 @@
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.Parcelable;
import android.os.Process;
import android.text.Selection;
@@ -82,6 +83,8 @@
Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener,
ScrimView.ScrimDrawingController {
+ private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
+
public static final float PULL_MULTIPLIER = .02f;
public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
@@ -181,6 +184,23 @@
} catch (Exception e) {
Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
}
+ Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
+ if (state != null) {
+ int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
+ if (currentPage != 0) {
+ rebindAdapters(true);
+ mViewPager.setCurrentPage(currentPage);
+ }
+ }
+ }
+
+
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ Bundle state = new Bundle();
+ state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
+ container.put(R.id.work_tab_state_id, state);
}
/**
@@ -231,7 +251,9 @@
private void resetWorkProfile() {
boolean isEnabled = !mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
- mWorkModeSwitch.updateCurrentState(isEnabled);
+ if (mWorkModeSwitch != null) {
+ mWorkModeSwitch.updateCurrentState(isEnabled);
+ }
mWorkAdapterProvider.updateCurrentState(isEnabled);
mAH[AdapterHolder.WORK].applyPadding();
}
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 6d57aa6..a800d34 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -67,6 +67,7 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ setSelected(true);
setOnClickListener(this);
if (Utilities.ATLEAST_R) {
mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index c6add31..a27d5c8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -26,6 +26,7 @@
import android.annotation.TargetApi;
import android.app.Fragment;
+import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
@@ -83,6 +84,8 @@
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.BaseLauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.NavigableAppWidgetHostView;
import com.android.launcher3.widget.custom.CustomWidgetManager;
@@ -202,6 +205,7 @@
private final InsettableFrameLayout mRootView;
private final Hotseat mHotseat;
private final CellLayout mWorkspace;
+ private final AppWidgetHost mAppWidgetHost;
public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) {
super(context);
@@ -255,6 +259,10 @@
mDp.workspacePadding.top,
mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.bottom);
+
+ mAppWidgetHost = FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()
+ ? new LauncherPreviewAppWidgetHost(context)
+ : null;
}
/** Populate preview and render it. */
@@ -354,14 +362,20 @@
private void inflateAndAddWidgets(
LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
- AppWidgetHostView view = new NavigableAppWidgetHostView(this) {
- @Override
- protected boolean shouldAllowDirectClick() {
- return false;
- }
- };
- view.setAppWidget(-1, providerInfo);
- view.updateAppWidget(null);
+ AppWidgetHostView view;
+ if (FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()) {
+ view = mAppWidgetHost.createView(mContext, info.appWidgetId, providerInfo);
+ } else {
+ view = new NavigableAppWidgetHostView(this) {
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ };
+ view.setAppWidget(-1, providerInfo);
+ view.updateAppWidget(null);
+ }
+
view.setTag(info);
addInScreenFromBind(view, info);
}
@@ -477,4 +491,31 @@
view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
view.layout(0, 0, width, height);
}
+
+ private class LauncherPreviewAppWidgetHost extends AppWidgetHost {
+
+ private LauncherPreviewAppWidgetHost(Context context) {
+ super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID);
+ }
+
+ @Override
+ protected AppWidgetHostView onCreateView(
+ Context context,
+ int appWidgetId,
+ AppWidgetProviderInfo appWidget) {
+ return new LauncherPreviewAppWidgetHostView(LauncherPreviewRenderer.this);
+ }
+ }
+
+ private static class LauncherPreviewAppWidgetHostView extends BaseLauncherAppWidgetHostView {
+
+ private LauncherPreviewAppWidgetHostView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ }
}
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index 97071bb..7198d54 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -1,7 +1,11 @@
package com.android.launcher3.model;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
+import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
@@ -59,4 +63,15 @@
}
return false;
}
+
+ /** Returns whether this {@link WidgetItem} has a preview layout that can be used. */
+ @SuppressLint("NewApi") // Already added API check.
+ public boolean hasPreviewLayout() {
+ return ATLEAST_S && widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL;
+ }
+
+ /** Returns whether this {@link WidgetItem} is for a shortcut rather than an app widget. */
+ public boolean isShortcut() {
+ return activityInfo != null;
+ }
}
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
deleted file mode 100644
index fd3d41a..0000000
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2017 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.notification;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.util.Themes;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * A {@link FrameLayout} that contains only icons of notifications.
- * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow.
- */
-public class NotificationFooterLayout extends FrameLayout {
-
- public interface IconAnimationEndListener {
- void onIconAnimationEnd(NotificationInfo animatedNotification);
- }
-
- private static final int MAX_FOOTER_NOTIFICATIONS = 5;
-
- private static final Rect sTempRect = new Rect();
-
- private final List<NotificationInfo> mNotifications = new ArrayList<>();
- private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
- private final boolean mRtl;
- private final int mBackgroundColor;
-
- FrameLayout.LayoutParams mIconLayoutParams;
- private View mOverflowEllipsis;
- private LinearLayout mIconRow;
- private NotificationItemView mContainer;
-
- public NotificationFooterLayout(Context context) {
- this(context, null, 0);
- }
-
- public NotificationFooterLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- Resources res = getResources();
- mRtl = Utilities.isRtl(res);
-
- int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
- mIconLayoutParams = new LayoutParams(iconSize, iconSize);
- mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
- setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
- mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
- }
-
-
- /**
- * Compute margin start for each icon such that the icons between the first one and the ellipsis
- * are evenly spaced out.
- */
- public void setWidth(int width) {
- if (getLayoutParams() != null) {
- getLayoutParams().width = width;
- }
- Resources res = getResources();
- int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
-
- int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
- int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
- + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
- int availableIconRowSpace = width - paddingEnd - ellipsisSpace
- - iconSize * MAX_FOOTER_NOTIFICATIONS;
- mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mOverflowEllipsis = findViewById(R.id.overflow);
- mIconRow = findViewById(R.id.icon_row);
- }
-
- void setContainer(NotificationItemView container) {
- mContainer = container;
- }
-
- /**
- * Keep track of the NotificationInfo, and then update the UI when
- * {@link #commitNotificationInfos()} is called.
- */
- public void addNotificationInfo(final NotificationInfo notificationInfo) {
- if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
- mNotifications.add(notificationInfo);
- } else {
- mOverflowNotifications.add(notificationInfo);
- }
- }
-
- /**
- * Adds icons and potentially overflow text for all of the NotificationInfo's
- * added using {@link #addNotificationInfo(NotificationInfo)}.
- */
- public void commitNotificationInfos() {
- mIconRow.removeAllViews();
-
- for (int i = 0; i < mNotifications.size(); i++) {
- NotificationInfo info = mNotifications.get(i);
- addNotificationIconForInfo(info);
- }
- updateOverflowEllipsisVisibility();
- }
-
- private void updateOverflowEllipsisVisibility() {
- mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE);
- }
-
- /**
- * Creates an icon for the given NotificationInfo, and adds it to the icon row.
- * @return the icon view that was added
- */
- private View addNotificationIconForInfo(NotificationInfo info) {
- View icon = new View(getContext());
- icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
- icon.setOnClickListener(info);
- icon.setTag(info);
- icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mIconRow.addView(icon, 0, mIconLayoutParams);
- return icon;
- }
-
- public void animateFirstNotificationTo(Rect toBounds,
- final IconAnimationEndListener callback) {
- AnimatorSet animation = new AnimatorSet();
- final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1);
-
- Rect fromBounds = sTempRect;
- firstNotification.getGlobalVisibleRect(fromBounds);
- float scale = (float) toBounds.height() / fromBounds.height();
- Animator moveAndScaleIcon = new PropertyListBuilder().scale(scale)
- .translationY(toBounds.top - fromBounds.top
- + (fromBounds.height() * scale - fromBounds.height()) / 2)
- .build(firstNotification);
- moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag());
- removeViewFromIconRow(firstNotification);
- }
- });
- animation.play(moveAndScaleIcon);
-
- // Shift all notifications (not the overflow) over to fill the gap.
- int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
- if (mRtl) {
- gapWidth = -gapWidth;
- }
- if (!mOverflowNotifications.isEmpty()) {
- NotificationInfo notification = mOverflowNotifications.remove(0);
- mNotifications.add(notification);
- View iconFromOverflow = addNotificationIconForInfo(notification);
- animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1));
- }
- int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving.
- // We have to reset the translation X to 0 when the new main notification
- // is removed from the footer.
- PropertyResetListener<View, Float> propertyResetListener
- = new PropertyResetListener<>(TRANSLATION_X, 0f);
- for (int i = 0; i < numIcons; i++) {
- final View child = mIconRow.getChildAt(i);
- Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth);
- shiftChild.addListener(propertyResetListener);
- animation.play(shiftChild);
- }
- animation.start();
- }
-
- private void removeViewFromIconRow(View child) {
- mIconRow.removeView(child);
- mNotifications.remove(child.getTag());
- updateOverflowEllipsisVisibility();
- if (mIconRow.getChildCount() == 0) {
- // There are no more icons in the footer, so hide it.
- if (mContainer != null) {
- mContainer.removeFooter();
- }
- }
- }
-
- public void trimNotifications(List<String> notifications) {
- if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
- return;
- }
- Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
- while (overflowIterator.hasNext()) {
- if (!notifications.contains(overflowIterator.next().notificationKey)) {
- overflowIterator.remove();
- }
- }
- for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
- View child = mIconRow.getChildAt(i);
- NotificationInfo childInfo = (NotificationInfo) child.getTag();
- if (!notifications.contains(childInfo.notificationKey)) {
- removeViewFromIconRow(child);
- }
- }
- }
-}
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 932e721..af943a6 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -16,12 +16,8 @@
package com.android.launcher3.notification;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
-
import android.animation.AnimatorSet;
-import android.app.Notification;
import android.content.Context;
-import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.MotionEvent;
@@ -32,11 +28,10 @@
import android.widget.TextView;
import com.android.launcher3.R;
-import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.Themes;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -50,39 +45,26 @@
private final PopupContainerWithArrow mPopupContainer;
private final ViewGroup mRootView;
- private final TextView mHeaderText;
private final TextView mHeaderCount;
private final NotificationMainView mMainView;
- private final NotificationFooterLayout mFooter;
- private final SingleAxisSwipeDetector mSwipeDetector;
- private final View mIconView;
private final View mHeader;
private View mGutter;
private boolean mIgnoreTouch = false;
- private boolean mAnimatingNextIcon;
- private int mNotificationHeaderTextColor = Notification.COLOR_DEFAULT;
+ private List<NotificationInfo> mNotificationInfos = new ArrayList<>();
public NotificationItemView(PopupContainerWithArrow container, ViewGroup rootView) {
mPopupContainer = container;
mRootView = rootView;
mContext = container.getContext();
- mHeaderText = container.findViewById(R.id.notification_text);
mHeaderCount = container.findViewById(R.id.notification_count);
mMainView = container.findViewById(R.id.main_view);
- mFooter = container.findViewById(R.id.footer);
- mIconView = container.findViewById(R.id.popup_item_icon);
mHeader = container.findViewById(R.id.header);
- mSwipeDetector = new SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL);
- mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
- mMainView.setSwipeDetector(mSwipeDetector);
- mFooter.setContainer(this);
-
float radius = Themes.getDialogCornerRadius(mContext);
rootView.setClipToOutline(true);
rootView.setOutlineProvider(new ViewOutlineProvider() {
@@ -108,19 +90,6 @@
}
}
- /**
- * Sets width for notification footer and spaces out items evenly
- */
- public void setFooterWidth(int footerWidth) {
- mFooter.setWidth(footerWidth);
- }
-
- public void removeFooter() {
- if (mRootView.indexOfChild(mFooter) >= 0) {
- mRootView.removeView(mFooter);
- }
- }
-
public void inverseGutterMargin() {
MarginLayoutParams lp = (MarginLayoutParams) mGutter.getLayoutParams();
int top = lp.topMargin;
@@ -131,27 +100,28 @@
public void removeAllViews() {
mRootView.removeView(mMainView);
mRootView.removeView(mHeader);
-
- if (mRootView.indexOfChild(mFooter) >= 0) {
- mRootView.removeView(mFooter);
- }
-
if (mGutter != null) {
mRootView.removeView(mGutter);
}
}
- public void updateHeader(int notificationCount, int iconColor) {
- mHeaderCount.setText(notificationCount <= 1 ? "" : String.valueOf(notificationCount));
- if (Color.alpha(iconColor) > 0) {
- if (mNotificationHeaderTextColor == Notification.COLOR_DEFAULT) {
- mNotificationHeaderTextColor =
- IconPalette.resolveContrastColor(mContext, iconColor,
- Themes.getAttrColor(mContext, R.attr.popupColorPrimary));
- }
- mHeaderText.setTextColor(mNotificationHeaderTextColor);
- mHeaderCount.setTextColor(mNotificationHeaderTextColor);
+ /**
+ * Updates the header text.
+ * @param notificationCount The number of notifications.
+ */
+ public void updateHeader(int notificationCount) {
+ final String text;
+ final int visibility;
+ if (notificationCount <= 1) {
+ text = "";
+ visibility = View.INVISIBLE;
+ } else {
+ text = String.valueOf(notificationCount);
+ visibility = View.VISIBLE;
+
}
+ mHeaderCount.setText(text);
+ mHeaderCount.setVisibility(visibility);
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
@@ -171,53 +141,39 @@
return false;
}
- mSwipeDetector.onTouchEvent(ev);
- return mSwipeDetector.isDraggingOrSettling();
- }
-
- public boolean onTouchEvent(MotionEvent ev) {
- if (mIgnoreTouch) {
- return false;
- }
- if (mMainView.getNotificationInfo() == null) {
- // The notification hasn't been populated yet.
- return false;
- }
- return mSwipeDetector.onTouchEvent(ev);
+ return false;
}
public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
+ mNotificationInfos.clear();
if (notificationInfos.isEmpty()) {
return;
}
+ mNotificationInfos.addAll(notificationInfos);
NotificationInfo mainNotification = notificationInfos.get(0);
mMainView.applyNotificationInfo(mainNotification, false);
-
- for (int i = 1; i < notificationInfos.size(); i++) {
- mFooter.addNotificationInfo(notificationInfos.get(i));
- }
- mFooter.commitNotificationInfos();
}
public void trimNotifications(final List<String> notificationKeys) {
- boolean dismissedMainNotification = !notificationKeys.contains(
- mMainView.getNotificationInfo().notificationKey);
- if (dismissedMainNotification && !mAnimatingNextIcon) {
- // Animate the next icon into place as the new main notification.
- mAnimatingNextIcon = true;
- mMainView.setContentVisibility(View.INVISIBLE);
- mMainView.setContentTranslation(0);
- mIconView.getGlobalVisibleRect(sTempRect);
- mFooter.animateFirstNotificationTo(sTempRect, (newMainNotification) -> {
- if (newMainNotification != null) {
- mMainView.applyNotificationInfo(newMainNotification, true);
- mMainView.setContentVisibility(View.VISIBLE);
+ NotificationInfo currentMainNotificationInfo = mMainView.getNotificationInfo();
+ boolean shouldUpdateMainNotification = !notificationKeys.contains(
+ currentMainNotificationInfo.notificationKey);
+
+ if (shouldUpdateMainNotification) {
+ int size = notificationKeys.size();
+ NotificationInfo nextNotification = null;
+ // We get the latest notification by finding the notification after the one that was
+ // just dismissed.
+ for (int i = 0; i < size; ++i) {
+ if (currentMainNotificationInfo == mNotificationInfos.get(i) && i + 1 < size) {
+ nextNotification = mNotificationInfos.get(i + 1);
+ break;
}
- mAnimatingNextIcon = false;
- });
- } else {
- mFooter.trimNotifications(notificationKeys);
+ }
+ if (nextNotification != null) {
+ mMainView.applyNotificationInfo(nextNotification, true);
+ }
}
}
}
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index e9b5f32..b8aa824 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -16,19 +16,15 @@
package com.android.launcher3.notification;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
-import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
-import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -40,19 +36,15 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.util.Themes;
/**
* A {@link android.widget.FrameLayout} that contains a single notification,
* e.g. icon + title + text.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
+public class NotificationMainView extends FrameLayout {
private static final FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
new FloatProperty<NotificationMainView>("contentTranslation") {
@@ -70,8 +62,6 @@
// This is used only to track the notification view, so that it can be properly logged.
public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
- private final ObjectAnimator mContentTranslateAnimator;
-
private NotificationInfo mNotificationInfo;
private ViewGroup mTextAndBackground;
private int mBackgroundColor;
@@ -82,7 +72,6 @@
private SingleAxisSwipeDetector mSwipeDetector;
private final ColorDrawable mColorDrawable;
- private final RippleDrawable mRippleDrawable;
public NotificationMainView(Context context) {
this(context, null, 0);
@@ -95,11 +84,7 @@
public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mContentTranslateAnimator = ObjectAnimator.ofFloat(this, CONTENT_TRANSLATION, 0);
mColorDrawable = new ColorDrawable(Color.TRANSPARENT);
- mRippleDrawable = new RippleDrawable(ColorStateList.valueOf(
- Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
- mColorDrawable, null);
}
@Override
@@ -118,7 +103,7 @@
private void updateBackgroundColor(int color) {
mBackgroundColor = color;
mColorDrawable.setColor(color);
- mTextAndBackground.setBackground(mRippleDrawable);
+ mTextAndBackground.setBackground(mColorDrawable);
if (mNotificationInfo != null) {
mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
mBackgroundColor));
@@ -140,10 +125,6 @@
animatorSetOut.play(colors);
}
- public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
- mSwipeDetector = swipeDetector;
- }
-
/**
* Sets the content of this view, animating it after a new icon shifts up if necessary.
*/
@@ -182,11 +163,6 @@
mIconView.setTranslationX(translation);
}
- public void setContentVisibility(int visibility) {
- mTextAndBackground.setVisibility(visibility);
- mIconView.setVisibility(visibility);
- }
-
public NotificationInfo getNotificationInfo() {
return mNotificationInfo;
}
@@ -202,56 +178,4 @@
mNotificationInfo.notificationKey);
launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
}
-
- // SingleAxisSwipeDetector.Listener's
- @Override
- public void onDragStart(boolean start, float startDisplacement) { }
-
-
- @Override
- public boolean onDrag(float displacement) {
- setContentTranslation(canChildBeDismissed()
- ? displacement : OverScroll.dampedScroll(displacement, getWidth()));
- mContentTranslateAnimator.cancel();
- return true;
- }
-
- @Override
- public void onDragEnd(float velocity) {
- final boolean willExit;
- final float endTranslation;
- final float startTranslation = mTextAndBackground.getTranslationX();
-
- if (!canChildBeDismissed()) {
- willExit = false;
- endTranslation = 0;
- } else if (mSwipeDetector.isFling(velocity)) {
- willExit = true;
- endTranslation = velocity < 0 ? - getWidth() : getWidth();
- } else if (Math.abs(startTranslation) > getWidth() / 2) {
- willExit = true;
- endTranslation = (startTranslation < 0 ? -getWidth() : getWidth());
- } else {
- willExit = false;
- endTranslation = 0;
- }
-
- long duration = BaseSwipeDetector.calculateDuration(velocity,
- (endTranslation - startTranslation) / getWidth());
-
- mContentTranslateAnimator.removeAllListeners();
- mContentTranslateAnimator.setDuration(duration)
- .setInterpolator(scrollInterpolatorForVelocity(velocity));
- mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);
- mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- mSwipeDetector.finishedScrolling();
- if (willExit) {
- onChildDismissed();
- }
- }
- });
- mContentTranslateAnimator.start();
- }
}
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 2095a0d..cb35f74 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -252,9 +252,9 @@
view.setBackgroundResource(R.drawable.single_item_primary);
} else if (totalVisibleShortcuts > 1) {
if (numVisibleShortcut == 0) {
- view.setBackground(mRoundedTop);
+ view.setBackground(mRoundedTop.getConstantState().newDrawable());
} else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) {
- view.setBackground(mRoundedBottom);
+ view.setBackground(mRoundedBottom.getConstantState().newDrawable());
} else {
view.setBackgroundResource(R.drawable.middle_item_primary);
}
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 2ae12ac..18f263a 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -138,14 +138,6 @@
}
@Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (mNotificationItemView != null) {
- return mNotificationItemView.onTouchEvent(ev) || super.onTouchEvent(ev);
- }
- return super.onTouchEvent(ev);
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_ACTION_POPUP) != 0;
}
@@ -272,12 +264,6 @@
}
View.inflate(getContext(), R.layout.notification_content, mNotificationContainer);
mNotificationItemView = new NotificationItemView(this, mNotificationContainer);
- if (mNumNotifications == 1) {
- mNotificationItemView.removeFooter();
- }
- else {
- mNotificationItemView.setFooterWidth(containerWidth);
- }
updateNotificationHeader();
}
int viewsToFlip = getChildCount();
@@ -462,8 +448,7 @@
ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
if (mNotificationItemView != null && dotInfo != null) {
- mNotificationItemView.updateHeader(
- dotInfo.getNotificationCount(), itemInfo.bitmap.color);
+ mNotificationItemView.updateHeader(dotInfo.getNotificationCount());
}
}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index cc658c9..71d288c 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -18,7 +18,13 @@
import android.content.Context;
import android.content.pm.ShortcutInfo;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
import android.graphics.Point;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -30,16 +36,20 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BubbleTextHolder;
/**
- * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
- * This lets us animate the DeepShortcutView (icon and text) separately from the background.
+ * A {@link android.widget.FrameLayout} that contains an icon and a {@link BubbleTextView} for text.
+ * This lets us animate the child BubbleTextView's background (transparent ripple) separately from
+ * the {@link DeepShortcutView} background color.
*/
public class DeepShortcutView extends FrameLayout implements BubbleTextHolder {
private static final Point sTempPoint = new Point();
+ private final Drawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+
private BubbleTextView mBubbleText;
private View mIconView;
@@ -63,6 +73,43 @@
super.onFinishInflate();
mBubbleText = findViewById(R.id.bubble_text);
mIconView = findViewById(R.id.icon);
+ tryUpdateTextBackground();
+ }
+
+ @Override
+ public void setBackground(Drawable background) {
+ super.setBackground(background);
+ tryUpdateTextBackground();
+ }
+
+ @Override
+ public void setBackgroundResource(int resid) {
+ super.setBackgroundResource(resid);
+ tryUpdateTextBackground();
+ }
+
+ /**
+ * Updates the text background to match the shape of this background (when applicable).
+ */
+ private void tryUpdateTextBackground() {
+ if (!(getBackground() instanceof GradientDrawable) || mBubbleText == null) {
+ return;
+ }
+ GradientDrawable background = (GradientDrawable) getBackground();
+
+ int color = Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight);
+ GradientDrawable backgroundMask = new GradientDrawable();
+ backgroundMask.setColor(color);
+ backgroundMask.setShape(GradientDrawable.RECTANGLE);
+ if (background.getCornerRadii() != null) {
+ backgroundMask.setCornerRadii(background.getCornerRadii());
+ } else {
+ backgroundMask.setCornerRadius(background.getCornerRadius());
+ }
+
+ RippleDrawable drawable = new RippleDrawable(ColorStateList.valueOf(color),
+ mTransparentDrawable, backgroundMask);
+ mBubbleText.setBackground(drawable);
}
@Override
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 4261d08..5cd3682 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -117,6 +117,10 @@
TestProtocol.sDisableSensorRotation = true;
return response;
+ case TestProtocol.REQUEST_IS_TABLET:
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, mDeviceProfile.isTablet);
+ return response;
+
default:
return null;
}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index b6da7fc..2f1f82d 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -94,6 +94,7 @@
public static final String REQUEST_GET_TEST_EVENTS = "get-test-events";
public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging";
public static final String REQUEST_CLEAR_DATA = "clear-data";
+ public static final String REQUEST_IS_TABLET = "is-tablet";
public static boolean sDebugTracing = false;
public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index 8e3ac20..8f814a1 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -108,8 +108,6 @@
switch (direction) {
case DIRECTION_TOP:
return new EdgeEffectProxy(getContext(), mEdgeGlowTop);
- case DIRECTION_BOTTOM:
- return new EdgeEffectProxy(getContext(), mEdgeGlowBottom);
}
return super.createEdgeEffect(view, direction);
}
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
new file mode 100644
index 0000000..2742882
--- /dev/null
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Launcher AppWidgetHostView with support for rounded corners and a fallback View.
+ */
+public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView {
+
+ protected final LayoutInflater mInflater;
+
+ private final Rect mEnforcedRectangle = new Rect();
+ private final float mEnforcedCornerRadius;
+ private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
+ outline.setEmpty();
+ } else {
+ outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
+ }
+ }
+ };
+
+ public BaseLauncherAppWidgetHostView(Context context) {
+ super(context);
+
+ setExecutor(Executors.THREAD_POOL_EXECUTOR);
+
+ mInflater = LayoutInflater.from(context);
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ }
+
+ @Override
+ protected View getErrorView() {
+ return mInflater.inflate(R.layout.appwidget_error, this, false);
+ }
+
+ /**
+ * Fall back to error layout instead of showing widget.
+ */
+ public void switchToErrorView() {
+ // Update the widget with 0 Layout id, to reset the view to error view.
+ updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ try {
+ super.onLayout(changed, left, top, right, bottom);
+ } catch (final RuntimeException e) {
+ post(this::switchToErrorView);
+ }
+
+ enforceRoundedCorners();
+ }
+
+ @UiThread
+ private void resetRoundedCorners() {
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ setClipToOutline(false);
+ }
+
+ @UiThread
+ private void enforceRoundedCorners() {
+ if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+ resetRoundedCorners();
+ return;
+ }
+ View background = RoundedCornerEnforcement.findBackground(this);
+ if (background == null
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ resetRoundedCorners();
+ return;
+ }
+ RoundedCornerEnforcement.computeRoundedRectangle(this,
+ background,
+ mEnforcedRectangle);
+ setOutlineProvider(mCornerRadiusEnforcementOutline);
+ setClipToOutline(true);
+ }
+
+ /** Returns the corner radius currently enforced, in pixels. */
+ public float getEnforcedCornerRadius() {
+ return mEnforcedCornerRadius;
+ }
+
+ /** Returns true if the corner radius are enforced for this App Widget. */
+ public boolean hasEnforcedCornerRadius() {
+ return getClipToOutline();
+ }
+}
diff --git a/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
new file mode 100644
index 0000000..afceadd
--- /dev/null
+++ b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Wrapper around {@link DatabaseWidgetPreviewLoader} that contains caching logic. */
+public class CachingWidgetPreviewLoader implements WidgetPreviewLoader {
+
+ @NonNull private final WidgetPreviewLoader mDelegate;
+ @NonNull private final Map<ComponentKey, Map<Size, CacheResult>> mCache = new ArrayMap<>();
+
+ public CachingWidgetPreviewLoader(@NonNull WidgetPreviewLoader delegate) {
+ mDelegate = delegate;
+ }
+
+ /** Returns whether the preview is loaded for the item and size. */
+ public boolean isPreviewLoaded(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ return getPreview(item, previewSize) != null;
+ }
+
+ /** Returns the cached preview for the item and size, or null if there is none. */
+ @Nullable
+ public Bitmap getPreview(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ return ((CacheResult.Loaded) cacheResult).mBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ private CacheResult getCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.get(toComponentKey(item));
+ if (cacheResults == null) {
+ return CacheResult.MISS;
+ }
+
+ return cacheResults.getOrDefault(previewSize, CacheResult.MISS);
+ }
+ }
+
+ /**
+ * Puts the result in the cache for the item and size. Returns the value previously in the
+ * cache, or null if there was none.
+ */
+ @Nullable
+ private CacheResult putCacheResult(
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @Nullable CacheResult cacheResult) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ CacheResult previous;
+ if (cacheResult == null) {
+ previous = cacheResults.remove(previewSize);
+ if (cacheResults.isEmpty()) {
+ mCache.remove(key);
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ return previous;
+ }
+ }
+
+ private void removeCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ cacheResults.remove(previewSize);
+ mCache.put(key, cacheResults);
+ }
+ }
+
+ /**
+ * Gets the preview for the widget item and size, using the value in the cache if stored.
+ *
+ * @return a {@link CancellationSignal}, which can cancel the request before it loads
+ */
+ @Override
+ @UiThread
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity, @NonNull WidgetItem item, @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ CancellationSignal signal = new CancellationSignal();
+ signal.setOnCancelListener(() -> {
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (!(cacheResult instanceof CacheResult.Loading)) {
+ // If the key isn't actively loading, then this is a no-op. Cancelling loading
+ // shouldn't clear the cache if we've already loaded.
+ return;
+ }
+
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ CacheResult.Loading updated = prev.withoutCallback(callback);
+
+ if (updated.mCallbacks.isEmpty()) {
+ // If the last callback was removed, then cancel the underlying request in the
+ // delegate.
+ prev.mCancellationSignal.cancel();
+ removeCacheResult(item, previewSize);
+ } else {
+ // If there are other callbacks still active, then don't cancel the delegate's
+ // request, just remove this callback from the set.
+ putCacheResult(item, previewSize, updated);
+ }
+ }
+ });
+
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ // If the bitmap is already present in the cache, invoke the callback immediately.
+ callback.onPreviewLoaded(((CacheResult.Loaded) cacheResult).mBitmap);
+ return signal;
+ }
+
+ if (cacheResult instanceof CacheResult.Loading) {
+ // If we're already loading the preview for this key, then just add the callback
+ // to the set we'll call after it loads.
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ putCacheResult(item, previewSize, prev.withCallback(callback));
+ return signal;
+ }
+
+ CancellationSignal delegateCancellationSignal =
+ mDelegate.loadPreview(
+ activity,
+ item,
+ previewSize,
+ preview -> {
+ CacheResult prev;
+ synchronized (mCache) {
+ prev = putCacheResult(
+ item, previewSize, new CacheResult.Loaded(preview));
+ }
+ if (prev instanceof CacheResult.Loading) {
+ // Notify each stored callback that the preview has loaded.
+ ((CacheResult.Loading) prev).mCallbacks
+ .forEach(c -> c.onPreviewLoaded(preview));
+ } else {
+ // If there isn't a loading object in the cache, then we were
+ // notified before adding this signal to the cache. Just
+ // call back to the provided callback, there can't be others.
+ callback.onPreviewLoaded(preview);
+ }
+ });
+ ArraySet<WidgetPreviewLoadedCallback> callbacks = new ArraySet<>();
+ callbacks.add(callback);
+ putCacheResult(
+ item,
+ previewSize,
+ new CacheResult.Loading(delegateCancellationSignal, callbacks));
+ }
+
+ return signal;
+ }
+
+ /** Clears all cached previews for {@code items}, cancelling any in-progress preview loading. */
+ public void clearPreviews(Iterable<WidgetItem> items) {
+ List<CacheResult> previousCacheResults = new ArrayList<>();
+ synchronized (mCache) {
+ for (WidgetItem item : items) {
+ Map<Size, CacheResult> previousMap = mCache.remove(toComponentKey(item));
+ if (previousMap != null) {
+ previousCacheResults.addAll(previousMap.values());
+ }
+ }
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ /** Clears all cached previews, cancelling any in-progress preview loading. */
+ public void clearAll() {
+ List<CacheResult> previousCacheResults;
+ synchronized (mCache) {
+ previousCacheResults =
+ mCache
+ .values()
+ .stream()
+ .flatMap(sizeToResult -> sizeToResult.values().stream())
+ .collect(Collectors.toList());
+ mCache.clear();
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ private abstract static class CacheResult {
+ static final CacheResult MISS = new CacheResult() {};
+
+ static final class Loading extends CacheResult {
+ @NonNull final CancellationSignal mCancellationSignal;
+ @NonNull final Set<WidgetPreviewLoadedCallback> mCallbacks;
+
+ Loading(@NonNull CancellationSignal cancellationSignal,
+ @NonNull Set<WidgetPreviewLoadedCallback> callbacks) {
+ mCancellationSignal = cancellationSignal;
+ mCallbacks = callbacks;
+ }
+
+ @NonNull
+ Loading withCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() + 1);
+ newCallbacks.addAll(mCallbacks);
+ newCallbacks.add(callback);
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+
+ @NonNull
+ Loading withoutCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (!mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() - 1);
+ for (WidgetPreviewLoadedCallback existingCallback : mCallbacks) {
+ if (!existingCallback.equals(callback)) {
+ newCallbacks.add(existingCallback);
+ }
+ }
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+ }
+
+ static final class Loaded extends CacheResult {
+ @NonNull final Bitmap mBitmap;
+
+ Loaded(@NonNull Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+ }
+ }
+
+ @NonNull
+ private static ComponentKey toComponentKey(@NonNull WidgetItem item) {
+ return new ComponentKey(item.componentName, item.user);
+ }
+}
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
similarity index 86%
rename from src/com/android/launcher3/WidgetPreviewLoader.java
rename to src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index ff3584a..6de3e11 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -1,4 +1,19 @@
-package com.android.launcher3;
+/*
+ * Copyright (C) 2021 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.widget;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -32,8 +47,14 @@
import android.util.Pair;
import android.util.Size;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
@@ -47,9 +68,6 @@
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SQLiteCacheHelper;
import com.android.launcher3.util.Thunk;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.WidgetCell;
-import com.android.launcher3.widget.WidgetManagerHelper;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
@@ -60,7 +78,8 @@
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
-public class WidgetPreviewLoader {
+/** {@link WidgetPreviewLoader} that loads preview images from a {@link CacheDb}. */
+public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
private static final String TAG = "WidgetPreviewLoader";
private static final boolean DEBUG = false;
@@ -80,7 +99,7 @@
private final UserCache mUserCache;
private final CacheDb mDb;
- public WidgetPreviewLoader(Context context, IconCache iconCache) {
+ public DatabaseWidgetPreviewLoader(Context context, IconCache iconCache) {
mContext = context;
mIconCache = iconCache;
mUserCache = UserCache.INSTANCE.get(context);
@@ -89,16 +108,24 @@
/**
* Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
- * called on UI thread
+ * called on UI thread.
*
* @return a request id which can be used to cancel the request.
*/
- public CancellationSignal getPreview(WidgetItem item, int previewWidth,
- int previewHeight, WidgetCell caller) {
+ @Override
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ int previewWidth = previewSize.getWidth();
+ int previewHeight = previewSize.getHeight();
String size = previewWidth + "x" + previewHeight;
WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
- PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
+ PreviewLoadTask task =
+ new PreviewLoadTask(activity, key, item, previewWidth, previewHeight, callback);
task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
CancellationSignal signal = new CancellationSignal();
@@ -106,6 +133,7 @@
return signal;
}
+ /** Clears the database storing previews. */
public void refresh() {
mDb.clear();
}
@@ -126,21 +154,37 @@
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
- public CacheDb(Context context) {
+ CacheDb(Context context) {
super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
}
@Override
public void onCreateTable(SQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
- COLUMN_COMPONENT + " TEXT NOT NULL, " +
- COLUMN_USER + " INTEGER NOT NULL, " +
- COLUMN_SIZE + " TEXT NOT NULL, " +
- COLUMN_PACKAGE + " TEXT NOT NULL, " +
- COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_PREVIEW_BITMAP + " BLOB, " +
- "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
+ database.execSQL("CREATE TABLE IF NOT EXISTS "
+ + TABLE_NAME
+ + " ("
+ + COLUMN_COMPONENT
+ + " TEXT NOT NULL, "
+ + COLUMN_USER
+ + " INTEGER NOT NULL, "
+ + COLUMN_SIZE
+ + " TEXT NOT NULL, "
+ + COLUMN_PACKAGE
+ + " TEXT NOT NULL, "
+ + COLUMN_LAST_UPDATED
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_VERSION
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_PREVIEW_BITMAP
+ + " BLOB, "
+ + "PRIMARY KEY ("
+ + COLUMN_COMPONENT
+ + ", "
+ + COLUMN_USER
+ + ", "
+ + COLUMN_SIZE
+ + ") "
+ +
");");
}
}
@@ -149,7 +193,7 @@
ContentValues values = new ContentValues();
values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
- values.put(CacheDb.COLUMN_SIZE, key.size);
+ values.put(CacheDb.COLUMN_SIZE, key.mSize);
values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
values.put(CacheDb.COLUMN_VERSION, versions[0]);
values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
@@ -157,12 +201,14 @@
mDb.insertOrReplace(values);
}
+ /** Removes the package from the preview database. */
public void removePackage(String packageName, UserHandle user) {
removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
}
- private void removePackage(String packageName, UserHandle user, long userSerial) {
- synchronized(mPackageVersions) {
+ /** Removes the package from the preview database. */
+ public void removePackage(String packageName, UserHandle user, long userSerial) {
+ synchronized (mPackageVersions) {
mPackageVersions.remove(packageName);
}
@@ -264,7 +310,7 @@
new String[]{
key.componentName.flattenToShortString(),
Long.toString(mUserCache.getSerialNumberForUser(key.user)),
- key.size
+ key.mSize
});
// If cancelled, skip getting the blob and decoding it into a bitmap
if (loadTask.isCancelled()) {
@@ -293,7 +339,7 @@
}
/**
- * Returns generatedPreview for a widget and if the preview should be saved in persistent
+ * Returns a generated preview for a widget and if the preview should be saved in persistent
* storage.
* @param launcher
* @param item
@@ -344,8 +390,10 @@
if (drawable != null) {
drawable = mutateOnMainThread(drawable);
} else {
- Log.w(TAG, "Can't load widget preview drawable 0x" +
- Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
+ Log.w(TAG, "Can't load widget preview drawable 0x"
+ + Integer.toHexString(info.previewImage)
+ + " for provider: "
+ + info.provider);
}
}
@@ -379,8 +427,8 @@
scale = maxPreviewWidth / (float) (previewWidth);
}
if (scale != 1f) {
- previewWidth = Math.max((int)(scale * previewWidth), 1);
- previewHeight = Math.max((int)(scale * previewHeight), 1);
+ previewWidth = Math.max((int) (scale * previewWidth), 1);
+ previewHeight = Math.max((int) (scale * previewHeight), 1);
}
final Canvas c = new Canvas();
@@ -554,13 +602,13 @@
}
}
- public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
+ private class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
implements CancellationSignal.OnCancelListener {
@Thunk final WidgetCacheKey mKey;
private final WidgetItem mInfo;
private final int mPreviewHeight;
private final int mPreviewWidth;
- private final WidgetCell mCaller;
+ private final WidgetPreviewLoadedCallback mCallback;
private final BaseActivity mActivity;
@Thunk long[] mVersions;
@Thunk Bitmap mBitmapToRecycle;
@@ -568,14 +616,14 @@
@Nullable private Bitmap mUnusedPreviewBitmap;
private boolean mSaveToDB = false;
- PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
- int previewHeight, WidgetCell caller) {
+ PreviewLoadTask(BaseActivity activity, WidgetCacheKey key, WidgetItem info,
+ int previewWidth, int previewHeight, WidgetPreviewLoadedCallback callback) {
+ mActivity = activity;
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
mPreviewWidth = previewWidth;
- mCaller = caller;
- mActivity = BaseActivity.fromContext(mCaller.getContext());
+ mCallback = callback;
if (DEBUG) {
Log.d(TAG, String.format("%s, %s, %d, %d",
mKey, mInfo, mPreviewHeight, mPreviewWidth));
@@ -593,9 +641,9 @@
synchronized (mUnusedBitmaps) {
// Check if we can re-use a bitmap
for (Bitmap candidate : mUnusedBitmaps) {
- if (candidate != null && candidate.isMutable() &&
- candidate.getWidth() == mPreviewWidth &&
- candidate.getHeight() == mPreviewHeight) {
+ if (candidate != null && candidate.isMutable()
+ && candidate.getWidth() == mPreviewWidth
+ && candidate.getHeight() == mPreviewHeight) {
unusedBitmap = candidate;
mUnusedBitmaps.remove(unusedBitmap);
break;
@@ -638,7 +686,7 @@
@Override
protected void onPostExecute(final Bitmap preview) {
- mCaller.applyPreview(preview);
+ mCallback.onPreviewLoaded(preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
@@ -716,21 +764,21 @@
private static final class WidgetCacheKey extends ComponentKey {
- @Thunk final String size;
+ @Thunk final String mSize;
- public WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
+ WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
super(componentName, user);
- this.size = size;
+ this.mSize = size;
}
@Override
public int hashCode() {
- return super.hashCode() ^ size.hashCode();
+ return super.hashCode() ^ mSize.hashCode();
}
@Override
public boolean equals(Object o) {
- return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
+ return super.equals(o) && ((WidgetCacheKey) o).mSize.equals(mSize);
}
}
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 70ed02f..fa50dfb 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -20,19 +20,16 @@
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
-import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Advanceable;
@@ -40,7 +37,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
@@ -51,7 +47,6 @@
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
@@ -61,7 +56,7 @@
/**
* {@inheritDoc}
*/
-public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
+public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
implements TouchCompleteListener, View.OnLongClickListener,
LocalColorExtractor.Listener {
@@ -76,8 +71,6 @@
// Maximum duration for which updates can be deferred.
private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
- protected final LayoutInflater mInflater;
-
private final CheckLongPressHelper mLongPressHelper;
protected final Launcher mLauncher;
private final Workspace mWorkspace;
@@ -101,18 +94,6 @@
private final Rect mWidgetSizeAtDrag = new Rect();
private final RectF mTempRectF = new RectF();
- private final Rect mEnforcedRectangle = new Rect();
- private final float mEnforcedCornerRadius;
- private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
- outline.setEmpty();
- } else {
- outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
- }
- }
- };
private final Object mUpdateLock = new Object();
private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
private long mDeferUpdatesUntilMillis = 0;
@@ -123,18 +104,15 @@
mLauncher = Launcher.getLauncher(context);
mWorkspace = mLauncher.getWorkspace();
mLongPressHelper = new CheckLongPressHelper(this, this);
- mInflater = LayoutInflater.from(context);
setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
setBackgroundResource(R.drawable.widget_internal_focus_bg);
- setExecutor(Executors.THREAD_POOL_EXECUTOR);
if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
setOnLightBackground(true);
}
mColorExtractor = LocalColorExtractor.newInstance(getContext());
mColorExtractor.setListener(this);
- mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
}
@@ -166,11 +144,6 @@
}
@Override
- protected View getErrorView() {
- return mInflater.inflate(R.layout.appwidget_error, this, false);
- }
-
- @Override
public void updateAppWidget(RemoteViews remoteViews) {
synchronized (mUpdateLock) {
mMostRecentRemoteViews = remoteViews;
@@ -304,34 +277,17 @@
}
}
- public void switchToErrorView() {
- // Update the widget with 0 Layout id, to reset the view to error view.
- updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
- }
-
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- try {
- super.onLayout(changed, left, top, right, bottom);
- } catch (final RuntimeException e) {
- post(new Runnable() {
- @Override
- public void run() {
- switchToErrorView();
- }
- });
- }
+ super.onLayout(changed, left, top, right, bottom);
mIsScrollable = checkScrollableRecursively(this);
if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
-
LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize);
updateColorExtraction(mCurrentWidgetSize,
mWorkspace.getPageIndexForScreenId(info.screenId));
}
-
- enforceRoundedCorners();
}
/** Starts the drag mode. */
@@ -502,40 +458,4 @@
}
return false;
}
-
- @UiThread
- private void resetRoundedCorners() {
- setOutlineProvider(ViewOutlineProvider.BACKGROUND);
- setClipToOutline(false);
- }
-
- @UiThread
- private void enforceRoundedCorners() {
- if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
- resetRoundedCorners();
- return;
- }
- View background = RoundedCornerEnforcement.findBackground(this);
- if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
- resetRoundedCorners();
- return;
- }
- RoundedCornerEnforcement.computeRoundedRectangle(this,
- background,
- mEnforcedRectangle);
- setOutlineProvider(mCornerRadiusEnforcementOutline);
- setClipToOutline(true);
- }
-
- /** Returns the corner radius currently enforced, in pixels. */
- public float getEnforcedCornerRadius() {
- return mEnforcedCornerRadius;
- }
-
- /** Returns true if the corner radius are enforced for this App Widget. */
- public boolean hasEnforcedCornerRadius() {
- return getClipToOutline();
- }
-
}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index b1ccfd9..91529be 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -19,7 +19,6 @@
import static com.android.launcher3.Utilities.ATLEAST_S;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -44,7 +43,6 @@
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.model.WidgetItem;
@@ -222,21 +220,18 @@
return;
}
- if (ATLEAST_S
- && mRemoteViewsPreview == null
- && item.widgetInfo != null
- && item.widgetInfo.previewLayout != Resources.ID_NULL) {
- mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext());
- LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
- LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
- item.widgetInfo.clone());
- // A hack to force the initial layout to be the preview layout since there is no API for
- // rendering a preview layout for work profile apps yet. For non-work profile layout, a
- // proper solution is to use RemoteViews(PackageName, LayoutId).
- launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
- setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
- launcherAppWidgetProviderInfo, /* remoteViews= */ null);
- }
+ if (!item.hasPreviewLayout()) return;
+
+ mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext());
+ LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
+ item.widgetInfo.clone());
+ // A hack to force the initial layout to be the preview layout since there is no API for
+ // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+ // proper solution is to use RemoteViews(PackageName, LayoutId).
+ launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
+ setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
+ launcherAppWidgetProviderInfo, /* remoteViews= */ null);
}
private void setAppWidgetHostViewPreview(
@@ -344,22 +339,25 @@
if (mActiveRequest != null) {
return;
}
- mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight,
- this);
+ mActiveRequest = mWidgetPreviewLoader.loadPreview(
+ BaseActivity.fromContext(getContext()), mItem,
+ new Size(mPreviewWidth, mPreviewHeight),
+ this::applyPreview);
}
/** Sets the widget preview image size in number of cells. */
- public void setPreviewSize(int spanX, int spanY) {
- setPreviewSize(spanX, spanY, 1f);
+ public Size setPreviewSize(int spanX, int spanY) {
+ return setPreviewSize(spanX, spanY, 1f);
}
/** Sets the widget preview image size, in number of cells, and preview scale. */
- public void setPreviewSize(int spanX, int spanY, float previewScale) {
+ public Size setPreviewSize(int spanX, int spanY, float previewScale) {
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, spanX, spanY);
mPreviewWidth = widgetSize.getWidth();
mPreviewHeight = widgetSize.getHeight();
mPreviewScale = previewScale;
+ return widgetSize;
}
@Override
diff --git a/src/com/android/launcher3/widget/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
new file mode 100644
index 0000000..ff5c82f
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+
+/** Asynchronous loader of preview bitmaps for {@link WidgetItem}s. */
+public interface WidgetPreviewLoader {
+ /**
+ * Loads a widget preview and calls back to {@code callback} when complete.
+ *
+ * @return a {@link CancellationSignal} which can be used to cancel the request.
+ */
+ @NonNull
+ @UiThread
+ CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback);
+
+ /** Callback class for requests to {@link WidgetPreviewLoader}. */
+ interface WidgetPreviewLoadedCallback {
+ void onPreviewLoaded(@NonNull Bitmap preview);
+ }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
index 73bae6f..abc79ff 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -59,6 +59,19 @@
@Rank
public abstract int getRank();
+ /**
+ * Marker interface for subclasses that are headers for widget list items.
+ *
+ * @param <T> The type of this class.
+ */
+ public interface Header<T extends WidgetsListBaseEntry & Header<T>> {
+ /** Returns whether the widget list is currently expanded. */
+ boolean isWidgetListShown();
+
+ /** Returns a copy of the item with the widget list shown. */
+ T withWidgetListShown();
+ }
+
@Retention(SOURCE)
@IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT})
public @interface Rank {
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
index 1fdc399..5b3ea94 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -21,41 +21,33 @@
import java.util.List;
/** An information holder for an app which has widgets or/and shortcuts. */
-public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
+public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListHeaderEntry> {
public final int widgetsCount;
public final int shortcutsCount;
- private boolean mIsWidgetListShown = false;
- private boolean mHasEntryUpdated = false;
+ private final boolean mIsWidgetListShown;
public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items) {
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
+ }
+
+ private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
super(pkgItem, titleSectionName, items);
widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
shortcutsCount = Math.max(0, items.size() - widgetsCount);
- }
-
- /** Sets if the widgets list associated with this header is shown. */
- public void setIsWidgetListShown(boolean isWidgetListShown) {
- if (mIsWidgetListShown != isWidgetListShown) {
- this.mIsWidgetListShown = isWidgetListShown;
- mHasEntryUpdated = true;
- } else {
- mHasEntryUpdated = false;
- }
+ mIsWidgetListShown = isWidgetListShown;
}
/** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
public boolean isWidgetListShown() {
return mIsWidgetListShown;
}
- /** Returns {@code true} if this entry has been updated due to user interactions. */
- public boolean hasEntryUpdated() {
- return mHasEntryUpdated;
- }
-
@Override
public String toString() {
return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
@@ -72,6 +64,18 @@
if (!(obj instanceof WidgetsListHeaderEntry)) return false;
WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj;
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
- && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown == otherEntry.mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
}
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
index 2aec3f8..c0f89bc 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -21,36 +21,28 @@
import java.util.List;
/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
-public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry {
+public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListSearchHeaderEntry> {
- private boolean mIsWidgetListShown = false;
- private boolean mHasEntryUpdated = false;
+ private final boolean mIsWidgetListShown;
public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items) {
- super(pkgItem, titleSectionName, items);
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
}
- /** Sets if the widgets list associated with this header is shown. */
- public void setIsWidgetListShown(boolean isWidgetListShown) {
- if (mIsWidgetListShown != isWidgetListShown) {
- this.mIsWidgetListShown = isWidgetListShown;
- mHasEntryUpdated = true;
- } else {
- mHasEntryUpdated = false;
- }
+ private WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
+ super(pkgItem, titleSectionName, items);
+ mIsWidgetListShown = isWidgetListShown;
}
/** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
public boolean isWidgetListShown() {
return mIsWidgetListShown;
}
- /** Returns {@code true} if this entry has been updated due to user interactions. */
- public boolean hasEntryUpdated() {
- return mHasEntryUpdated;
- }
-
@Override
public String toString() {
return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size();
@@ -67,6 +59,18 @@
if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false;
WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
- && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListSearchHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListSearchHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListSearchHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index 42896ba..dfe447a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -177,7 +177,7 @@
*/
private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
- return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow);
+ return !curRow.equals(newRow);
}
if (newRow instanceof WidgetsListSearchHeaderEntry
&& curRow instanceof WidgetsListSearchHeaderEntry) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 6863c60..5d9adf0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.os.Process;
import android.util.Log;
+import android.util.Size;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
@@ -35,19 +36,25 @@
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.LabelComparator;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
import java.util.Arrays;
@@ -79,7 +86,9 @@
private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
+ private final Context mContext;
private final Launcher mLauncher;
+ private final CachingWidgetPreviewLoader mCachingPreviewLoader;
private final WidgetsDiffReporter mDiffReporter;
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
@@ -97,16 +106,23 @@
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
+ @Nullable private PackageUserKey mPendingClickHeader;
+ private int mShortcutPreviewPadding;
+
+ private final WidgetPreviewLoadedCallback mPreviewLoadedCallback =
+ ignored -> updateVisibleEntries();
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
- WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+ DatabaseWidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
+ mContext = context;
mLauncher = Launcher.getLauncher(context);
+ mCachingPreviewLoader = new CachingWidgetPreviewLoader(widgetPreviewLoader);
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);
- mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
+ mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(
layoutInflater, iconClickListener, iconLongClickListener,
- widgetPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
+ mCachingPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
mViewHolderBinders.put(
VIEW_TYPE_WIDGETS_HEADER,
@@ -122,6 +138,9 @@
/* onHeaderClickListener= */ this,
listDrawableFactory,
/* listAdapter= */ this));
+ mShortcutPreviewPadding =
+ 2 * context.getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
}
@Override
@@ -177,6 +196,7 @@
/** Updates the widget list based on {@code tempEntries}. */
public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
+ mCachingPreviewLoader.clearAll();
mAllEntries = tempEntries.stream().sorted(mRowComparator)
.collect(Collectors.toList());
if (shouldClearVisibleEntries()) {
@@ -189,36 +209,110 @@
public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
// Forget the expanded package every time widget list is refreshed in search mode.
mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
setWidgets(searchResults);
}
private void updateVisibleEntries() {
- mAllEntries.forEach(entry -> {
- if (entry instanceof WidgetsListHeaderEntry) {
- ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
- isHeaderForVisibleContent(entry));
- } else if (entry instanceof WidgetsListSearchHeaderEntry) {
- ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown(
- isHeaderForVisibleContent(entry));
- }
- });
+ // If not all previews are ready, then defer this update and try again after the preview
+ // loads.
+ if (!ensureAllPreviewsReady()) return;
+
+ // Get the current top of the header with the matching key before adjusting the visible
+ // entries.
+ OptionalInt previousPositionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ OptionalInt topForPackageUserKey =
+ getOffsetForPosition(previousPositionForPackageUserKey);
+
List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
.filter(entry -> (mFilter == null || mFilter.test(entry))
&& mHeaderAndSelectedContentFilter.test(entry))
+ .map(entry -> {
+ // Adjust the original entries to expand headers for the selected content.
+ if (entry instanceof WidgetsListBaseEntry.Header<?>
+ && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ return ((WidgetsListBaseEntry.Header<?>) entry).withWidgetListShown();
+ }
+ return entry;
+ })
.collect(Collectors.toList());
+
mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
+
+ if (mPendingClickHeader != null) {
+ // Get the position for the clicked header after adjusting the visible entries. The
+ // position may have changed if another header had previously been expanded.
+ OptionalInt positionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
+ mPendingClickHeader = null;
+ }
}
- /** Returns whether {@code entry} matches {@link #mWidgetsContentVisiblePackageUserKey}. */
- private boolean isHeaderForVisibleContent(WidgetsListBaseEntry entry) {
- return isHeaderForPackageUserKey(entry, mWidgetsContentVisiblePackageUserKey);
+ /**
+ * Checks that all preview images are loaded and starts loading for those that aren't ready.
+ *
+ * @return true if all previews are ready and the data can be updated, false otherwise.
+ */
+ private boolean ensureAllPreviewsReady() {
+ boolean allReady = true;
+ BaseActivity activity = BaseActivity.fromContext(mContext);
+ for (WidgetsListBaseEntry entry : mAllEntries) {
+ if (!(entry instanceof WidgetsListContentEntry)) continue;
+
+ WidgetsListContentEntry contentEntry = (WidgetsListContentEntry) entry;
+ if (!matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ // If the entry isn't visible, clear any loaded previews.
+ mCachingPreviewLoader.clearPreviews(contentEntry.mWidgets);
+ continue;
+ }
+
+ for (int i = 0; i < entry.mWidgets.size(); i++) {
+ WidgetItem widgetItem = entry.mWidgets.get(i);
+ DeviceProfile deviceProfile = activity.getDeviceProfile();
+ Size widgetSize =
+ WidgetSizes.getWidgetSizePx(
+ deviceProfile,
+ widgetItem.spanX,
+ widgetItem.spanY);
+ if (widgetItem.isShortcut()) {
+ widgetSize =
+ new Size(
+ widgetSize.getWidth() + mShortcutPreviewPadding,
+ widgetSize.getHeight() + mShortcutPreviewPadding);
+ }
+
+ if (widgetItem.hasPreviewLayout()
+ || mCachingPreviewLoader.isPreviewLoaded(widgetItem, widgetSize)) {
+ // The widget is ready if it can be rendered with a preview layout or if its
+ // preview bitmap is in the cache.
+ continue;
+ }
+
+ // If we've reached this point, we should load the preview for the widget.
+ allReady = false;
+ mCachingPreviewLoader.loadPreview(
+ activity,
+ widgetItem,
+ widgetSize,
+ mPreviewLoadedCallback);
+ }
+ }
+ return allReady;
}
/** Returns whether {@code entry} matches {@code key}. */
- private boolean isHeaderForPackageUserKey(WidgetsListBaseEntry entry, PackageUserKey key) {
- return (entry instanceof WidgetsListHeaderEntry
- || entry instanceof WidgetsListSearchHeaderEntry)
- && new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user).equals(key);
+ private static boolean isHeaderForPackageUserKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key);
+ }
+
+ private static boolean matchesKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ if (key == null) return false;
+ return entry.mPkgItem.packageName.equals(key.mPackageName)
+ && entry.mPkgItem.user.equals(key.mUser);
}
/**
@@ -227,6 +321,7 @@
public void resetExpandedHeader() {
if (mWidgetsContentVisiblePackageUserKey != null) {
mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
updateVisibleEntries();
}
}
@@ -285,6 +380,8 @@
// Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
+ cancelLoadingPreviews();
+
if (showWidgets) {
mWidgetsContentVisiblePackageUserKey = packageUserKey;
mLauncher.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED);
@@ -292,17 +389,15 @@
mWidgetsContentVisiblePackageUserKey = null;
}
- // Get the current top of the header with the matching key before adjusting the visible
- // entries.
- OptionalInt topForPackageUserKey =
- getOffsetForPosition(getPositionForPackageUserKey(packageUserKey));
+ // Store the header that was clicked so that its position will be maintained the next time
+ // we update the entries.
+ mPendingClickHeader = packageUserKey;
updateVisibleEntries();
+ }
- // Get the position for the clicked header after adjusting the visible entries. The
- // position may have changed if another header had previously been expanded.
- OptionalInt positionForPackageUserKey = getPositionForPackageUserKey(packageUserKey);
- scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
+ private void cancelLoadingPreviews() {
+ mCachingPreviewLoader.clearAll();
}
/** Returns the position of the currently expanded header, or empty if it's not present. */
@@ -315,7 +410,8 @@
* Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not
* present.
*/
- private OptionalInt getPositionForPackageUserKey(PackageUserKey key) {
+ @NonNull
+ private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) {
return IntStream.range(0, mVisibleEntries.size())
.filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key))
.findFirst();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 7e8c55b..7b52663 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -18,8 +18,9 @@
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
-import android.content.Context;
+import android.graphics.Bitmap;
import android.util.Log;
+import android.util.Size;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,9 +31,9 @@
import android.widget.TableRow;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.util.WidgetsTableUtils;
@@ -52,17 +53,16 @@
private final LayoutInflater mLayoutInflater;
private final OnClickListener mIconClickListener;
private final OnLongClickListener mIconLongClickListener;
- private final WidgetPreviewLoader mWidgetPreviewLoader;
private final WidgetsListDrawableFactory mListDrawableFactory;
+ private final CachingWidgetPreviewLoader mWidgetPreviewLoader;
private final WidgetsListAdapter mWidgetsListAdapter;
private boolean mApplyBitmapDeferred = false;
public WidgetsListTableViewHolderBinder(
- Context context,
LayoutInflater layoutInflater,
OnClickListener iconClickListener,
OnLongClickListener iconLongClickListener,
- WidgetPreviewLoader widgetPreviewLoader,
+ CachingWidgetPreviewLoader widgetPreviewLoader,
WidgetsListDrawableFactory listDrawableFactory,
WidgetsListAdapter listAdapter) {
mLayoutInflater = layoutInflater;
@@ -75,7 +75,7 @@
/**
* Defers applying bitmap on all the {@link WidgetCell} at
- * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if
+ * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry, int)} if
* {@code applyBitmapDeferred} is {@code true}.
*/
public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
@@ -124,10 +124,15 @@
WidgetCell widget = (WidgetCell) row.getChildAt(j);
widget.clear();
WidgetItem widgetItem = widgetItemsPerRow.get(j);
- widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+ Size previewSize = widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
- widget.ensurePreview();
+ Bitmap preview = mWidgetPreviewLoader.getPreview(widgetItem, previewSize);
+ if (preview == null) {
+ widget.ensurePreview();
+ } else {
+ widget.applyPreview(preview);
+ }
widget.setVisibility(View.VISIBLE);
}
}
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index b35c75b..df5d465 100644
--- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -127,7 +127,7 @@
@Override
public void setActiveMarker(int activePage) {
updateTabTextColor(activePage);
- updateIndicatorPosition(activePage);
+ updateIndicatorPosition(mIsRtl ? 1 - activePage : activePage);
if (mOnActivePageChangedListener != null && mLastActivePage != activePage) {
mOnActivePageChangedListener.onActivePageChanged(activePage);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index e86be2a..55be593 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -136,7 +136,7 @@
case THREE_BUTTON:
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.runToState(
- () -> mLauncher.waitForSystemUiObject("recent_apps").click(),
+ () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
OVERVIEW_STATE_ORDINAL);
break;
}
@@ -224,7 +224,7 @@
case THREE_BUTTON:
// Double press the recents button.
- UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+ UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
mLauncher.getOverview();
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 96e8222..95a15c0 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -283,6 +283,11 @@
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ public boolean isTablet() {
+ return getTestInfo(TestProtocol.REQUEST_IS_TABLET)
+ .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
void setActiveContainer(VisibleContainer container) {
sActiveContainer = new WeakReference<>(container);
}
@@ -554,29 +559,35 @@
public String getNavigationModeMismatchError(boolean waitForCorrectState) {
final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
final NavigationModel navigationModel = getNavigationModel();
-
+ String resPackage = getNavigationButtonResPackage();
if (navigationModel == NavigationModel.THREE_BUTTON) {
- if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ if (!mDevice.wait(Until.hasObject(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button not present in 3-button mode";
}
} else {
- if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ if (!mDevice.wait(Until.gone(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button is present in non-3-button mode";
}
}
if (navigationModel == NavigationModel.ZERO_BUTTON) {
- if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ if (!mDevice.wait(Until.gone(By.res(resPackage, "home")), waitTime)) {
return "Home button is present in gestural mode";
}
} else {
- if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ if (!mDevice.wait(Until.hasObject(By.res(resPackage, "home")), waitTime)) {
return "Home button not present in non-gestural mode";
}
}
return null;
}
+ private String getNavigationButtonResPackage() {
+ return isTablet() && getNavigationModel() == NavigationModel.THREE_BUTTON ?
+ getLauncherPackageName() :
+ SYSTEMUI_PACKAGE;
+ }
+
private UiObject2 verifyContainerType(ContainerType containerType) {
waitForLauncherInitialized();
@@ -741,7 +752,7 @@
}
runToState(
- waitForSystemUiObject("home")::click,
+ waitForNavigationUiObject("home")::click,
NORMAL_STATE_ORDINAL,
!hasLauncherObject(WORKSPACE_RES_ID)
&& (hasLauncherObject(APPS_RES_ID)
@@ -891,6 +902,15 @@
return object;
}
+ @NonNull
+ UiObject2 waitForNavigationUiObject(String resId) {
+ String resPackage = getNavigationButtonResPackage();
+ final UiObject2 object = mDevice.wait(
+ Until.findObject(By.res(resPackage, resId)), WAIT_TIME_MS);
+ assertNotNull("Can't find a navigation UI object with id: " + resId, object);
+ return object;
+ }
+
@Nullable
UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
try {
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 1ea0922..f4fe49d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -63,7 +63,7 @@
/**
* Swipes up to All Apps.
*
- * @return the App Apps object.
+ * @return the All Apps object.
*/
@NonNull
public AllApps switchToAllApps() {