• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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