1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.model 18 19 import android.platform.test.flag.junit.SetFlagsRule 20 import android.util.Log 21 import androidx.test.ext.junit.runners.AndroidJUnit4 22 import androidx.test.filters.SmallTest 23 import androidx.test.platform.app.InstrumentationRegistry 24 import com.android.launcher3.Flags 25 import com.android.launcher3.GridType.Companion.GRID_TYPE_ANY 26 import com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE 27 import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME 28 import com.android.launcher3.celllayout.board.CellLayoutBoard 29 import com.android.launcher3.pm.UserCache 30 import com.android.launcher3.util.rule.TestToPhoneFileCopier 31 import com.android.launcher3.util.rule.setFlags 32 import org.junit.Before 33 import org.junit.Rule 34 import org.junit.Test 35 import org.junit.runner.RunWith 36 import org.mockito.kotlin.mock 37 import org.mockito.kotlin.verify 38 39 private val phoneContext = InstrumentationRegistry.getInstrumentation().targetContext 40 41 data class EntryData( 42 val x: Int, 43 val y: Int, 44 val screenId: Int, 45 val spanX: Int, 46 val spanY: Int, 47 val rank: Int, 48 ) 49 50 /** 51 * Holds the data needed to run a test in GridMigrationTest, usually we would have a src 52 * GridMigrationData and a dst GridMigrationData meaning the data after a migration has occurred. 53 * This class holds a gridState, which is the size of the grid like 5x5 (among other things). a 54 * dbHelper which contains the readable database and writable database used to migrate the 55 * databases. 56 * 57 * You can also get all the entries defined in the dbHelper database. 58 */ 59 class GridMigrationData(dbFileName: String?, val gridState: DeviceGridState) { 60 61 val dbHelper: DatabaseHelper = 62 DatabaseHelper( 63 phoneContext, 64 dbFileName, 65 { UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) }, 66 {}, 67 ) 68 69 fun readEntries(): List<DbEntry> = 70 GridSizeMigrationDBController.readAllEntries( 71 dbHelper.readableDatabase, 72 TABLE_NAME, 73 phoneContext, 74 ) 75 } 76 77 /** 78 * Test the migration of a database from one size to another. It reads a database from the test 79 * assets, uploads it into the phone and migrates the database to a database in memory which is 80 * later compared against a database in the test assets to make sure they are identical. 81 */ 82 @SmallTest 83 @RunWith(AndroidJUnit4::class) 84 class GridMigrationTest { 85 private val DB_FILE = "test_launcher.db" 86 87 @JvmField 88 @Rule 89 val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) 90 91 val modelDelegate = mock<ModelDelegate>() 92 93 @Before setupnull94 fun setup() { 95 setFlagsRule.setFlags(true, Flags.FLAG_ONE_GRID_SPECS) 96 } 97 migratenull98 private fun migrate(src: GridMigrationData, dst: GridMigrationData) { 99 if (Flags.gridMigrationRefactor()) { 100 val gridSizeMigrationLogic = GridSizeMigrationLogic() 101 gridSizeMigrationLogic.migrateGrid( 102 phoneContext, 103 src.gridState, 104 dst.gridState, 105 dst.dbHelper, 106 src.dbHelper.readableDatabase, 107 true, 108 modelDelegate, 109 ) 110 } else { 111 GridSizeMigrationDBController.migrateGridIfNeeded( 112 phoneContext, 113 src.gridState, 114 dst.gridState, 115 dst.dbHelper, 116 src.dbHelper.readableDatabase, 117 true, 118 modelDelegate, 119 ) 120 } 121 } 122 123 /** 124 * Makes sure that none of the items overlaps on the result, i.e. no widget or icons share the 125 * same space in the db. 126 */ validateDbnull127 private fun validateDb(data: GridMigrationData) { 128 // The array size is just a big enough number to fit all the number of workspaces 129 val boards = Array(100) { CellLayoutBoard(data.gridState.columns, data.gridState.rows) } 130 data.readEntries().forEach { 131 val cellLayoutBoard = boards[it.screenId] 132 assert(cellLayoutBoard.isEmpty(it.cellX, it.cellY, it.spanX, it.spanY)) { 133 "Db has overlapping items" 134 } 135 cellLayoutBoard.addWidget(it.cellX, it.cellY, it.spanX, it.spanY) 136 } 137 } 138 comparenull139 private fun compare(dst: GridMigrationData, target: GridMigrationData, src: GridMigrationData) { 140 val sort = compareBy<DbEntry>({ it.screenId }, { it.cellX }, { it.cellY }) 141 val mapF = { it: DbEntry -> 142 EntryData(it.cellX, it.cellY, it.screenId, it.spanX, it.spanY, it.rank) 143 } 144 val entriesDst = dst.readEntries().sortedWith(sort).map(mapF) 145 val entriesTarget = target.readEntries().sortedWith(sort).map(mapF) 146 val entriesSrc = src.readEntries().sortedWith(sort).map(mapF) 147 Log.i( 148 TAG, 149 "entriesSrc: $entriesSrc\n entriesDst: $entriesDst\n entriesTarget: $entriesTarget", 150 ) 151 assert(entriesDst == entriesTarget) { 152 "The elements on the dst database is not the same as in the target" 153 } 154 } 155 156 /** 157 * Migrate src into dst and compare to target. This method validates 4 things: 158 * 1. dst has the same number of items as src after the migration, meaning, none of the items 159 * were removed during the migration. 160 * 2. dst is valid, meaning that none of the items overlap with each other. 161 * 3. dst is equal to target to ensure we don't unintentionally change the migration logic. 162 * 4. migration notifies the complete callback. 163 */ runTestnull164 private fun runTest(src: GridMigrationData, dst: GridMigrationData, target: GridMigrationData) { 165 migrate(src, dst) 166 assert(src.readEntries().size == dst.readEntries().size) { 167 "Source db and destination db do not contain the same number of elements" 168 } 169 validateDb(dst) 170 compare(dst, target, src) 171 verify(modelDelegate).gridMigrationComplete(src.gridState, dst.gridState) 172 } 173 174 // Copying the src db for all tests. 175 @JvmField 176 @Rule 177 val fileCopier = 178 TestToPhoneFileCopier( 179 src = "databases/GridMigrationTest/$DB_FILE", 180 dest = "databases/$DB_FILE", 181 removeOnFinish = true, 182 ) 183 184 @JvmField 185 @Rule 186 val result5x5to3x3 = 187 TestToPhoneFileCopier( 188 src = "databases/GridMigrationTest/result5x5to3x3.db", 189 dest = "databases/result5x5to3x3.db", 190 removeOnFinish = true, 191 ) 192 193 @Test 5x5 to 3x3null194 fun `5x5 to 3x3`() = 195 runTest( 196 src = 197 GridMigrationData( 198 DB_FILE, 199 DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE, GRID_TYPE_ANY), 200 ), 201 dst = 202 GridMigrationData( 203 null, // in memory db, to download a new db change null for 204 // the filename of the db name to store it. Do not use existing names. 205 DeviceGridState(3, 3, 3, TYPE_PHONE, "", GRID_TYPE_ANY), 206 ), 207 target = 208 GridMigrationData( 209 "result5x5to3x3.db", 210 DeviceGridState(3, 3, 3, TYPE_PHONE, "", GRID_TYPE_ANY), 211 ), 212 ) 213 214 @JvmField 215 @Rule 216 val result5x5to4x7 = 217 TestToPhoneFileCopier( 218 src = "databases/GridMigrationTest/result5x5to4x7.db", 219 dest = "databases/result5x5to4x7.db", 220 removeOnFinish = true, 221 ) 222 223 @Test 224 fun `5x5 to 4x7`() = 225 runTest( 226 src = 227 GridMigrationData( 228 DB_FILE, 229 DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE, GRID_TYPE_ANY), 230 ), 231 dst = 232 GridMigrationData( 233 null, // in memory db, to download a new db change null for 234 // the filename of the db name to store it. Do not use existing names. 235 DeviceGridState(4, 7, 4, TYPE_PHONE, "", GRID_TYPE_ANY), 236 ), 237 target = 238 GridMigrationData( 239 "result5x5to4x7.db", 240 DeviceGridState(4, 7, 4, TYPE_PHONE, "", GRID_TYPE_ANY), 241 ), 242 ) 243 244 @JvmField 245 @Rule 246 val result5x5to5x8 = 247 TestToPhoneFileCopier( 248 src = "databases/GridMigrationTest/result5x5to5x8.db", 249 dest = "databases/result5x5to5x8.db", 250 removeOnFinish = true, 251 ) 252 253 @Test 254 fun `5x5 to 5x8`() = 255 runTest( 256 src = 257 GridMigrationData( 258 DB_FILE, 259 DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE, GRID_TYPE_ANY), 260 ), 261 dst = 262 GridMigrationData( 263 null, // in memory db, to download a new db change null 264 // for 265 // the filename of the db name to store it. Do not use existing names. 266 DeviceGridState(5, 8, 5, TYPE_PHONE, "", GRID_TYPE_ANY), 267 ), 268 target = 269 GridMigrationData( 270 "result5x5to5x8.db", 271 DeviceGridState(5, 8, 5, TYPE_PHONE, "", GRID_TYPE_ANY), 272 ), 273 ) 274 275 companion object { 276 private const val TAG = "GridMigrationTest" 277 } 278 } 279