refactor: adopt androidx.core KTX helpers (Bitmap/Uri/SharedPreferences)

This commit is contained in:
davotoula
2026-07-11 17:00:40 +01:00
parent 56e4b1ec7d
commit 0053edf2dd
18 changed files with 63 additions and 29 deletions
+20
View File
@@ -282,6 +282,26 @@ Do this before considering the task complete.
- The only acceptable inline fully-qualified names are: a genuine name
collision (prefer `import ... as Alias` instead), or where the language
requires it. Comments, KDoc, and string literals are exempt.
- **Prefer the `androidx.core` KTX extension over the raw platform Java call**
when one exists — this is what Android Lint's `UseKtx` flags. Common swaps:
`Bitmap.createBitmap(w, h, cfg)``createBitmap(w, h)`,
`Bitmap.createScaledBitmap(src, w, h, f)``src.scale(w, h, f)`,
`Uri.parse(s)``s.toUri()`, and `prefs.edit()…apply()``prefs.edit { }`.
Only adopt the KTX form when it's behaviour-preserving: keep any explicit
argument that differs from the extension's default (a non-`ARGB_8888`
`Bitmap.Config`, `scale(filter = false)`), and leave calls the KTX has no
equivalent for (e.g. the `createBitmap` pixels/matrix overloads, or a
conditional-`apply()` editor loop) untouched.
- **This "prefer the KTX sugar" rule does NOT extend to collection operators.**
The KTX preference is about platform wrappers (`Bitmap`/`Uri`/`SharedPreferences`),
which compile to the identical call. Collections are the opposite: in hot
event/parse paths Quartz deliberately uses raw JVM arrays (`TagArray =
Array<Array<String>>`) and the inline `fast*` operators (`fastForEach`,
`fastAny`, `fastFirstOrNull`, `fastFirstNotNullOfOrNull`, … in
`nip01Core/core/TagArray.kt`) instead of Kotlin `List` + stdlib
`forEach`/`map`/`filter`/`any` — the `fast*` variants allocate no iterator,
no intermediate list, and no lambda object. Don't "modernize" those into
stdlib collection calls; match the surrounding hot-path style.
### Navigation Shell
- **Desktop**: Sidebar + main content area
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst
import android.graphics.Bitmap
import android.graphics.Color
import androidx.core.graphics.createBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.AccountSettings
@@ -81,7 +82,7 @@ class ImageUploadTesting {
.build()
private fun getBitmap(): ByteArray {
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(200, 300)
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
bitmap.setPixel(x, y, Color.rgb(Random.nextInt(), Random.nextInt(), Random.nextInt()))
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.images
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
@@ -44,7 +45,7 @@ class ThumbnailDiskCacheInstrumentedTest {
cacheDir = File(appContext.cacheDir, "thumbnail-test-${UUID.randomUUID()}")
cache = ThumbnailDiskCache(cacheDir)
sourceFile = File(appContext.cacheDir, "source-${UUID.randomUUID()}.jpg")
val bitmap = Bitmap.createBitmap(64, 64, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(64, 64)
sourceFile.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
bitmap.recycle()
}
@@ -32,6 +32,7 @@ import android.os.Messenger
import android.os.RemoteException
import android.os.SystemClock
import android.util.Log
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
import com.vitorpamplona.amethyst.commons.napplet.NappletBroker
@@ -359,7 +360,7 @@ class NappletBrokerService : Service() {
val intent =
Intent(applicationContext, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.parse("nostr:connectedapp?coordinate=" + Uri.encode(coordinate))
data = ("nostr:connectedapp?coordinate=" + Uri.encode(coordinate)).toUri()
}
runCatching { applicationContext.startActivity(intent) }
.onFailure { Log.w("NappletBrokerService", "Could not open Connected Apps detail", it) }
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.napplet
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
@@ -86,7 +86,7 @@ object WebAppNetworkRegistry {
}
/** The host key for [url] (e.g. `vitorpamplona.com`), or the raw string if it has no host. */
fun hostKeyOf(url: String): String = runCatching { Uri.parse(url).host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url
fun hostKeyOf(url: String): String = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url
/** Whether the site behind [url] routes through Tor. Defaults to true (Tor) for any site never set. */
fun useTor(url: String): Boolean = modes[hostKeyOf(url)] ?: true
@@ -27,6 +27,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.stringRes
@@ -64,7 +65,7 @@ object CalendarReminderNotifier {
val tapIntent =
Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = android.net.Uri.parse(deepLink)
data = deepLink.toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val tapPendingIntent =
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.calendar
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
/**
* Device-wide preferences for the calendar reminder worker.
@@ -40,13 +41,13 @@ class CalendarReminderPrefs(
fun isEnabled(): Boolean = prefs.getBoolean(KEY_ENABLED, DEFAULT_ENABLED)
fun setEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_ENABLED, enabled).apply()
prefs.edit { putBoolean(KEY_ENABLED, enabled) }
}
fun leadMinutes(): Int = prefs.getInt(KEY_LEAD_MINUTES, DEFAULT_LEAD_MINUTES)
fun setLeadMinutes(minutes: Int) {
prefs.edit().putInt(KEY_LEAD_MINUTES, minutes).apply()
prefs.edit { putInt(KEY_LEAD_MINUTES, minutes) }
}
companion object {
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.calendar
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
/**
* Persistent "I've already notified for this event" set. Backed by [SharedPreferences] because
@@ -54,7 +55,7 @@ class CalendarReminderStore(
eventId: String,
eventStartSeconds: Long,
) {
prefs.edit().putLong(keyFor(eventId), eventStartSeconds).apply()
prefs.edit { putLong(keyFor(eventId), eventStartSeconds) }
}
/**
@@ -25,6 +25,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import androidx.core.graphics.createBitmap
import kotlin.math.roundToInt
/**
@@ -57,7 +58,7 @@ fun roadEventPinBitmap(
val cx = width / 2f
val cy = pad + radius
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap)
// Head circle + pointer drawn as a single path so they share one shadow
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note.types
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -46,6 +45,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.stringRes
@@ -160,8 +160,7 @@ private fun Ps1SaveIconImage(icon: Ps1SaveIcon) {
val frames =
remember(icon) {
icon.frames.map { pixels ->
Bitmap
.createBitmap(Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE, Bitmap.Config.ARGB_8888)
createBitmap(Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE)
.apply { setPixels(pixels, 0, Ps1SaveIcon.SIZE, 0, 0, Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE) }
.asImageBitmap()
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.browser
import android.net.Uri
import android.os.Build
import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
@@ -44,6 +43,7 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
@@ -193,14 +193,14 @@ private fun EmbeddedWebAppTab(
}
/** The host of [url] for the tab title, falling back to the raw string. */
internal fun hostLabel(url: String): String = runCatching { Uri.parse(url).host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url
internal fun hostLabel(url: String): String = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url
/**
* The `scheme://host[:port]` origin of [url] — the exact form the sandbox reports for NIP-07 consent, so
* it matches the `browser:<origin>` permission-ledger key. Null when [url] has no usable scheme/host.
*/
internal fun browserOrigin(url: String): String? {
val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return null
val uri = runCatching { url.toUri() }.getOrNull() ?: return null
val scheme = uri.scheme?.takeIf { it.isNotBlank() } ?: return null
val host = uri.host?.takeIf { it.isNotBlank() } ?: return null
return "$scheme://$host" + if (uri.port > 0) ":${uri.port}" else ""
@@ -59,6 +59,7 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -582,11 +583,11 @@ private fun LocationRow(location: String) {
val trimmed = location.trim()
val intent =
if (isUrl) {
Intent(Intent.ACTION_VIEW, Uri.parse(trimmed))
Intent(Intent.ACTION_VIEW, trimmed.toUri())
} else {
// `geo:0,0?q=<location>` is the Android geo intent; the user's
// installed maps app handles it.
Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=${Uri.encode(trimmed)}"))
Intent(Intent.ACTION_VIEW, "geo:0,0?q=${Uri.encode(trimmed)}".toUri())
}
// runCatching swallows ActivityNotFoundException when no handler is
// installed — we don't have anywhere useful to fall back to.
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.commons.blurhash
import android.graphics.Bitmap
import androidx.core.graphics.scale
actual class PlatformImage(
val bitmap: Bitmap,
@@ -43,7 +44,7 @@ actual class PlatformImage(
actual fun scale(
width: Int,
height: Int,
): PlatformImage = PlatformImage(Bitmap.createScaledBitmap(bitmap, width, height, false))
): PlatformImage = PlatformImage(bitmap.scale(width, height, false))
actual companion object {
actual fun create(
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.commons.keystorage
import android.content.Context
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.Dispatchers
@@ -86,7 +87,7 @@ actual class SecureKeyStorage private actual constructor() {
) {
withContext(Dispatchers.IO) {
try {
encryptedPrefs.edit().putString(KEY_PREFIX + npub, privKeyHex).apply()
encryptedPrefs.edit { putString(KEY_PREFIX + npub, privKeyHex) }
} catch (e: Exception) {
throw SecureStorageException("Failed to save private key", e)
}
@@ -108,7 +109,7 @@ actual class SecureKeyStorage private actual constructor() {
val key = KEY_PREFIX + npub
val existed = encryptedPrefs.contains(key)
if (existed) {
encryptedPrefs.edit().remove(key).apply()
encryptedPrefs.edit { remove(key) }
}
existed
} catch (e: Exception) {
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.nip64Chess
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
actual class ChessDismissedGamesStorage private actual constructor() {
private var prefs: SharedPreferences? = null
@@ -48,6 +49,6 @@ actual class ChessDismissedGamesStorage private actual constructor() {
userPubkey: String,
ids: Set<String>,
) {
prefs?.edit()?.putStringSet(prefsKey(userPubkey), ids)?.apply()
prefs?.edit { putStringSet(prefsKey(userPubkey), ids) }
}
}
@@ -54,6 +54,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.graphics.scale
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.webkit.JavaScriptReplyProxy
@@ -456,7 +458,7 @@ class NappletBrowserActivity : ComponentActivity() {
runCatching {
val scaled =
if (icon.width > ICON_MAX_PX || icon.height > ICON_MAX_PX) {
Bitmap.createScaledBitmap(icon, ICON_MAX_PX, ICON_MAX_PX, true)
icon.scale(ICON_MAX_PX, ICON_MAX_PX)
} else {
icon
}
@@ -597,7 +599,7 @@ class NappletBrowserActivity : ComponentActivity() {
// Key the persisted choice on the host actually displayed (which may differ from startUrl after
// in-page navigation), so the preference sticks to the right site.
val liveUrl = if (this::webView.isInitialized) webView.url ?: startUrl else startUrl
val host = runCatching { Uri.parse(liveUrl).host }.getOrNull()?.takeIf { it.isNotBlank() } ?: return
val host = runCatching { liveUrl.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: return
val msg =
Message.obtain(null, NappletIpc.MSG_SET_WEB_TOR).apply {
data =
@@ -613,7 +615,7 @@ class NappletBrowserActivity : ComponentActivity() {
private var title: String = ""
private fun barTitle(): String = title.ifBlank { runCatching { Uri.parse(startUrl).host }.getOrNull() ?: getString(CommonsR.string.napplet_untitled) }
private fun barTitle(): String = title.ifBlank { runCatching { startUrl.toUri().host }.getOrNull() ?: getString(CommonsR.string.napplet_untitled) }
/**
* The top pull-down sheet: a small grabber at the top edge (out of the corner where a site shows its
@@ -644,7 +646,7 @@ class NappletBrowserActivity : ComponentActivity() {
*/
private fun openPermissions() {
val liveUrl = if (this::webView.isInitialized) webView.url ?: startUrl else startUrl
val uri = runCatching { Uri.parse(liveUrl) }.getOrNull() ?: return
val uri = runCatching { liveUrl.toUri() }.getOrNull() ?: return
val scheme = uri.scheme?.takeIf { it.isNotBlank() } ?: return
val host = uri.host?.takeIf { it.isNotBlank() } ?: return
val origin = "$scheme://$host" + if (uri.port > 0) ":${uri.port}" else ""
@@ -774,6 +776,6 @@ class NappletBrowserActivity : ComponentActivity() {
.putExtra(EXTRA_THEME, theme)
.putExtra(EXTRA_IS_FAVORITE, isFavorite)
// Distinct task identity per URL for documentLaunchMode=intoExisting.
.setData(Uri.parse(url))
.setData(url.toUri())
}
}
@@ -45,6 +45,7 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import androidx.core.graphics.createBitmap
import androidx.privacysandbox.ui.provider.toCoreLibInfo
import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.WebMessageCompat
@@ -206,7 +207,7 @@ class NappletBrowserService : Service() {
val outW = (boxW * zoom).toInt().coerceAtLeast(1)
val outH = (boxH * zoom).toInt().coerceAtLeast(1)
val t0 = SystemClock.elapsedRealtimeNanos()
val bitmap = Bitmap.createBitmap(outW, outH, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(outW, outH)
val canvas = Canvas(bitmap)
canvas.drawColor(tab.bgColor)
// Map the source rect (centered on cx,cy in view px) into the zoomed output bitmap.
@@ -45,6 +45,7 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import androidx.core.graphics.createBitmap
import androidx.privacysandbox.ui.provider.toCoreLibInfo
import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.ProxyConfig
@@ -238,7 +239,7 @@ class NappletHostService : Service() {
val outW = (boxW * zoom).toInt().coerceAtLeast(1)
val outH = (boxH * zoom).toInt().coerceAtLeast(1)
val t0 = SystemClock.elapsedRealtimeNanos()
val bitmap = Bitmap.createBitmap(outW, outH, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(outW, outH)
val canvas = Canvas(bitmap)
canvas.drawColor(tab.bgColor)
canvas.scale(zoom, zoom)