Merge "Check if all apps are translucent when finishing recents animation." into main
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 617cac7..bfa00bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,12 +17,14 @@
package com.android.launcher3.model;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.oneGridSpecs;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -130,6 +132,20 @@
// Only use this strategy when comparing the previous grid to the new grid and the
// columns are the same and the destination has more rows
copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
+
+ if (oneGridSpecs()) {
+ DbReader destReader = new DbReader(
+ target.getWritableDatabase(), TABLE_NAME, context);
+ boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
+ if (shouldShiftCells) {
+ shiftTableByXCells(
+ target.getWritableDatabase(),
+ (destDeviceState.getRows() - srcDeviceState.getRows()),
+ TABLE_NAME);
+ }
+ }
+
+ // Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context);
return true;
}
@@ -427,17 +443,22 @@
}
}
- static void copyCurrentGridToNewGrid(
- @NonNull Context context,
- @NonNull DeviceGridState destDeviceState,
- @NonNull DatabaseHelper target,
- @NonNull SQLiteDatabase source) {
- // Only use this strategy when comparing the previous grid to the new grid and the
- // columns are the same and the destination has more rows
- copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
- destDeviceState.writeToPrefs(context);
+ private static boolean shouldShiftCells(final DbReader destReader, final int srcGridRowCount) {
+ List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
+ int firstPageItemsRowPosSum = workspaceItems.stream()
+ .filter(entry -> entry.screenId == 0)
+ .mapToInt(entry -> entry.cellY).sum();
+ int firstPageWorkspaceItemsCount = (int) workspaceItems.stream()
+ .filter(entry -> entry.screenId == 0).count();
+ if (firstPageWorkspaceItemsCount == 0) {
+ return false;
+ }
+ float srcGridMidPoint = srcGridRowCount / 2f;
+ float firstPageItemPosAvg = (float) firstPageItemsRowPosSum / firstPageWorkspaceItemsCount;
+ return (firstPageItemPosAvg >= srcGridMidPoint);
}
+
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static class DbReader {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index c856d4b..3f52d8a 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -21,15 +21,20 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags
+import com.android.launcher3.Flags.oneGridSpecs
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.get
import com.android.launcher3.LauncherPrefs.Companion.getPrefs
import com.android.launcher3.LauncherSettings
+import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME
+import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE
import com.android.launcher3.Utilities
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
-import com.android.launcher3.provider.LauncherDbUtils
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.provider.LauncherDbUtils.copyTable
+import com.android.launcher3.provider.LauncherDbUtils.dropTable
+import com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells
import com.android.launcher3.util.CellAndSpan
import com.android.launcher3.util.GridOccupancy
import com.android.launcher3.util.IntArray
@@ -59,27 +64,30 @@
// amount of rows we simply copy over the source grid to the destination grid, rather
// than undergoing the general grid migration.
if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
- GridSizeMigrationDBController.copyCurrentGridToNewGrid(
- context,
- destDeviceState,
- target,
- source,
- )
+ copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
+ if (oneGridSpecs()) {
+ val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
+ val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
+ if (shouldShiftCells) {
+ shiftTableByXCells(
+ target.writableDatabase,
+ (destDeviceState.rows - srcDeviceState.rows),
+ TABLE_NAME,
+ )
+ }
+ }
+ // Save current configuration, so that the migration does not run again.
+ destDeviceState.writeToPrefs(context)
return
}
- LauncherDbUtils.copyTable(
- source,
- LauncherSettings.Favorites.TABLE_NAME,
- target.writableDatabase,
- LauncherSettings.Favorites.TMP_TABLE,
- context,
- )
+
+ copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
val migrationStartTime = System.currentTimeMillis()
try {
SQLiteTransaction(target.writableDatabase).use { t ->
- val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
- val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
+ val srcReader = DbReader(t.db, TMP_TABLE, context)
+ val destReader = DbReader(t.db, TABLE_NAME, context)
val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
@@ -95,7 +103,7 @@
// Migrate workspace.
migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
- LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+ dropTable(t.db, TMP_TABLE)
t.commit()
}
} catch (e: Exception) {
@@ -112,6 +120,19 @@
}
}
+ private fun shouldShiftCells(destReader: DbReader, srcGridRowCount: Int): Boolean {
+ val workspaceItems = destReader.loadAllWorkspaceEntries()
+ val firstPageItemsRowPosSum =
+ workspaceItems.sumOf { entry -> if (entry.screenId == 0) entry.cellY else 0 }
+ val firstPageWorkspaceItemsCount = workspaceItems.count { entry -> entry.screenId == 0 }
+ if (firstPageWorkspaceItemsCount == 0) {
+ return false
+ }
+ val srcGridMidPoint = srcGridRowCount / 2f
+ val firstPageItemPosAvg = firstPageItemsRowPosSum / firstPageWorkspaceItemsCount.toFloat()
+ return (firstPageItemPosAvg >= srcGridMidPoint)
+ }
+
/** Handles hotseat migration. */
@VisibleForTesting
fun migrateHotseat(
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
index 3c68e46..6f1d0dd 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.kt
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -131,6 +131,11 @@
}
}
+ @JvmStatic
+ fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) {
+ db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") }
+ }
+
/**
* Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
* shortcut or any shortcut which requires some permission to launch
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 82229f8..e4c50f0 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -18,18 +18,23 @@
import android.content.Context
import android.util.Log
+import android.view.InflateException
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.launcher3.BubbleTextView
import com.android.launcher3.BuildConfig
import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.util.CancellableTask
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
+import java.lang.IllegalStateException
const val PREINFLATE_ICONS_ROW_COUNT = 4
const val EXTRA_ICONS_COUNT = 2
@@ -39,10 +44,11 @@
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
var hasWorkProfile = false
- private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+ @VisibleForTesting(otherwise = PROTECTED)
+ var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
companion object {
private const val TAG = "AllAppsRecyclerViewPool"
@@ -53,7 +59,7 @@
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
- fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+ fun preInflateAllAppsViewHolders(context: T) {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
val preInflateCount = getPreinflateCount(context)
@@ -97,36 +103,65 @@
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
+ preInflateAllAppsViewHolders(
+ adapter,
+ BaseAllAppsAdapter.VIEW_TYPE_ICON,
+ activeRv,
+ preInflateCount,
+ ) {
+ getPreinflateCount(context)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PROTECTED)
+ fun preInflateAllAppsViewHolders(
+ adapter: RecyclerView.Adapter<*>,
+ viewType: Int,
+ activeRv: RecyclerView,
+ preInflationCount: Int,
+ preInflationCountProvider: () -> Int,
+ ) {
+ if (preInflationCount <= 0) {
+ return
+ }
mCancellableTask?.cancel()
var task: CancellableTask<List<ViewHolder>>? = null
task =
CancellableTask(
{
val list: ArrayList<ViewHolder> = ArrayList()
- for (i in 0 until preInflateCount) {
+ for (i in 0 until preInflationCount) {
if (task?.canceled == true) {
break
}
// If activeRv's layout manager has been reset to null on main thread, skip
// the preinflation as we cannot generate correct LayoutParams
if (activeRv.layoutManager == null) {
+ list.clear()
break
}
- list.add(
- adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
- )
+ try {
+ list.add(adapter.createViewHolder(activeRv, viewType))
+ } catch (e: InflateException) {
+ list.clear()
+ // It's still possible for UI thread to set activeRv's layout manager to
+ // null and we should break the loop and cancel the preinflation.
+ break
+ }
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
- for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+ // Run preInflationCountProvider again as the needed VH might have changed
+ val newPreInflationCount = preInflationCountProvider.invoke()
+ for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
putRecycledView(viewHolders[i])
}
},
)
mCancellableTask = task
- VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+ VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
}
/**
@@ -143,10 +178,11 @@
* app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
* suffice fast scrolling.
*
- * Note that we need to preinfate extra app icons in size of one all apps pages, so that opening
- * all apps don't need to inflate app icons.
+ * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+ * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+ * icons.
*/
- fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+ fun getPreinflateCount(context: T): Int {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000..3afb0b5
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 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.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.LayoutManager
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
+
+ private lateinit var underTest: AllAppsRecyclerViewPool<T>
+ private lateinit var adapter: RecyclerView.Adapter<*>
+
+ @Mock private lateinit var parent: RecyclerView
+ @Mock private lateinit var itemView: View
+ @Mock private lateinit var layoutManager: LayoutManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = spy(AllAppsRecyclerViewPool())
+ adapter =
+ object : RecyclerView.Adapter<ViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ object : ViewHolder(itemView) {}
+
+ override fun getItemCount() = 0
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+ }
+ underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+ `when`(parent.layoutManager).thenReturn(layoutManager)
+ }
+
+ @Test
+ fun preinflate_success() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+ }
+
+ @Test
+ fun preinflate_not_triggered() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_before_runOnMainThread() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+ underTest.clear()
+
+ awaitTasksCompleted()
+ verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_after_run() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+ awaitTasksCompleted()
+
+ underTest.clear()
+
+ verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
+ Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+ }
+
+ companion object {
+ private const val VIEW_TYPE: Int = 4
+ }
+}