1717import android .os .Handler ;
1818import android .os .PowerManager ;
1919import android .os .SystemClock ;
20+
21+ // Local helper for parsing TRMNL API responses + downloading images.
2022import android .util .Log ;
2123import android .view .MotionEvent ;
2224import android .view .View ;
3739import java .util .Locale ;
3840import java .net .HttpURLConnection ;
3941import java .net .URL ;
40- import java .net .URLDecoder ;
41-
42- import org .json .JSONObject ;
43-
4442import android .graphics .Bitmap ;
4543import android .graphics .BitmapFactory ;
46- import android .graphics .Matrix ;
4744
4845import java .io .File ;
4946import java .io .FileOutputStream ;
@@ -319,6 +316,10 @@ public void onReceive(Context context, Intent intent) {
319316 showGenericImageAndSleep ();
320317 return ;
321318 }
319+ if (fetchInProgress ) {
320+ logD ("alarm: fetch already in progress, skipping" );
321+ return ;
322+ }
322323 // Electric-Sign-style: if we slept with WiFi off, turn it on and wait before fetching
323324 WifiManager wifi = (WifiManager ) a .getSystemService (Context .WIFI_SERVICE );
324325 if (ApiPrefs .isAllowSleep (a ) && wifi != null && !wifi .isWifiEnabled ()
@@ -369,7 +370,7 @@ protected void onResume() {
369370 startFetch ();
370371 }
371372 }
372- scheduleRefresh ();
373+ // Don't schedule here - fetch completion will schedule the next refresh
373374 }
374375 }
375376
@@ -560,6 +561,15 @@ private void setKeepScreenAwake(boolean awake) {
560561 }
561562
562563 /** Schedule alarm to wake and trigger next fetch at (now + millis). */
564+ /** Schedule the next fetch cycle based on allow-sleep setting. */
565+ private void scheduleNextCycle () {
566+ if (ApiPrefs .isAllowSleep (this )) {
567+ scheduleScreensaverThenSleep ();
568+ } else {
569+ scheduleRefresh ();
570+ }
571+ }
572+
563573 private long scheduleReload (long millis ) {
564574 if (alarmManager == null || alarmPendingIntent == null ) return 0 ;
565575 Calendar cal = Calendar .getInstance ();
@@ -807,6 +817,11 @@ private void appendLogLine(String line) {
807817 }
808818
809819 /** Hide boot screen and show normal content */
820+
821+ /** Hide the boot header layout even after bootComplete (used when UI overlaps). */
822+ private void hideBootLayout () {
823+ if (bootLayout != null ) bootLayout .setVisibility (View .GONE );
824+ }
810825 private void hideBootScreen () {
811826 if (bootComplete ) return ;
812827 bootComplete = true ;
@@ -841,6 +856,8 @@ private void showMenu() {
841856
842857 /** Show status text in the dialog (Loading/Connecting/Error); optionally show Next for retry. Keeps image visible. */
843858 private void showMenuStatus (String msg , boolean showNextButton ) {
859+ // Hide boot screen when showing menu status
860+ hideBootLayout ();
844861 if (loadingStatusView != null ) {
845862 loadingStatusView .setText (msg );
846863 loadingStatusView .setVisibility (View .VISIBLE );
@@ -1038,7 +1055,29 @@ protected Object doInBackground(Object[] params) {
10381055 httpsUrl ,
10391056 headers );
10401057 if (bcResult != null && !bcResult .startsWith ("Error:" )) {
1041- ApiResult parsed = (a != null ) ? a .parseResponseAndMaybeFetchImage (bcResult ) : null ;
1058+ ApiResult parsed = null ;
1059+ if (a != null ) {
1060+ final DisplayActivity aFinal = a ;
1061+ TrmnlApiResponseParser .Result r = TrmnlApiResponseParser .parseAndMaybeFetchImage (
1062+ aFinal .getApplicationContext (),
1063+ bcResult ,
1064+ new TrmnlApiResponseParser .Logger () {
1065+ public void logD (String msg ) { aFinal .logD (msg ); }
1066+ public void logW (String msg ) { aFinal .logW (msg ); }
1067+ });
1068+ if (r != null && r .showImage && r .bitmap != null ) {
1069+ if (r .refreshRateSeconds > 0 ) {
1070+ a .updateRefreshRateSeconds (r .refreshRateSeconds );
1071+ }
1072+ parsed = new ApiResult (r .rawText , r .imageUrl , r .bitmap );
1073+ } else {
1074+ // Preserve previous behavior: still allow refresh rate update even if no image
1075+ if (r != null && r .refreshRateSeconds > 0 ) {
1076+ a .updateRefreshRateSeconds (r .refreshRateSeconds );
1077+ }
1078+ parsed = new ApiResult (bcResult );
1079+ }
1080+ }
10421081 return (parsed != null ) ? parsed : new ApiResult (bcResult );
10431082 }
10441083 }
@@ -1162,6 +1201,8 @@ protected void onPostExecute(Object result) {
11621201 a .hideBootScreen ();
11631202 a .imageView .setImageBitmap (ar .bitmap );
11641203 a .lastDisplayedImage = ar .bitmap ;
1204+ // Always write screensaver immediately so TRMNL appears in NOOK's screensaver list
1205+ a .writeScreenshotToScreensaver (ar .bitmap );
11651206 a .imageView .setVisibility (View .VISIBLE );
11661207 if (a .contentScroll != null ) {
11671208 a .contentScroll .setVisibility (View .GONE );
@@ -1176,35 +1217,32 @@ protected void onPostExecute(Object result) {
11761217 a .forceFullRefresh ();
11771218 a .logD ("displayed image" );
11781219 a .logD ("next display in " + (a .refreshMs / 1000L ) + "s" );
1179- if (ApiPrefs .isAllowSleep (a )) {
1180- a .scheduleScreensaverThenSleep ();
1181- } else {
1182- a .scheduleRefresh ();
1183- }
1220+ a .scheduleNextCycle ();
11841221 float v = getBatteryVoltage (a );
11851222 if (v >= 0f ) a .logD ("Battery-Voltage: " + String .format (Locale .US , "%.1f" , v ));
11861223 int rssi = getWifiRssi (a );
11871224 if (rssi != -999 ) a .logD ("rssi: " + rssi );
11881225 return ;
11891226 }
11901227
1228+ // Got API response but no image - show error and schedule retry
11911229 String text = ar .rawText != null ? ar .rawText : "Error: null result" ;
1230+ a .logD ("response body:\n " + text );
1231+ a .logD ("no image in response, will retry" );
11921232 if (fromMenu ) {
1193- a .showMenuStatus (text .length () > 80 ? text .substring (0 , 77 ) + "…" : text , true );
1233+ // User tapped Next - show error in menu dialog, let them retry
1234+ a .showMenuStatus ("No image - tap Next to retry" , true );
1235+ a .forceFullRefresh ();
11941236 } else {
1195- a .contentView .setText (text );
1196- if (a .contentScroll != null ) a .contentScroll .setVisibility (View .VISIBLE );
1197- if (a .imageView != null ) a .imageView .setVisibility (View .GONE );
1198- if (a .logView != null ) a .logView .setVisibility (View .VISIBLE );
1237+ // Background fetch - keep current display, just schedule retry
1238+ a .logD ("next display in " + (a .refreshMs / 1000L ) + "s" );
1239+ }
1240+ // Schedule next refresh (keep trying)
1241+ if (ApiPrefs .isAllowSleep (a )) {
1242+ a .scheduleScreensaverThenSleep ();
1243+ } else {
1244+ a .scheduleRefresh ();
11991245 }
1200- a .forceFullRefresh ();
1201- a .logD ("response body:\n " + text );
1202- a .logD ("displayed response" );
1203- a .logD ("next display in " + (a .refreshMs / 1000L ) + "s" );
1204- float v = getBatteryVoltage (a );
1205- if (v >= 0f ) a .logD ("Battery-Voltage: " + String .format (Locale .US , "%.1f" , v ));
1206- int rssi = getWifiRssi (a );
1207- if (rssi != -999 ) a .logD ("rssi: " + rssi );
12081246 return ;
12091247 }
12101248
@@ -1220,6 +1258,8 @@ protected void onPostExecute(Object result) {
12201258 a .forceFullRefresh ();
12211259 a .logD ("fetch error: " + text );
12221260 a .logD ("next display in " + (a .refreshMs / 1000L ) + "s" );
1261+ // Schedule next refresh even on error (keep trying)
1262+ a .scheduleNextCycle ();
12231263 float v = getBatteryVoltage (a );
12241264 if (v >= 0f ) a .logD ("Battery-Voltage: " + String .format (Locale .US , "%.1f" , v ));
12251265 int rssi = getWifiRssi (a );
@@ -1248,80 +1288,6 @@ private static class ApiResult {
12481288 }
12491289 }
12501290
1251- private ApiResult parseResponseAndMaybeFetchImage (String jsonText ) {
1252- try {
1253- JSONObject obj = new JSONObject (jsonText );
1254- int status = obj .optInt ("status" , -1 );
1255- // API returns 0 for display
1256- if (status != 0 && status != 200 ) {
1257- return new ApiResult (jsonText );
1258- }
1259- logD ("api status: " + status );
1260-
1261- int refreshRateSeconds = obj .optInt ("refresh_rate" , -1 );
1262- if (refreshRateSeconds > 0 ) {
1263- updateRefreshRateSeconds (refreshRateSeconds );
1264- }
1265-
1266- String imageUrl = obj .optString ("image_url" , null );
1267- if (imageUrl == null || imageUrl .length () == 0 ) {
1268- return new ApiResult (jsonText );
1269- }
1270- logD ("api image_url: " + imageUrl );
1271-
1272- // Log a decoded URL for readability, but use the encoded URL for fetch.
1273- try {
1274- String decoded = URLDecoder .decode (imageUrl , "UTF-8" );
1275- logD ("decoded image url: " + decoded );
1276- } catch (Throwable ignored ) {
1277- }
1278-
1279- Hashtable headers = buildImageHeaders ();
1280- byte [] imageBytes = null ;
1281- for (int attempt = 1 ; attempt <= 2 ; attempt ++) {
1282- if (attempt > 1 ) {
1283- logW ("Image fetch attempt " + (attempt -1 ) + " failed - retrying in 3s" );
1284- try { Thread .sleep (3000 ); } catch (InterruptedException ignored ) {}
1285- }
1286- imageBytes = BouncyCastleHttpClient .getHttpsBytes (
1287- getApplicationContext (),
1288- imageUrl ,
1289- headers );
1290- if (imageBytes != null && imageBytes .length > 0 ) break ;
1291- }
1292- if (imageBytes == null || imageBytes .length == 0 ) {
1293- logW ("image fetch failed after retries for url: " + imageUrl );
1294- return new ApiResult ("Error: Failed to download image from " + imageUrl );
1295- }
1296- logD ("image bytes: " + imageBytes .length );
1297-
1298- Bitmap bitmap = BitmapFactory .decodeByteArray (imageBytes , 0 , imageBytes .length );
1299- if (bitmap == null ) {
1300- logW ("image decode failed" );
1301- return new ApiResult (jsonText );
1302- }
1303- if (imageUrl .endsWith ("/empty_state.bmp" )) {
1304- bitmap = rotate90 (bitmap );
1305- }
1306- return new ApiResult (jsonText , imageUrl , bitmap );
1307- } catch (Throwable t ) {
1308- logW ("response parse failed: " + t );
1309- return new ApiResult (jsonText );
1310- }
1311- }
1312-
1313- private Bitmap rotate90 (Bitmap src ) {
1314- try {
1315- Matrix m = new Matrix ();
1316- m .postRotate (90f );
1317- return Bitmap .createBitmap (
1318- src , 0 , 0 , src .getWidth (), src .getHeight (), m , true );
1319- } catch (Throwable t ) {
1320- logW ("image rotate failed: " + t );
1321- return src ;
1322- }
1323- }
1324-
13251291 private static Hashtable buildApiHeaders (String apiId , String apiToken , float batteryVoltage , int rssi ) {
13261292 Hashtable headers = new Hashtable ();
13271293 headers .put ("User-Agent" , "TRMNL-Nook/1.0 (Android 2.1)" );
0 commit comments