git.iko.soy/opencode-android
on d518ef7 help sign in
72 lines 3.7 kB history raw
1package soy.iko.opencode.platform
3import android.content.Context
4import android.content.Intent
5import android.widget.RemoteViews
6import android.widget.RemoteViewsService
7import soy.iko.opencode.MainActivity
8import soy.iko.opencode.R
9import soy.iko.opencode.data.repo.RecentSession
10import soy.iko.opencode.data.repo.RecentSessionsStore
11import soy.iko.opencode.notification.NotificationActionReceiver
12import java.security.MessageDigest
14/** Serves the list rows for [SessionsWidgetProvider] from [RecentSessionsStore]. */
15class SessionsWidgetService : RemoteViewsService() {
16 override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
17 SessionsWidgetFactory(applicationContext)
20private class SessionsWidgetFactory(private val context: Context) : RemoteViewsService.RemoteViewsFactory {
22 private var items: List<RecentSession> = emptyList()
24 override fun onCreate() { items = RecentSessionsStore.read(context) }
26 // Re-read from disk whenever the app signals the data changed (notifyAppWidgetViewDataChanged).
27 override fun onDataSetChanged() { items = RecentSessionsStore.read(context) }
29 override fun onDestroy() { items = emptyList() }
31 override fun getCount(): Int = items.size
33 override fun getViewAt(position: Int): RemoteViews {
34 val row = RemoteViews(context.packageName, R.layout.widget_session_item)
35 val session = items.getOrNull(position) ?: return row
36 row.setTextViewText(R.id.widget_item_title, session.title.ifBlank { context.getString(R.string.session) })
37 // Fill-in intent merged into the list's template: carries the session id the app opens,
38 // plus the profile id of the server that ran the session so it opens under THAT server
39 // even after the user has switched connections — mirrors the notification action path.
40 val fillIn = Intent().putExtra(MainActivity.EXTRA_SESSION_ID, session.id)
41 if (session.profileId.isNotBlank()) {
42 fillIn.putExtra(NotificationActionReceiver.EXTRA_PROFILE_ID, session.profileId)
43 }
44 row.setOnClickFillInIntent(R.id.widget_item_root, fillIn)
45 return row
46 }
48 override fun getLoadingView(): RemoteViews? = null
49 override fun getViewTypeCount(): Int = 1
50 // Derive a stable id from the session id via a collision-resistant SHA-256 digest
51 // (truncated to 63 bits) rather than String.hashCode(), whose 32-bit output can
52 // collide for distinct session ids — with hasStableIds=true the RemoteViews framework
53 // would then mis-recycle two sessions' row content. Mirrors SessionNotifications.notifId.
54 // Falls back to the position for an out-of-range index.
55 override fun getItemId(position: Int): Long {
56 val id = items.getOrNull(position)?.id ?: return position.toLong()
57 val digest = runCatching { MessageDigest.getInstance("SHA-256").digest(id.toByteArray()) }
58 .getOrNull() ?: return position.toLong()
59 // Take the first 8 bytes, mask to 63 bits (always positive) to fit a Long id.
60 // The shl/or/and infix precedence requires parenthesizing each shift individually
61 // (see SessionNotifications.notifId for the rationale).
62 return (((digest[0].toLong() and 0xFF) shl 56) or
63 ((digest[1].toLong() and 0xFF) shl 48) or
64 ((digest[2].toLong() and 0xFF) shl 40) or
65 ((digest[3].toLong() and 0xFF) shl 32) or
66 ((digest[4].toLong() and 0xFF) shl 24) or
67 ((digest[5].toLong() and 0xFF) shl 16) or
68 ((digest[6].toLong() and 0xFF) shl 8) or
69 (digest[7].toLong() and 0xFF)).and(0x7FFFFFFFFFFFFFFF)
70 }
71 override fun hasStableIds(): Boolean = true