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)
44 row.setOnClickFillInIntent(R.id.widget_item_root, fillIn)
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)
71 override fun hasStableIds(): Boolean = true
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list