Skip to content

Commit c0f4059

Browse files
committed
Add Gift Mode feature for BYOD gifting
- New GiftModeSettingsActivity for configuring gift mode (names, device code) - Gift mode display shows setup instructions for recipient - Gift screensaver uses generic_display with 'tap home button' hint - Auto-disable gift mode when credentials are saved - Add Developer Perks hint to Settings and Credentials pages - Settings UI improvements: grey button style, single-column layout - Make NOOK_IP configurable via environment variable
1 parent 8ae5c98 commit c0f4059

9 files changed

Lines changed: 589 additions & 108 deletions

File tree

AGENTS/build-tooling.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,21 @@ When working in a git worktree, you need:
7676
## Release flow
7777

7878
Release guidance now lives in `AGENTS/release.md`.
79+
80+
## Image Generation with ImageMagick
81+
82+
For generating static images (e.g., screensavers), use ImageMagick `convert`:
83+
84+
```bash
85+
# Example: Add text to existing image and rotate for NOOK screensaver
86+
convert res/drawable-mdpi/generic_display.jpg \
87+
-gravity South -pointsize 16 -fill 'gray50' -font DejaVu-Sans \
88+
-annotate +0+30 "Your text here" \
89+
-rotate 90 \
90+
res/drawable-mdpi/output.png
91+
```
92+
93+
Key notes:
94+
- Use `-font DejaVu-Sans` to avoid italic text (default can be italic)
95+
- NOOK screensaver is native portrait (600x800), so rotate landscape images 90° CW
96+
- Use `gray50` for subtle text matching app UI

AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
android:configChanges="orientation|keyboardHidden|screenSize"
4141
android:label="Credentials"
4242
android:theme="@style/FullscreenTheme" />
43+
<activity
44+
android:name=".GiftModeSettingsActivity"
45+
android:configChanges="orientation|keyboardHidden|screenSize"
46+
android:label="Gift Mode Settings"
47+
android:theme="@style/FullscreenTheme" />
4348
</application>
4449

4550
</manifest>
5.97 KB
Loading

