tags — parsed via jsoup 1.18.1
| Plain text | Tab or whitespace-delimited lines — the fallback parser |
Each parser returns a ParseResult containing a course list, detected table name, and start date. The parse() function tries each detector in order and returns the first successful parse. ICS time-to-period mapping uses startNode = ((startMin - 480) / 55) + 1 — 8:00 AM = period 1, each 55-minute block maps to one period.
2. University Academic System Reverse Engineering
Chinese universities use wildly different academic systems. Sleepy supports 9 protocol variants across 5 families, implemented across 11 parser classes (6 of which fall back to a shared JwQzParser base).
2.1 Wisedu — the HEU protocol
The hardest protocol to reverse-engineer. HEU uses a modern Wisedu system with a POST endpoint at /jwapp/sys/wdkb/modules/xskcb/xskcb.do. The payload contains 7 key fields: KCM (course name), SKJS (teacher), JASMC (room), SKXQ (day of week), KSJC (start period), JSJC (end period), SKZC (week bitmap).
The week bitmap is a 0-indexed string of 0s and 1s — each character represents one teaching week. The frontend compresses this into weekRuns (single/double week intervals) for display. The login flow uses a WebView that injects a JavaScript bridge (WiseduBridge) and a 5-step fetch chain to extract schedule data, traversing iframe and frame elements to capture the full DOM.
2.2 URP — the JSON injection variant
The new URP protocol injects schedule data into the page as a JavaScript variable: var kbxx_json = {...};. The dateList field contains a 20-bit weekBits string of 0/1 characters, parsed into ranges using a stride-detection algorithm that identifies consecutive 1s in the bitmap.
2.3 Qiangzhi — the workhorse base class
JwQzParser serves as the base for 6 of the 11 parser classes. It parses HTML tables with class="displayTag", using tableName = "kbcontent" as the selector. The node = nodeCount * 2 - 1 formula maps HTML table rows to period numbers. Courses sharing the same time slot are separated by "-----" delimiters within a single table cell.
3. Gold Angle HSL Color Assignment
Each course in Sleepy gets a unique color, assigned automatically through the gold angle algorithm — not a fixed palette, not a random hash. The algorithm in CourseTableView.kt computes:
val hue = (courseId * 137.508) % 360
137.508° is the golden angle — 360 / phi^2. It guarantees that any two courses have a hue separation of at least ~27° for up to 13 courses, and the colors never cluster. The resulting hue is used in an HSL color with lightness and saturation adapted per theme (higher saturation in dark mode, lower in light).
An 8-rule keyword matcher overrides the computed color for well-known courses: English gets blue-green, Military Science gets olive, Physics gets orange, etc. These rules are hardcoded in WidgetContent.kt and apply consistently across all views and widgets. A 6-color hash fallback palette catches any course not matched by keyword rules.
4. Hybrid Widget System: Glance + RemoteViews
Sleepy ships 4 home-screen widgets on a 15-minute WorkManager refresh cycle. Three use Glance (Today, TwoDay, WeekList) — a Compose-like declarative API. The WeekGrid widget uses RemoteViews with manual Canvas Bitmap rendering because Glance 1.1.0 has a known bug: when converting to RemoteViews, the LinearLayout drops children for periods 11 and above.
4.1 WeekGrid Bitmap rendering pipeline
The WeekGridWidgetProvider renders the full weekly grid into a 360x600dp fixed-size Bitmap. The rendering process:
- Query Room for the current week's courses via
ScheduleRepository
- Resolve the active table using
WidgetTableResolver — 3-tier priority: user-set non-default table > default table with courses > any table with courses
- Draw the grid structure: 8 columns (time labels + 7 days), rows per time slot at 52dp cell height
- Place course cards using the same gold-angle color algorithm as the in-app view
- Determine text color (white or black) by checking system brightness via
isDarkOn()
- Wrap the Bitmap in a RemoteViews
ImageView and broadcast APPWIDGET_UPDATE
The WidgetUpdater must refresh both Glance widgets (via .update()) and the RemoteViews widget (via broadcast) — missing either path leaves one widget silently stale. A PinWidgetActivity handles the Android 12+ widget pinning flow for first-time setup.
5. SmartPeriodConfig: Auto-Derivation Engine
Manual time slot configuration is tedious: 12 periods × 2 time fields = 24 form fields. The SmartPeriodConfig system automates this with 4 parameters:
- Total periods — how many periods per day
- First period start — e.g. "08:00"
- Period duration — minutes per period (e.g. 45)
- Break duration — default minutes between periods (e.g. 5)
Plus optional specific breaks — longer gaps after certain periods (e.g. 15 minutes after period 4 for lunch). The engine computes all start times and end times sequentially, then serializes the result to TimeTableEntity.smartConfigJson as a JSON string for persistence. The TimeSlotEditor composable switches between Manual and Auto modes via a top tab bar.
6. CourseTableLayout: Box-with-Offset Grid
The week grid view was the most challenging Compose layout. The naive approach — a LazyColumn with row-based measuring — collapsed to a single cell because fillMaxSize() made content height equal to viewport height, leaving nothing to scroll.
The solution: CardsGridView wraps a BoxWithConstraints and uses absolute Modifier.offset() positioning for each course card. The grid structure is pre-computed from 5 layout constants (header height, time column width, slot height, row gap, column gap) and the actual cell dimensions are derived from BoxWithConstraints width divided by 8 columns.
Course cards that span multiple periods are rendered as taller rectangles with proportional padding. This avoids the Compose measurement pass collapsing the layout, at the cost of manual coordinate math for every card position.
7. Room Schema Design
The database has two entities and a JSON-backed config:
- CourseEntity (23 fields): course name, teacher, room, note, startWeek, endWeek, dayOfWeek, startNode, step, ownTime flag, startTime, endTime, type (1=odd-week / 2=even-week / 0=every-week), color, groupId (UUID for batch operations), tableId (foreign key)
- TimeTableEntity (8 fields): name, startDate, maxWeek, timeJson (12-period schedule as JSON array), smartConfigJson, isDefault flag
The groupId field on CourseEntity is a java.util.UUID.randomUUID().toString() — not derived from course name. This allows ScheduleRepository.getGroupCourses() to load all related courses for batch editing without relying on name collisions.
8. Import Pipeline: Preview, Conflict Detection, Apply
The import flow has three stages:
- Parse:
ScheduleParser.parse() → ParseResult with courses, table name, start date
- Preview:
buildImportPreview() runs conflict detection — comparing incoming course time slots against existing courses. Returns ImportPreview with incomingCount, conflictCount, and cleanCount metrics
- Apply: Three
ImportApplyMode options — ReplaceCurrent (clear all, insert new), ImportAsNew (create separate table), AppendNonConflict (skip time-conflicting courses)
Conflict detection uses a time range overlap check: coursesConflict(a, b) compares day-of-week intersection and minute-range overlap for each course pair. The preview dialog shows 3 metric cards (courses / conflicts / appendable) color-coded: red for conflicts, green for clean imports.
Open Source
Sleepy is GPL-3.0, 21 releases, supporting 3 ABIs (arm64-v8a / armeabi-v7a / x86_64). Available on GitHub Releases. No Play Store, no ads, no tracking.
Star on GitHub · Download APK
See also: Sleepy Operation Docs — step-by-step user guide for installation, import, widgets, themes, and troubleshooting.
← cd .. /