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 androidx.test.ext.junit.runners.AndroidJUnit4
21 import androidx.test.filters.SmallTest
22 import androidx.test.platform.app.InstrumentationRegistry
23 import com.android.launcher3.Flags
24 import com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE
25 import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME
26 import com.android.launcher3.celllayout.board.CellLayoutBoard
27 import com.android.launcher3.pm.UserCache
28 import com.android.launcher3.util.rule.TestToPhoneFileCopier
29 import com.android.launcher3.util.rule.setFlags
30 import org.junit.Before
31 import org.junit.Rule
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 
35 private val phoneContext = InstrumentationRegistry.getInstrumentation().targetContext
36 
37 data class EntryData(val x: Int, val y: Int, val spanX: Int, val spanY: Int, val rank: Int)
38 
39 /**
40  * Holds the data needed to run a test in GridMigrationTest, usually we would have a src
41  * GridMigrationData and a dst GridMigrationData meaning the data after a migration has occurred.
42  * This class holds a gridState, which is the size of the grid like 5x5 (among other things). a
43  * dbHelper which contains the readable database and writable database used to migrate the
44  * databases.
45  *
46  * You can also get all the entries defined in the dbHelper database.
47  */
48 class GridMigrationData(dbFileName: String?, val gridState: DeviceGridState) {
49 
50     val dbHelper: DatabaseHelper =
51         DatabaseHelper(
52             phoneContext,
53             dbFileName,
54             { UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) },
55             {}
56         )
57 
58     fun readEntries(): List<GridSizeMigrationUtil.DbEntry> =
59         GridSizeMigrationUtil.readAllEntries(dbHelper.readableDatabase, TABLE_NAME, phoneContext)
60 }
61 
62 /**
63  * Test the migration of a database from one size to another. It reads a database from the test
64  * assets, uploads it into the phone and migrates the database to a database in memory which is
65  * later compared against a database in the test assets to make sure they are identical.
66  */
67 @SmallTest
68 @RunWith(AndroidJUnit4::class)
69 class GridMigrationTest {
70     private val DB_FILE = "test_launcher.db"
71 
72     @JvmField
73     @Rule
74     val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
75 
76     // Copying the src db for all tests.
77     @JvmField
78     @Rule
79     val fileCopier =
80         TestToPhoneFileCopier(
81             src = "databases/GridMigrationTest/$DB_FILE",
82             dest = "databases/$DB_FILE",
83             removeOnFinish = true
84         )
85 
86     @Before
setupnull87     fun setup() {
88         setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_GRID_MIGRATION_FIX)
89     }
90 
migratenull91     private fun migrate(src: GridMigrationData, dst: GridMigrationData) {
92         GridSizeMigrationUtil.migrateGridIfNeeded(
93             phoneContext,
94             src.gridState,
95             dst.gridState,
96             dst.dbHelper,
97             src.dbHelper.readableDatabase
98         )
99     }
100 
101     /**
102      * Makes sure that none of the items overlaps on the result, i.e. no widget or icons share the
103      * same space in the db.
104      */
validateDbnull105     private fun validateDb(data: GridMigrationData) {
106         // The array size is just a big enough number to fit all the number of workspaces
107         val boards = Array(100) { CellLayoutBoard(data.gridState.columns, data.gridState.rows) }
108         data.readEntries().forEach {
109             val cellLayoutBoard = boards[it.screenId]
110             assert(cellLayoutBoard.isEmpty(it.cellX, it.cellY, it.spanX, it.spanY)) {
111                 "Db has overlapping items"
112             }
113             cellLayoutBoard.addWidget(it.cellX, it.cellY, it.spanX, it.spanY)
114         }
115     }
116 
comparenull117     private fun compare(dst: GridMigrationData, target: GridMigrationData) {
118         val sort = compareBy<GridSizeMigrationUtil.DbEntry>({ it.cellX }, { it.cellY })
119         val mapF = { it: GridSizeMigrationUtil.DbEntry ->
120             EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank)
121         }
122         val entriesDst = dst.readEntries().sortedWith(sort).map(mapF)
123         val entriesTarget = target.readEntries().sortedWith(sort).map(mapF)
124 
125         assert(entriesDst == entriesTarget) {
126             "The elements on the dst database is not the same as in the target"
127         }
128     }
129 
130     /**
131      * Migrate src into dst and compare to target. This method validates 3 things:
132      * 1. dst has the same number of items as src after the migration, meaning, none of the items
133      *    were removed during the migration.
134      * 2. dst is valid, meaning that none of the items overlap with each other.
135      * 3. dst is equal to target to ensure we don't unintentionally change the migration logic.
136      */
runTestnull137     private fun runTest(src: GridMigrationData, dst: GridMigrationData, target: GridMigrationData) {
138         migrate(src, dst)
139         assert(src.readEntries().size == dst.readEntries().size) {
140             "Source db and destination db do not contain the same number of elements"
141         }
142         validateDb(dst)
143         compare(dst, target)
144     }
145 
146     @JvmField
147     @Rule
148     val result5x5to3x3 =
149         TestToPhoneFileCopier(
150             src = "databases/GridMigrationTest/result5x5to3x3.db",
151             dest = "databases/result5x5to3x3.db",
152             removeOnFinish = true
153         )
154 
155     @Test
5x5 to 3x3null156     fun `5x5 to 3x3`() =
157         runTest(
158             src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
159             dst =
160                 GridMigrationData(
161                     null, // in memory db, to download a new db change null for the filename of the
162                     // db name to store it. Do not use existing names.
163                     DeviceGridState(3, 3, 3, TYPE_PHONE, "")
164                 ),
165             target =
166                 GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, ""))
167         )
168 
169     @JvmField
170     @Rule
171     val result5x5to4x7 =
172         TestToPhoneFileCopier(
173             src = "databases/GridMigrationTest/result5x5to4x7.db",
174             dest = "databases/result5x5to4x7.db",
175             removeOnFinish = true
176         )
177 
178     @Test
179     fun `5x5 to 4x7`() =
180         runTest(
181             src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
182             dst =
183                 GridMigrationData(
184                     null, // in memory db, to download a new db change null for the filename of the
185                     // db name to store it. Do not use existing names.
186                     DeviceGridState(4, 7, 4, TYPE_PHONE, "")
187                 ),
188             target =
189                 GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, ""))
190         )
191 
192     @JvmField
193     @Rule
194     val result5x5to5x8 =
195         TestToPhoneFileCopier(
196             src = "databases/GridMigrationTest/result5x5to5x8.db",
197             dest = "databases/result5x5to5x8.db",
198             removeOnFinish = true
199         )
200 
201     @Test
202     fun `5x5 to 5x8`() =
203         runTest(
204             src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
205             dst =
206                 GridMigrationData(
207                     null, // in memory db, to download a new db change null for the filename of the
208                     // db name to store it. Do not use existing names.
209                     DeviceGridState(5, 8, 5, TYPE_PHONE, "")
210                 ),
211             target =
212                 GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, ""))
213         )
214 
215     @JvmField
216     @Rule
217     val flaggedResult5x5to5x8 =
218         TestToPhoneFileCopier(
219             src = "databases/GridMigrationTest/flagged_result5x5to5x8.db",
220             dest = "databases/flagged_result5x5to5x8.db",
221             removeOnFinish = true
222         )
223 
224     @Test
225     fun `flagged 5x5 to 5x8`() {
226         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_GRID_MIGRATION_FIX)
227         runTest(
228             src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
229             dst =
230                 GridMigrationData(
231                     null, // in memory db, to download a new db change null for the filename of the
232                     // db name to store it. Do not use existing names.
233                     DeviceGridState(5, 8, 5, TYPE_PHONE, "")
234                 ),
235             target =
236                 GridMigrationData(
237                     "flagged_result5x5to5x8.db",
238                     DeviceGridState(5, 8, 5, TYPE_PHONE, "")
239                 )
240         )
241     }
242 }
243