src/com/bpmct/trmnl_nook_simple_touch/ApiPrefs.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ public class ApiPrefs {
1111
private static final String DEFAULT_API_BASE_URL = "https://usetrmnl.com/api";
1212
private static final String KEY_ALLOW_SLEEP = "allow_sleep";
1313
private static final String KEY_FILE_LOGGING = "file_logging";
14+
private static final String KEY_GIFT_MODE = "gift_mode";
15+
private static final String KEY_FRIENDLY_DEVICE_CODE = "friendly_device_code";
16+
private static final String KEY_GIFT_FROM_NAME = "gift_from_name";
17+
private static final String KEY_GIFT_TO_NAME = "gift_to_name";
1418
private static final String SCREENSAVER_PATH = "/media/screensavers/TRMNL/display.png";
1519

1620
public static boolean hasCredentials(Context context) {
@@ -91,7 +95,7 @@ private static String normalizeBaseUrl(String baseUrl, String defaultBaseUrl) {
9195
/** Whether the device may sleep between display updates (Electric-Sign-style). Default true. */
9296
public static boolean isAllowSleep(Context context) {
9397
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
94-
return prefs.getBoolean(KEY_ALLOW_SLEEP, true);
98+
return prefs.getBoolean(KEY_ALLOW_SLEEP, false);
9599
}
96100

97101
public static void setAllowSleep(Context context, boolean allow) {
@@ -113,4 +117,50 @@ public static void setFileLoggingEnabled(Context context, boolean enabled) {
113117
public static String getScreensaverPath() {
114118
return SCREENSAVER_PATH;
115119
}
120+
121+
public static boolean isGiftModeEnabled(Context context) {
122+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
123+
return prefs.getBoolean(KEY_GIFT_MODE, false);
124+
}
125+
126+
public static void setGiftModeEnabled(Context context, boolean enabled) {
127+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
128+
.putBoolean(KEY_GIFT_MODE, enabled).commit();
129+
}
130+
131+
public static String getFriendlyDeviceCode(Context context) {
132+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
133+
String value = prefs.getString(KEY_FRIENDLY_DEVICE_CODE, null);
134+
if (value == null || value.trim().length() == 0) return null;
135+
return value.trim();
136+
}
137+
138+
public static void saveFriendlyDeviceCode(Context context, String code) {
139+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
140+
.putString(KEY_FRIENDLY_DEVICE_CODE, code != null ? code.trim() : "").commit();
141+
}
142+
143+
public static String getGiftFromName(Context context) {
144+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
145+
String value = prefs.getString(KEY_GIFT_FROM_NAME, null);
146+
if (value == null || value.trim().length() == 0) return null;
147+
return value.trim();
148+
}
149+
150+
public static void saveGiftFromName(Context context, String name) {
151+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
152+
.putString(KEY_GIFT_FROM_NAME, name != null ? name.trim() : "").commit();
153+
}
154+
155+
public static String getGiftToName(Context context) {
156+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
157+
String value = prefs.getString(KEY_GIFT_TO_NAME, null);
158+
if (value == null || value.trim().length() == 0) return null;
159+
return value.trim();
160+
}
161+
162+
public static void saveGiftToName(Context context, String name) {
163+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
164+
.putString(KEY_GIFT_TO_NAME, name != null ? name.trim() : "").commit();
165+
}
116166
}

src/com/bpmct/trmnl_nook_simple_touch/CredentialsActivity.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ protected void onCreate(Bundle savedInstanceState) {
5959
ViewGroup.LayoutParams.WRAP_CONTENT);
6060
inner.addView(baseUrlInput, baseUrlParams);
6161

62+
// Show hint about where to find credentials if using default URL
63+
String currentBaseUrl = ApiPrefs.getApiBaseUrl(this);
64+
if (currentBaseUrl != null && currentBaseUrl.contains("usetrmnl.com")) {
65+
TextView credHint = new TextView(this);
66+
credHint.setText("Find credentials in Device Settings → Developer Perks on trmnl.com");
67+
credHint.setTextSize(11);
68+
credHint.setTextColor(0xFF888888);
69+
LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
70+
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
71+
hintParams.topMargin = 8;
72+
inner.addView(credHint, hintParams);
73+
}
74+
6275
TextView idLabel = new TextView(this);
6376
idLabel.setText("Device ID (MAC Address)");
6477
idLabel.setTextSize(14);
@@ -156,6 +169,8 @@ public void onClick(View v) {
156169
}
157170
ApiPrefs.saveCredentials(CredentialsActivity.this, id, token);
158171
ApiPrefs.saveApiBaseUrl(CredentialsActivity.this, baseUrl);
172+
// Auto-disable gift mode when credentials are saved
173+
ApiPrefs.setGiftModeEnabled(CredentialsActivity.this, false);
159174
statusView.setText("Saved.");
160175
android.content.Intent intent = new android.content.Intent(CredentialsActivity.this, DisplayActivity.class);
161176
intent.putExtra(DisplayActivity.EXTRA_CLEAR_IMAGE, true);

src/com/bpmct/trmnl_nook_simple_touch/DisplayActivity.java

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,9 @@ protected void onResume() {
369369
boolean wifiJustOn = ensureWifiOnWhenForeground();
370370

371371
applyIntentState(getIntent());
372-
if (USE_GENERIC_IMAGE) {
372+
if (ApiPrefs.isGiftModeEnabled(this)) {
373+
showGiftModeScreen();
374+
} else if (USE_GENERIC_IMAGE) {
373375
showGenericImageAndSleep();
374376
} else if (ensureCredentials()) {
375377
if (!fetchInProgress) {
@@ -858,6 +860,155 @@ private void setBootStatus(String status) {
858860
}
859861
}
860862

863+
private void showGiftModeScreen() {
864+
hideBootScreen();
865+
if (bootLayout != null) bootLayout.setVisibility(View.GONE);
866+
if (logView != null) logView.setVisibility(View.GONE);
867+
if (imageRotateLayout != null) imageRotateLayout.setVisibility(View.GONE);
868+
if (contentScroll != null) contentScroll.setVisibility(View.VISIBLE);
869+
870+
String code = ApiPrefs.getFriendlyDeviceCode(this);
871+
String fromName = ApiPrefs.getGiftFromName(this);
872+
String toName = ApiPrefs.getGiftToName(this);
873+
874+
// Build a left-aligned layout
875+
ScrollView.LayoutParams scrollParams = new ScrollView.LayoutParams(
876+
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT);
877+
878+
LinearLayout giftLayout = new LinearLayout(this);
879+
giftLayout.setOrientation(LinearLayout.VERTICAL);
880+
giftLayout.setGravity(Gravity.CENTER_VERTICAL);
881+
giftLayout.setPadding(50, 40, 50, 40);
882+
giftLayout.setLayoutParams(scrollParams);
883+
884+
// Logo + Title row
885+
LinearLayout headerRow = new LinearLayout(this);
886+
headerRow.setOrientation(LinearLayout.HORIZONTAL);
887+
headerRow.setGravity(Gravity.CENTER_VERTICAL);
888+
889+
ImageView logo = new ImageView(this);
890+
logo.setImageResource(R.drawable.ic_launcher);
891+
headerRow.addView(logo);
892+
893+
TextView title = new TextView(this);
894+
title.setText("TRMNL");
895+
title.setTextSize(22);
896+
title.setTextColor(0xFF000000);
897+
title.setPadding(12, 0, 0, 0);
898+
headerRow.addView(title);
899+
900+
giftLayout.addView(headerRow);
901+
902+
// Greeting
903+
StringBuilder greeting = new StringBuilder();
904+
if (toName != null && toName.length() > 0) {
905+
greeting.append("Hey ").append(toName).append("! ");
906+
}
907+
if (fromName != null && fromName.length() > 0) {
908+
greeting.append(fromName).append(" gifted you this display!");
909+
} else {
910+
greeting.append("This display was gifted to you!");
911+
}
912+
913+
TextView greetingView = new TextView(this);
914+
greetingView.setText(greeting.toString());
915+
greetingView.setTextSize(14);
916+
greetingView.setTextColor(0xFF000000);
917+
LinearLayout.LayoutParams greetParams = new LinearLayout.LayoutParams(
918+
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
919+
greetParams.topMargin = 16;
920+
giftLayout.addView(greetingView, greetParams);
921+
922+
// Description
923+
TextView desc = new TextView(this);
924+
desc.setText("Use it for weather, calendars, news, or hundreds of other plugins from trmnl.com");
925+
desc.setTextSize(12);
926+
desc.setTextColor(0xFF666666);
927+
LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(
928+
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
929+
descParams.topMargin = 6;
930+
giftLayout.addView(desc, descParams);
931+
932+
// Divider line
933+
View divider = new View(this);
934+
divider.setBackgroundColor(0xFFCCCCCC);
935+
LinearLayout.LayoutParams divParams = new LinearLayout.LayoutParams(
936+
ViewGroup.LayoutParams.FILL_PARENT, 1);
937+
divParams.topMargin = 20;
938+
giftLayout.addView(divider, divParams);
939+
940+
// Setup steps title
941+
TextView stepsTitle = new TextView(this);
942+
stepsTitle.setText("SETUP");
943+
stepsTitle.setTextSize(11);
944+
stepsTitle.setTextColor(0xFF888888);
945+
LinearLayout.LayoutParams stepsTitleParams = new LinearLayout.LayoutParams(
946+
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
947+
stepsTitleParams.topMargin = 16;
948+
giftLayout.addView(stepsTitle, stepsTitleParams);
949+
950+
// Step 1
951+
giftLayout.addView(createStepRow("1", "Sign up at trmnl.com/signup"), createStepParams(12));
952+
953+
// Step 2
954+
String step2Text = (code != null && code.length() > 0)
955+
? "Add device with code: " + code
956+
: "Add device (get code from gifter)";
957+
giftLayout.addView(createStepRow("2", step2Text), createStepParams(8));
958+
959+
// Step 3
960+
giftLayout.addView(createStepRow("3", "Tap screen → Settings → Edit"), createStepParams(8));
961+
962+
// Replace contentView's parent contents
963+
if (contentScroll != null) {
964+
contentScroll.removeAllViews();
965+
contentScroll.setFillViewport(true);
966+
contentScroll.addView(giftLayout);
967+
}
968+
969+
// Write gift mode screen to screensaver so NOOK shows it when asleep
970+
writeGiftModeScreensaver(code, fromName, toName);
971+
}
972+
973+
private LinearLayout createStepRow(String number, String text) {
974+
LinearLayout row = new LinearLayout(this);
975+
row.setOrientation(LinearLayout.HORIZONTAL);
976+
977+
TextView numView = new TextView(this);
978+
numView.setText(number);
979+
numView.setTextSize(16);
980+
numView.setTextColor(0xFF000000);
981+
numView.setGravity(Gravity.CENTER);
982+
numView.setBackgroundColor(0xFFEEEEEE);
983+
numView.setPadding(12, 4, 12, 4);
984+
row.addView(numView);
985+
986+
TextView textView = new TextView(this);
987+
textView.setText(text);
988+
textView.setTextSize(12);
989+
textView.setTextColor(0xFF000000);
990+
textView.setPadding(12, 0, 0, 0);
991+
textView.setGravity(Gravity.CENTER_VERTICAL);
992+
row.addView(textView);
993+
994+
return row;
995+
}
996+
997+
private LinearLayout.LayoutParams createStepParams(int topMargin) {
998+
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
999+
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
1000+
params.topMargin = topMargin;
1001+
return params;
1002+
}
1003+
1004+
private void writeGiftModeScreensaver(String code, String fromName, String toName) {
1005+
// Use gift screensaver image (already in native portrait orientation 600x800)
1006+
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gift_screensaver);
1007+
if (bitmap != null) {
1008+
writeScreenshotToScreensaver(bitmap);
1009+
}
1010+
}
1011+
8611012
private void toggleMenu() {
8621013
if (menuVisible) {
8631014
hideMenu();
@@ -1011,6 +1162,10 @@ public void run() {
10111162
}
10121163

10131164
private boolean ensureCredentials() {
1165+
// Don't redirect to settings if gift mode is enabled
1166+
if (ApiPrefs.isGiftModeEnabled(this)) {
1167+
return false;
1168+
}
10141169
if (!ApiPrefs.hasCredentials(this)) {
10151170
startActivity(new Intent(this, SettingsActivity.class));
10161171
return false;

0 commit comments

Comments
 (0)