Skip to content

Commit 6362f53

Browse files
committed
Merge fix-refresh-reliability
2 parents aaf7549 + 157c833 commit 6362f53

5 files changed

Lines changed: 200 additions & 117 deletions

File tree

.mux/init

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ set -e
44
# Symlink untracked files from main repo for worktrees
55
MAIN_REPO="/home/benpotter/workspace/trmnl-nook-simple-touch"
66

7-
if [ -f "$MAIN_REPO/local.properties" ]; then
8-
ln -sf "$MAIN_REPO/local.properties" local.properties
9-
echo "Linked local.properties"
10-
fi
7+
for props in local.properties project.properties; do
8+
if [ -f "$MAIN_REPO/$props" ]; then
9+
ln -sf "$MAIN_REPO/$props" "$props"
10+
echo "Linked $props"
11+
fi
12+
done
1113

1214
for jar in "$MAIN_REPO"/libs/*.jar; do
1315
if [ -f "$jar" ]; then

project.properties

Lines changed: 0 additions & 14 deletions
This file was deleted.

project.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/home/benpotter/workspace/trmnl-nook-simple-touch/project.properties

src/com/bpmct/trmnl_nook_simple_touch/DisplayActivity.java

Lines changed: 65 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import android.os.Handler;
1818
import android.os.PowerManager;
1919
import android.os.SystemClock;
20+
21+
// Local helper for parsing TRMNL API responses + downloading images.
2022
import android.util.Log;
2123
import android.view.MotionEvent;
2224
import android.view.View;
@@ -37,13 +39,8 @@
3739
import java.util.Locale;
3840
import java.net.HttpURLConnection;
3941
import java.net.URL;
40-
import java.net.URLDecoder;
41-
42-
import org.json.JSONObject;
43-
4442
import android.graphics.Bitmap;
4543
import android.graphics.BitmapFactory;
46-
import android.graphics.Matrix;
4744

4845
import java.io.File;
4946
import 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

Comments
 (0)