That fuck shit the fascists are using
at master 265 lines 8.8 kB view raw
1package org.tm.archive.database 2 3import android.content.ContentValues 4import android.content.Context 5import android.database.Cursor 6import android.net.Uri 7import androidx.core.content.contentValuesOf 8import androidx.core.net.toUri 9import org.json.JSONException 10import org.json.JSONObject 11import org.signal.core.util.deleteAll 12import org.signal.core.util.logging.Log 13import org.signal.core.util.readToList 14import org.signal.core.util.requireInt 15import org.signal.core.util.requireLong 16import org.signal.core.util.requireNonNullString 17import org.signal.core.util.requireString 18import org.signal.core.util.select 19import org.signal.core.util.update 20import org.tm.archive.BuildConfig 21import org.tm.archive.database.model.RemoteMegaphoneRecord 22import java.util.concurrent.TimeUnit 23 24/** 25 * Stores remotely configured megaphones. 26 */ 27class RemoteMegaphoneTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) { 28 29 companion object { 30 private val TAG = Log.tag(RemoteMegaphoneTable::class.java) 31 32 private const val TABLE_NAME = "remote_megaphone" 33 private const val ID = "_id" 34 private const val UUID = "uuid" 35 private const val COUNTRIES = "countries" 36 private const val PRIORITY = "priority" 37 private const val MINIMUM_VERSION = "minimum_version" 38 private const val DONT_SHOW_BEFORE = "dont_show_before" 39 private const val DONT_SHOW_AFTER = "dont_show_after" 40 private const val SHOW_FOR_DAYS = "show_for_days" 41 private const val CONDITIONAL_ID = "conditional_id" 42 private const val PRIMARY_ACTION_ID = "primary_action_id" 43 private const val SECONDARY_ACTION_ID = "secondary_action_id" 44 private const val IMAGE_URL = "image_url" 45 private const val IMAGE_BLOB_URI = "image_uri" 46 private const val TITLE = "title" 47 private const val BODY = "body" 48 private const val PRIMARY_ACTION_TEXT = "primary_action_text" 49 private const val SECONDARY_ACTION_TEXT = "secondary_action_text" 50 private const val SHOWN_AT = "shown_at" 51 private const val FINISHED_AT = "finished_at" 52 private const val PRIMARY_ACTION_DATA = "primary_action_data" 53 private const val SECONDARY_ACTION_DATA = "secondary_action_data" 54 private const val SNOOZED_AT = "snoozed_at" 55 private const val SEEN_COUNT = "seen_count" 56 57 val CREATE_TABLE = """ 58 CREATE TABLE $TABLE_NAME ( 59 $ID INTEGER PRIMARY KEY, 60 $UUID TEXT UNIQUE NOT NULL, 61 $PRIORITY INTEGER NOT NULL, 62 $COUNTRIES TEXT, 63 $MINIMUM_VERSION INTEGER NOT NULL, 64 $DONT_SHOW_BEFORE INTEGER NOT NULL, 65 $DONT_SHOW_AFTER INTEGER NOT NULL, 66 $SHOW_FOR_DAYS INTEGER NOT NULL, 67 $CONDITIONAL_ID TEXT, 68 $PRIMARY_ACTION_ID TEXT, 69 $SECONDARY_ACTION_ID TEXT, 70 $IMAGE_URL TEXT, 71 $IMAGE_BLOB_URI TEXT DEFAULT NULL, 72 $TITLE TEXT NOT NULL, 73 $BODY TEXT NOT NULL, 74 $PRIMARY_ACTION_TEXT TEXT, 75 $SECONDARY_ACTION_TEXT TEXT, 76 $SHOWN_AT INTEGER DEFAULT 0, 77 $FINISHED_AT INTEGER DEFAULT 0, 78 $PRIMARY_ACTION_DATA TEXT DEFAULT NULL, 79 $SECONDARY_ACTION_DATA TEXT DEFAULT NULL, 80 $SNOOZED_AT INTEGER DEFAULT 0, 81 $SEEN_COUNT INTEGER DEFAULT 0 82 ) 83 """ 84 85 const val VERSION_FINISHED = Int.MAX_VALUE 86 } 87 88 fun insert(record: RemoteMegaphoneRecord) { 89 writableDatabase.insert(TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, record.toContentValues()) 90 } 91 92 fun update(uuid: String, priority: Long, countries: String?, title: String, body: String, primaryActionText: String?, secondaryActionText: String?) { 93 writableDatabase 94 .update(TABLE_NAME) 95 .values( 96 PRIORITY to priority, 97 COUNTRIES to countries, 98 TITLE to title, 99 BODY to body, 100 PRIMARY_ACTION_TEXT to primaryActionText, 101 SECONDARY_ACTION_TEXT to secondaryActionText 102 ) 103 .where("$UUID = ?", uuid) 104 .run() 105 } 106 107 fun getAll(): List<RemoteMegaphoneRecord> { 108 return readableDatabase 109 .select() 110 .from(TABLE_NAME) 111 .run() 112 .readToList { it.toRemoteMegaphoneRecord() } 113 } 114 115 fun getPotentialMegaphonesAndClearOld(now: Long): List<RemoteMegaphoneRecord> { 116 val records: List<RemoteMegaphoneRecord> = readableDatabase 117 .select() 118 .from(TABLE_NAME) 119 .where("$FINISHED_AT = ? AND $MINIMUM_VERSION <= ? AND ($DONT_SHOW_AFTER > ? AND $DONT_SHOW_BEFORE < ?)", 0, BuildConfig.CANONICAL_VERSION_CODE, now, now) 120 .orderBy("$PRIORITY DESC") 121 .run() 122 .readToList { it.toRemoteMegaphoneRecord() } 123 124 val oldRecords: Set<RemoteMegaphoneRecord> = records 125 .filter { it.shownAt > 0 && it.showForNumberOfDays > 0 } 126 .filter { it.shownAt + TimeUnit.DAYS.toMillis(it.showForNumberOfDays) < now } 127 .toSet() 128 129 for (oldRecord in oldRecords) { 130 clear(oldRecord.uuid) 131 } 132 133 return records - oldRecords 134 } 135 136 fun setImageUri(uuid: String, uri: Uri?) { 137 writableDatabase 138 .update(TABLE_NAME) 139 .values(IMAGE_BLOB_URI to uri?.toString()) 140 .where("$UUID = ?", uuid) 141 .run() 142 } 143 144 fun markShown(uuid: String) { 145 writableDatabase 146 .update(TABLE_NAME) 147 .values(SHOWN_AT to System.currentTimeMillis()) 148 .where("$UUID = ?", uuid) 149 .run() 150 } 151 152 fun markFinished(uuid: String) { 153 writableDatabase 154 .update(TABLE_NAME) 155 .values( 156 IMAGE_URL to null, 157 IMAGE_BLOB_URI to null, 158 FINISHED_AT to System.currentTimeMillis() 159 ) 160 .where("$UUID = ?", uuid) 161 .run() 162 } 163 164 fun snooze(remote: RemoteMegaphoneRecord) { 165 writableDatabase 166 .update(TABLE_NAME) 167 .values( 168 SEEN_COUNT to remote.seenCount + 1, 169 SNOOZED_AT to System.currentTimeMillis() 170 ) 171 .where("$UUID = ?", remote.uuid) 172 .run() 173 } 174 175 fun clearImageUrl(uuid: String) { 176 writableDatabase 177 .update(TABLE_NAME) 178 .values(IMAGE_URL to null) 179 .where("$UUID = ?", uuid) 180 .run() 181 } 182 183 fun clear(uuid: String) { 184 writableDatabase 185 .update(TABLE_NAME) 186 .values( 187 MINIMUM_VERSION to VERSION_FINISHED, 188 IMAGE_URL to null, 189 IMAGE_BLOB_URI to null 190 ) 191 .where("$UUID = ?", uuid) 192 .run() 193 } 194 195 /** Only call from internal settings */ 196 fun debugRemoveAll() { 197 writableDatabase.deleteAll(TABLE_NAME) 198 } 199 200 private fun RemoteMegaphoneRecord.toContentValues(): ContentValues { 201 return contentValuesOf( 202 UUID to uuid, 203 PRIORITY to priority, 204 COUNTRIES to countries, 205 MINIMUM_VERSION to minimumVersion, 206 DONT_SHOW_BEFORE to doNotShowBefore, 207 DONT_SHOW_AFTER to doNotShowAfter, 208 SHOW_FOR_DAYS to showForNumberOfDays, 209 CONDITIONAL_ID to conditionalId, 210 PRIMARY_ACTION_ID to primaryActionId?.id, 211 SECONDARY_ACTION_ID to secondaryActionId?.id, 212 IMAGE_URL to imageUrl, 213 TITLE to title, 214 BODY to body, 215 PRIMARY_ACTION_TEXT to primaryActionText, 216 SECONDARY_ACTION_TEXT to secondaryActionText, 217 FINISHED_AT to finishedAt, 218 PRIMARY_ACTION_DATA to primaryActionData?.toString(), 219 SECONDARY_ACTION_DATA to secondaryActionData?.toString(), 220 SNOOZED_AT to snoozedAt, 221 SEEN_COUNT to seenCount 222 ) 223 } 224 225 private fun Cursor.toRemoteMegaphoneRecord(): RemoteMegaphoneRecord { 226 return RemoteMegaphoneRecord( 227 id = requireLong(ID), 228 uuid = requireNonNullString(UUID), 229 priority = requireLong(PRIORITY), 230 countries = requireString(COUNTRIES), 231 minimumVersion = requireInt(MINIMUM_VERSION), 232 doNotShowBefore = requireLong(DONT_SHOW_BEFORE), 233 doNotShowAfter = requireLong(DONT_SHOW_AFTER), 234 showForNumberOfDays = requireLong(SHOW_FOR_DAYS), 235 conditionalId = requireString(CONDITIONAL_ID), 236 primaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(PRIMARY_ACTION_ID)), 237 secondaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(SECONDARY_ACTION_ID)), 238 imageUrl = requireString(IMAGE_URL), 239 imageUri = requireString(IMAGE_BLOB_URI)?.toUri(), 240 title = requireNonNullString(TITLE), 241 body = requireNonNullString(BODY), 242 primaryActionText = requireString(PRIMARY_ACTION_TEXT), 243 secondaryActionText = requireString(SECONDARY_ACTION_TEXT), 244 shownAt = requireLong(SHOWN_AT), 245 finishedAt = requireLong(FINISHED_AT), 246 primaryActionData = requireString(PRIMARY_ACTION_DATA).parseJsonObject(), 247 secondaryActionData = requireString(SECONDARY_ACTION_DATA).parseJsonObject(), 248 snoozedAt = requireLong(SNOOZED_AT), 249 seenCount = requireInt(SEEN_COUNT) 250 ) 251 } 252 253 private fun String?.parseJsonObject(): JSONObject? { 254 if (this == null) { 255 return null 256 } 257 258 return try { 259 JSONObject(this) 260 } catch (e: JSONException) { 261 Log.w(TAG, "Unable to parse data", e) 262 null 263 } 264 } 265}