1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
package de.profpatsch.homescreen
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
/**
* Everything the launcher remembers.
*
* Which applications exist comes from LauncherApps and which have been used
* recently comes from UsageStatsManager; both are the system's to answer. The
* only state that is ours is which applications have been pinned, so that is
* all this holds.
*
* SQLite is in the platform (android.database.sqlite, API 1) and needs no
* library: Room is a code generator over these same classes, and would have to
* be unpacked from an .aar by hand. See homescreen.1, CAVEATS.
*
* Migrations are numbered and applied in order, exactly once. Never modify a
* migration that has been applied; add another one. This is the same discipline
* as the Go projects in this repository, for the same reason: the statements
* below have already run against a database that still exists.
*/
class Db(context: Context) : SQLiteOpenHelper(context, NAME, null, VERSION) {
companion object {
private const val NAME = "homescreen.db"
/** Bump when adding a migration, and add it to [MIGRATIONS]. */
private const val VERSION = 1
private val MIGRATIONS: List<String> = listOf(
"""
CREATE TABLE schema_version (
version INTEGER PRIMARY KEY,
applied_at INTEGER NOT NULL
);
-- One row per pinned application.
--
-- component is ComponentName.flattenToString(), which names the
-- activity rather than the package: an application can publish more
-- than one launcher entry, and pinning one of them should not pin
-- its siblings.
--
-- added_at is when it was pinned. The grid does not sort by it --
-- pinned applications are listed alphabetically, so that pinning a
-- new one never moves the others -- but it is free to record and it
-- is the only way to answer when something was pinned.
CREATE TABLE favourite (
component TEXT PRIMARY KEY,
added_at INTEGER NOT NULL
);
""",
)
}
override fun onCreate(db: SQLiteDatabase) {
applyMigrations(db, from = 0)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
applyMigrations(db, from = oldVersion)
}
private fun applyMigrations(db: SQLiteDatabase, from: Int) {
for (i in from until MIGRATIONS.size) {
// execSQL runs one statement, so a migration containing several has
// to be split. Blank fragments come from the trailing semicolon.
for (statement in MIGRATIONS[i].split(";")) {
if (statement.isNotBlank()) db.execSQL(statement)
}
db.insertWithOnConflict(
"schema_version",
null,
ContentValues().apply {
put("version", i + 1)
put("applied_at", System.currentTimeMillis())
},
SQLiteDatabase.CONFLICT_REPLACE,
)
}
}
/** The pinned activities, as flattened ComponentNames. */
fun favourites(): Set<String> {
val out = mutableSetOf<String>()
readableDatabase.rawQuery("SELECT component FROM favourite", null).use { c ->
while (c.moveToNext()) out.add(c.getString(0))
}
return out
}
fun addFavourite(component: String) {
writableDatabase.insertWithOnConflict(
"favourite",
null,
ContentValues().apply {
put("component", component)
put("added_at", System.currentTimeMillis())
},
// Pinning something already pinned is not an error, and must not
// overwrite the original added_at.
SQLiteDatabase.CONFLICT_IGNORE,
)
}
fun removeFavourite(component: String) {
writableDatabase.delete("favourite", "component = ?", arrayOf(component))
}
}
|