aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/kindle/install/appreg.install.sql19
-rw-r--r--src/kindle/install/install.sh48
-rw-r--r--src/kindle/install/whispertouch.install.sql1
-rw-r--r--src/kindle/libotautils5382
-rw-r--r--src/kindle/uninstall/appreg.uninstall.sql8
-rw-r--r--src/kindle/uninstall/uninstall.sh37
-rw-r--r--src/main/filtered/version.txt3
-rw-r--r--src/main/java/uk/co/notori/gol/KGOLBooklet.java138
-rw-r--r--src/main/java/uk/co/notori/gol/Main.java35
-rw-r--r--src/main/java/uk/co/notori/gol/MainScreen.java185
-rw-r--r--src/main/java/uk/co/notori/gol/MainUI.java329
-rw-r--r--src/main/java/uk/co/notori/gol/Panel.java249
-rw-r--r--src/main/java/uk/co/notori/gol/Util.java134
13 files changed, 1568 insertions, 0 deletions
diff --git a/src/kindle/install/appreg.install.sql b/src/kindle/install/appreg.install.sql
new file mode 100644
index 0000000..f5b2576
--- /dev/null
+++ b/src/kindle/install/appreg.install.sql
@@ -0,0 +1,19 @@
+INSERT OR IGNORE INTO "handlerIds" VALUES('uk.co.notori.gol');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','lipcId','uk.co.notori.gol');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','jar','/opt/amazon/ebook/booklet/GameOfLife.jar');
+
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','maxUnloadTime','45');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','maxGoTime','60');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','maxPauseTime','60');
+
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','default-chrome-style','NH');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','unloadPolicy','unloadOnPause');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','extend-start','Y');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','searchbar-mode','transient');
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','supportedOrientation','U');
+
+INSERT OR IGNORE INTO "mimetypes" VALUES('gol','MT:image/x.gol');
+INSERT OR IGNORE INTO "extenstions" VALUES('gol','MT:image/x.gol');
+INSERT OR IGNORE INTO "properties" VALUES('archive.displaytags.mimetypes','image/x.gol','GameOfLife');
+INSERT OR IGNORE INTO "associations" VALUES('com.lab126.generic.extractor','extractor','GL:*.gol','true');
+INSERT OR IGNORE INTO "associations" VALUES('uk.co.notori.gol','application','MT:image/x.gol','true');
diff --git a/src/kindle/install/install.sh b/src/kindle/install/install.sh
new file mode 100644
index 0000000..67d93ed
--- /dev/null
+++ b/src/kindle/install/install.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+#
+# KUAL Booklet installer
+#
+# $Id$
+#
+##
+
+# Pull libOTAUtils for logging & progress handling
+[ -f ./libotautils5 ] && source ./libotautils5
+
+
+otautils_update_progressbar
+
+logmsg "I" "install" "" "installing booklet"
+cp -f "GameOfLife.jar" "/opt/amazon/ebook/booklet/GameOfLife.jar"
+
+otautils_update_progressbar
+
+logmsg "I" "install" "" "registering booklet"
+sqlite3 "/var/local/appreg.db" < "appreg.install.sql"
+
+otautils_update_progressbar
+
+# Enable WhisperTouch on the Voyage (c.f., https://github.com/koreader/koreader/issues/6038#issuecomment-612564693)
+# Use the FBInk binary chosen by libOTAUtils
+eval $(${FBINK_BIN} -e)
+if [ "${deviceName}" = "Voyage" ] ; then
+ logmsg "I" "install" "" "enabling whispertouch"
+ sqlite3 "/var/local/appreg.db" < "whispertouch.install.sql"
+fi
+
+otautils_update_progressbar
+
+
+logmsg "I" "install" "" "creating application"
+touch "/mnt/us/documents/GameOfLife.gol"
+
+otautils_update_progressbar
+
+logmsg "I" "install" "" "cleaning up"
+rm -f "GameOfLife.jar" "appreg.install.sql"
+
+logmsg "I" "install" "" "done"
+
+otautils_update_progressbar
+
+return 0
diff --git a/src/kindle/install/whispertouch.install.sql b/src/kindle/install/whispertouch.install.sql
new file mode 100644
index 0000000..2753f4f
--- /dev/null
+++ b/src/kindle/install/whispertouch.install.sql
@@ -0,0 +1 @@
+INSERT OR IGNORE INTO "properties" VALUES('uk.co.notori.gol','whisper-touch','supported');
diff --git a/src/kindle/libotautils5 b/src/kindle/libotautils5
new file mode 100644
index 0000000..49c4457
--- /dev/null
+++ b/src/kindle/libotautils5
@@ -0,0 +1,382 @@
+#!/bin/sh
+##
+#
+# Logging/Progressbar handling for OTA update scripts
+#
+# $Id$
+#
+# kate: syntax bash;
+#
+##
+
+## Logging
+# Pull some helper functions for logging
+_FUNCTIONS=/etc/upstart/functions
+[ -f ${_FUNCTIONS} ] && source ${_FUNCTIONS}
+
+# Make sure HACKNAME is set (NOTE: This should be overriden in the update script)
+[ -z "${HACKNAME}" ] && HACKNAME="ota_script"
+
+
+# Adapt the K5 logging calls to the simpler legacy syntax
+logmsg()
+{
+ f_log "${1}" "${HACKNAME}" "${2}" "${3}" "${4}"
+ # Add our own echo, like on legacy devices (useful for MRPI logging, since f_log's one is sent to /dev/console)
+ if [ "${1}" != "D" ] ; then
+ echo "system: ${1} ${HACKNAME}:${2}:${3}:${4}"
+ fi
+}
+
+
+# We need to get the proper constants for our model...
+kmodel="$(cut -c3-4 /proc/usid)"
+case "${kmodel}" in
+ "13" | "54" | "2A" | "4F" | "52" | "53" )
+ # Voyage...
+ SCREEN_X_RES=1088
+ SCREEN_Y_RES=1448
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "24" | "1B" | "1D" | "1F" | "1C" | "20" | "D4" | "5A" | "D5" | "D6" | "D7" | "D8" | "F2" | "17" | "60" | "F4" | "F9" | "62" | "61" | "5F" )
+ # PaperWhite...
+ SCREEN_X_RES=768
+ SCREEN_Y_RES=1024
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "C6" | "DD" )
+ # KT2...
+ SCREEN_X_RES=608
+ SCREEN_Y_RES=800
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "0F" | "11" | "10" | "12" )
+ # Touch
+ SCREEN_X_RES=600
+ SCREEN_Y_RES=800
+ EIPS_X_RES=12
+ EIPS_Y_RES=20
+ ;;
+ * )
+ # Try the new device ID scheme...
+ kmodel="$(cut -c4-6 /proc/usid)"
+ case "${kmodel}" in
+ "0G1" | "0G2" | "0G4" | "0G5" | "0G6" | "0G7" | "0KB" | "0KC" | "0KD" | "0KE" | "0KF" | "0KG" | "0LK" | "0LL" )
+ # PW3...
+ SCREEN_X_RES=1088
+ SCREEN_Y_RES=1448
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "0GC" | "0GD" | "0GR" | "0GS" | "0GT" | "0GU" )
+ # Oasis...
+ SCREEN_X_RES=1088
+ SCREEN_Y_RES=1448
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "0DU" | "0K9" | "0KA" )
+ # KT3...
+ SCREEN_X_RES=608
+ SCREEN_Y_RES=800
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "0LM" | "0LN" | "0LP" | "0LQ" | "0P1" | "0P2" | "0P6" | "0P7" | "0P8" | "0S1" | "0S2" | "0S3" | "0S4" | "0S7" | "0SA" )
+ # Oasis 2...
+ SCREEN_X_RES=1280
+ SCREEN_Y_RES=1680
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "0PP" | "0T1" | "0T2" | "0T3" | "0T4" | "0T5" | "0T6" | "0T7" | "0TJ" | "0TK" | "0TL" | "0TM" | "0TN" | "102" | "103" | "16Q" | "16R" | "16S" | "16T" | "16U" | "16V" )
+ # PW4...
+ SCREEN_X_RES=1088
+ SCREEN_Y_RES=1448
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "10L" | "0WF" | "0WG" | "0WH" | "0WJ" | "0VB" )
+ # KT4...
+ SCREEN_X_RES=608
+ SCREEN_Y_RES=800
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ "11L" | "0WQ" | "0WP" | "0WN" | "0WM" | "0WL" )
+ # Oasis 3...
+ SCREEN_X_RES=1280
+ SCREEN_Y_RES=1680
+ EIPS_X_RES=16
+ EIPS_Y_RES=24
+ ;;
+ * )
+ # Fallback... We shouldn't ever hit that.
+ SCREEN_X_RES=600
+ SCREEN_Y_RES=800
+ EIPS_X_RES=12
+ EIPS_Y_RES=20
+ ;;
+ esac
+ ;;
+esac
+# And now we can do the maths ;)
+EIPS_MAXCHARS="$((${SCREEN_X_RES} / ${EIPS_X_RES}))"
+EIPS_MAXLINES="$((${SCREEN_Y_RES} / ${EIPS_Y_RES}))"
+
+
+# Adapted from libkh[5]
+## Check if we have an FBInk binary available somewhere...
+# Default to something that won't horribly blow up...
+FBINK_BIN="true"
+for my_hackdir in linkss linkfonts libkh usbnet ; do
+ my_fbink="/mnt/us/${my_hackdir}/bin/fbink"
+ if [ -x "${my_fbink}" ] ; then
+ FBINK_BIN="${my_fbink}"
+ # Got it!
+ break
+ fi
+done
+has_fbink()
+{
+ # Because the fallback is the "true" binary/shell built-in ;).
+ if [ "${FBINK_BIN}" != "true" ] ; then
+ # Got it!
+ return 0
+ fi
+
+ # If we got this far, we don't have fbink installed
+ return 1
+}
+
+do_fbink_print()
+{
+ # We need at least two args
+ if [ $# -lt 2 ] ; then
+ echo "not enough arguments passed to do_fbink_print ($# while we need at least 2)"
+ return
+ fi
+
+ kh_eips_string="${1}"
+ kh_eips_y_shift_up="${2}"
+
+ # Unlike eips, we need at least a single space to even try to print something ;).
+ if [ "${kh_eips_string}" == "" ] ; then
+ kh_eips_string=" "
+ fi
+
+ # Check if we asked for a highlighted message...
+ if [ "${3}" == "h" ] ; then
+ fbink_extra_args="h"
+ else
+ fbink_extra_args=""
+ fi
+
+ # NOTE: FBInk will handle the padding. FBInk's default font is square, not tall like eips,
+ # so we compensate by tweaking the baseline ;).
+ ${FBINK_BIN} -qpm${fbink_extra_args} -y $(( -4 - ${kh_eips_y_shift_up} )) "${kh_eips_string}"
+}
+
+do_fbink_bar()
+{
+ # We need at least two args
+ if [ $# -lt 2 ] ; then
+ echo "not enough arguments passed to do_fbink_bar ($# while we need at least 2)"
+ return
+ fi
+
+ fbink_progress="${1}"
+ kh_eips_y_shift_up="${2}"
+
+ ${FBINK_BIN} -qP ${fbink_progress} -y $(( -4 - ${kh_eips_y_shift_up} ))
+}
+
+do_eips_print()
+{
+ # We need at least two args
+ if [ $# -lt 2 ] ; then
+ echo "not enough arguments passed to do_eips_print ($# while we need at least 2)"
+ return
+ fi
+
+ kh_eips_string="${1}"
+ kh_eips_y_shift_up="${2}"
+
+ # Get the real string length now
+ kh_eips_strlen="${#kh_eips_string}"
+
+ # Add the right amount of left & right padding, since we're centered, and eips doesn't trigger a full refresh,
+ # so we'll have to padd our string with blank spaces to make sure two consecutive messages don't run into each other
+ kh_padlen="$(((${EIPS_MAXCHARS} - ${kh_eips_strlen}) / 2))"
+
+ # Left padding...
+ while [ ${#kh_eips_string} -lt $((${kh_eips_strlen} + ${kh_padlen})) ] ; do
+ kh_eips_string=" ${kh_eips_string}"
+ done
+
+ # Right padding (crop to the edge of the screen)
+ while [ ${#kh_eips_string} -lt ${EIPS_MAXCHARS} ] ; do
+ kh_eips_string="${kh_eips_string} "
+ done
+
+ # And finally, show our formatted message centered on the bottom of the screen (NOTE: Redirect to /dev/null to kill unavailable character & pixel not in range warning messages)
+ eips 0 $((${EIPS_MAXLINES} - 2 - ${kh_eips_y_shift_up})) "${kh_eips_string}" >/dev/null
+}
+
+eips_print_bottom_centered()
+{
+ # We need at least two args
+ if [ $# -lt 2 ] ; then
+ echo "not enough arguments passed to eips_print_bottom_centered ($# while we need at least 2)"
+ return
+ fi
+
+ kh_eips_string="${1}"
+ kh_eips_y_shift_up="${2}"
+
+ # Sleep a tiny bit to workaround the logic in the 'new' (K4+) eInk controllers that tries to bundle updates
+ if [ "${EIPS_SLEEP}" == "true" ] ; then
+ usleep 150000 # 150ms
+ fi
+
+ # Can we use FBInk?
+ if has_fbink ; then
+ do_fbink_print "${kh_eips_string}" ${kh_eips_y_shift_up}
+ else
+ do_eips_print "${kh_eips_string}" ${kh_eips_y_shift_up}
+ fi
+}
+
+
+## Progressbar
+# Some constants...
+_BLANKET="com.lab126.blanket"
+_OTAMODULE="${_BLANKET}.ota"
+
+# Check if blanket is running
+if pkill -0 blanket ; then
+ BLANKET_IS_UP="true"
+else
+ BLANKET_IS_UP="false"
+fi
+
+# Send progress to blanket, or print it manually otherwise
+otautils_update_progress_indicator()
+{
+ local cur_percentage="${1}"
+
+ if [ "${BLANKET_IS_UP}" == "true" ] ; then
+ lipc-send-event ${_OTAMODULE} otaSplashProgress -i ${cur_percentage}
+ else
+ # NOTE: We can actually draw a progress bar with FBInk!
+ if has_fbink ; then
+ do_fbink_bar ${cur_percentage} 2
+ else
+ do_eips_print "Progress: ${cur_percentage}/100" 2
+ fi
+ fi
+}
+
+# Check if arg is an int
+is_integer()
+{
+ # Cheap trick ;)
+ [ "${1}" -eq "${1}" ] 2>/dev/null
+ return $?
+}
+
+# The amount of steps needed to fill the progress bar
+# I'm lazy, so just count the amount of calls in the script itself ;)
+# NOTE: Yup, $0 still points to the original script that sourced us :).
+[ -z ${STEPCOUNT} ] && STEPCOUNT="$(grep -c '^[[:blank:]]*otautils_update_progressbar$' ${0} 2>/dev/null)"
+# Make sure it's sane...
+is_integer "${STEPCOUNT}" || STEPCOUNT=1
+# NOTE: If you need to for some strange reason, this can be overriden in the update script
+
+# In case we need to catch failure early...
+otautils_die()
+{
+ local error_string="${1}"
+
+ if [ "${BLANKET_IS_UP}" == "true" ] ; then
+ lipc-send-event ${_OTAMODULE} otaSplashError -s "${error_string}"
+ else
+ eips_print_bottom_centered "Error: ${error_string}" 1
+ fi
+ if [ $? -eq 0 ] ; then
+ logmsg "D" "guierror" "" "display error screen: ${error_string}"
+ else
+ logmsg "W" "guierror" "status=fail" "display error screen: ${error_string}"
+ fi
+
+ # And it is called die, after all ;)
+ sleep 5
+ exit 1
+}
+
+# Fill up our progress bar, one step at a time
+# Keep track of what we're doing...
+_CUR_STEP=0
+_CUR_PERCENTAGE=0
+otautils_update_progressbar()
+{
+ # One more step...
+ _CUR_STEP=$((_CUR_STEP + 1))
+ # Bounds checking...
+ if [ ${_CUR_STEP} -lt 0 ] ; then
+ _CUR_STEP=0
+ elif [ ${_CUR_STEP} -gt ${STEPCOUNT} ] ; then
+ _CUR_STEP=${STEPCOUNT}
+ fi
+
+ # Make that a percentage
+ local bar_percentage=$(( (${_CUR_STEP} * 100) / ${STEPCOUNT} ))
+ # We can only *fill* the bar...
+ if [ ${_CUR_PERCENTAGE} -lt ${bar_percentage} ] ; then
+ _CUR_PERCENTAGE=${bar_percentage}
+ fi
+
+ # Make sure that percentage is sane...
+ is_integer "${_CUR_PERCENTAGE}" || _CUR_PERCENTAGE=0
+ # Bounds checking...
+ if [ ${_CUR_PERCENTAGE} -gt 100 ] ; then
+ _CUR_PERCENTAGE=100
+ elif [ ${_CUR_PERCENTAGE} -lt 0 ] ; then
+ _CUR_PERCENTAGE=0
+ fi
+
+ # Finally, refresh the bar
+ otautils_update_progress_indicator "${_CUR_PERCENTAGE}"
+ if [ $? -eq 0 ] ; then
+ logmsg "D" "guiprogress" "progress=${_CUR_PERCENTAGE}" "update progress indicator"
+ else
+ logmsg "W" "guiprogress" "progress=${_CUR_PERCENTAGE},status=fail" "update progress indicator"
+ fi
+}
+
+# This may come in handy for bridge related packages...
+make_mutable() {
+ local my_path="${1}"
+ # NOTE: Can't do that on symlinks, hence the hoop-jumping...
+ if [ -d "${my_path}" ] ; then
+ find "${my_path}" -type d -exec chattr -i '{}' \;
+ find "${my_path}" -type f -exec chattr -i '{}' \;
+ elif [ -f "${my_path}" ] ; then
+ chattr -i "${my_path}"
+ fi
+}
+
+make_immutable() {
+ local my_path="${1}"
+ if [ -d "${my_path}" ] ; then
+ find "${my_path}" -type d -exec chattr +i '{}' \;
+ find "${my_path}" -type f -exec chattr +i '{}' \;
+ elif [ -f "${my_path}" ] ; then
+ chattr +i "${my_path}"
+ fi
+}
+
+# That's all, folks ;)
diff --git a/src/kindle/uninstall/appreg.uninstall.sql b/src/kindle/uninstall/appreg.uninstall.sql
new file mode 100644
index 0000000..b1d96cc
--- /dev/null
+++ b/src/kindle/uninstall/appreg.uninstall.sql
@@ -0,0 +1,8 @@
+DELETE FROM "handlerIds" WHERE handlerId='uk.co.notori.gol';
+DELETE FROM "properties" WHERE handlerId='uk.co.notori.gol';
+DELETE FROM "associations" WHERE handlerId='uk.co.notori.gol';
+
+DELETE FROM "mimetypes" WHERE ext='gol';
+DELETE FROM "extenstions" WHERE ext='gol';
+DELETE FROM "properties" WHERE value='GameOfLife';
+DELETE FROM "associations" WHERE contentId='GL:*.gol';
diff --git a/src/kindle/uninstall/uninstall.sh b/src/kindle/uninstall/uninstall.sh
new file mode 100644
index 0000000..cdafdb8
--- /dev/null
+++ b/src/kindle/uninstall/uninstall.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+#
+# KUAL Booklet uninstaller
+#
+# $Id$
+#
+##
+
+# Pull libOTAUtils for logging & progress handling
+[ -f ./libotautils5 ] && source ./libotautils5
+
+
+otautils_update_progressbar
+
+logmsg "I" "uninstall" "" "uninstalling booklet"
+rm -f "/opt/amazon/ebook/booklet/GameOfLife.jar"
+
+otautils_update_progressbar
+
+logmsg "I" "uninstall" "" "deregistering booklet"
+sqlite3 "/var/local/appreg.db" < "appreg.uninstall.sql"
+
+otautils_update_progressbar
+
+logmsg "I" "uninstall" "" "removing application"
+rm -f "/mnt/us/documents/GameOfLife.gol"
+
+otautils_update_progressbar
+
+logmsg "I" "uninstall" "" "cleaning up"
+rm -f "appreg.uninstall.sql"
+
+logmsg "I" "uninstall" "" "done"
+
+otautils_update_progressbar
+
+return 0
diff --git a/src/main/filtered/version.txt b/src/main/filtered/version.txt
new file mode 100644
index 0000000..c7ba737
--- /dev/null
+++ b/src/main/filtered/version.txt
@@ -0,0 +1,3 @@
+version=${pom.version}
+build.date=${timestamp}
+gitSha1=${buildNumber}
diff --git a/src/main/java/uk/co/notori/gol/KGOLBooklet.java b/src/main/java/uk/co/notori/gol/KGOLBooklet.java
new file mode 100644
index 0000000..efb620b
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/KGOLBooklet.java
@@ -0,0 +1,138 @@
+package uk.co.notori.gol;
+
+import com.amazon.kindle.booklet.AbstractBooklet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.net.URI;
+
+/**
+ * Kindle Booklet entry for application
+ */
+public class KGOLBooklet extends AbstractBooklet implements ActionListener {
+
+ static {
+ System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+ System.setProperty("org.slf4j.simpleLogger.logFile","/mnt/us/kgol.log");
+ System.setProperty("org.slf4j.simpleLogger.showDateTime","true");
+ System.setProperty("org.slf4j.simpleLogger.showShortLogName","true");
+ System.setProperty("org.slf4j.simpleLogger.dateTimeFormat","yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ log = LoggerFactory.getLogger(KGOLBooklet.class);
+ Util.setKindle(true);
+ }
+
+ private static final Logger log;
+ private Container rootContainer = null;
+
+ public KGOLBooklet() {
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ KGOLBooklet.this.longStart();
+ }
+ },
+ 1000
+ );
+ }
+
+ public void start(URI uri) {
+ log.info("start called with {} ", uri);
+ super.start(uri);
+ }
+
+ // Because this got obfuscated...
+ private Container getUIContainer() {
+ // Check our cached value, first
+ if (rootContainer != null) {
+ return rootContainer;
+ } else {
+ try {
+ Container container = Util.getUIContainer(this);
+ if (container == null) {
+ log.error("Failed to find getUIContainer method, abort!");
+ endBooklet();
+ return null;
+ }
+ rootContainer = container;
+ return container;
+ } catch (Throwable t) {
+ throw new RuntimeException(t.toString());
+ }
+ }
+ }
+
+ private void endBooklet() {
+ try {
+ log.info("Ending Booklet");
+ Runtime.getRuntime().exec("lipc-set-prop com.lab126.appmgrd stop app://uk.co.notori.gol");
+ } catch (IOException e) {
+ log.error("Failed when terminating ", e);
+ }
+ }
+
+ private void longStart() {
+ try {
+ initializeUI();
+ } catch (Throwable t) {
+ log.error(t.getMessage(), new RuntimeException(t));
+ endBooklet();
+ throw new RuntimeException(t);
+ }
+ }
+
+ private void initializeUI() {
+ log.debug("Starting Up");
+ Container root = getUIContainer();
+
+ log.debug("Got UI container: {}", root);
+ assert root != null;
+
+ // clear the container first
+ root.removeAll();
+
+ Font rootFont = new Font("SansSerif", Font.PLAIN, 12);
+ root.setFont(rootFont);
+
+ final KGOLBooklet booklet = this;
+
+ MainScreen mainScreen = new MainScreen(
+ root,
+ new MainScreen.ExitHook() {
+ public void exit() {
+ booklet.endBooklet();
+ }
+ }
+ );
+
+ // force a repaint
+ try {
+ root.requestFocus();
+
+ mainScreen.start();
+
+ } catch (Exception e) {
+ log.error("Error during UI initialization", e);
+ }
+ }
+
+ public void destroy() {
+ // Try to cleanup behind us on exit...
+ try {
+ // NOTE: This can be a bit racey with stop(),
+ // so sleep for a tiny bit so our commandToRunOnExit actually has a chance to run...
+ Thread.sleep(175);
+ Util.updateCCDB("Game of Life", "/mnt/us/documents/GameOfLife.kgol");
+ } catch (Exception ignored) {
+ // Avoid the framework shouting at us
+ }
+
+ super.destroy();
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ log.debug("Action Performed {} ", e);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Main.java b/src/main/java/uk/co/notori/gol/Main.java
new file mode 100644
index 0000000..018ee0d
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Main.java
@@ -0,0 +1,35 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Main class for desktop testing
+ */
+public class Main {
+
+ static {
+ System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+ System.setProperty("org.slf4j.simpleLogger.logFile", "System.err");
+ System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
+ System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true");
+ System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ }
+
+ public static void main(String[] args) {
+ Util.setKindle(false);
+
+ JFrame frame = new JFrame("Conway's Game of Life");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setSize(800, 600);
+
+
+ new MainScreen(frame, new MainScreen.ExitHook() {
+ public void exit() {
+ System.exit(0);
+ }
+ });
+
+ frame.setVisible(true);
+ }
+}
diff --git a/src/main/java/uk/co/notori/gol/MainScreen.java b/src/main/java/uk/co/notori/gol/MainScreen.java
new file mode 100644
index 0000000..5829f42
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/MainScreen.java
@@ -0,0 +1,185 @@
+package uk.co.notori.gol;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Main screen manager
+ */
+public class MainScreen {
+
+ private static final Logger log = LoggerFactory.getLogger(MainScreen.class);
+
+ private JPanel currentUI;
+ private final Container root;
+ private final ExitHook mainExitHook;
+
+ public interface ExitHook {
+ void exit();
+ }
+
+ public interface NewGameHook {
+ void newGame();
+ }
+
+ public MainScreen(Container root, ExitHook exitHook) {
+ this(root, exitHook, null);
+ }
+
+ public MainScreen(Container root, ExitHook exitHook, String savePath) {
+ this.root = root;
+ this.mainExitHook = exitHook;
+
+ if (!Util.isKindle()) {
+ root.setSize(new Dimension(800, 600));
+ }
+ // start new game grid on load
+ newGame();
+ }
+
+ public void start() {
+ log.info("Starting Game of Life application");
+
+ if (currentUI != null) {
+ currentUI.setVisible(true);
+
+ // force a complete refresh
+ root.invalidate();
+ root.validate();
+ root.repaint();
+
+ // very ugly fix for weird component visibility behaviour
+ if (Util.isKindle()) {
+ scheduleRefresh(300);
+ scheduleRefresh(600);
+ scheduleRefresh(1200);
+ scheduleRefresh(2000);
+ scheduleRefresh(3000);
+
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ refreshButtonsOnly(root);
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during button refresh", e);
+ }
+ }
+ },
+ 3500
+ );
+ }
+ }
+
+ log.info("Application started");
+ }
+
+ // another very ugly fix for weird component visibility behaviour
+ private void scheduleRefresh(final int delay) {
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ refreshComponentsRecursively(root);
+
+ currentUI.invalidate();
+ currentUI.validate();
+ currentUI.repaint();
+
+ root.invalidate();
+ root.validate();
+ root.repaint();
+
+ log.info("Completed UI refresh after " + delay + "ms");
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during UI refresh", e);
+ }
+ }
+ },
+ delay
+ );
+ }
+
+ // another very ugly fix for weird component visibility behaviour
+ private void refreshComponentsRecursively(Container container) {
+ Component[] components = container.getComponents();
+ for (int i = 0; i < components.length; i++) {
+ Component component = components[i];
+ component.invalidate();
+ component.validate();
+ component.repaint();
+
+ // handling for buttons to ensure visibility
+ if (component instanceof JButton) {
+ JButton button = (JButton)component;
+ button.setVisible(true);
+ }
+
+ if (component instanceof Container) {
+ refreshComponentsRecursively((Container)component);
+ }
+ }
+ }
+
+ private void refreshButtonsOnly(Container container) {
+ Component[] components = container.getComponents();
+ for (int i = 0; i < components.length; i++) {
+ Component component = components[i];
+
+ if (component instanceof JButton) {
+ JButton button = (JButton)component;
+ button.setVisible(true);
+ button.invalidate();
+ button.validate();
+ button.repaint();
+ log.info("Refreshed button: " + button.getText());
+ }
+
+ if (component instanceof Container) {
+ refreshButtonsOnly((Container)component);
+ }
+ }
+ }
+
+ public void newGame() {
+ log.info("Starting new game");
+
+ // remove current UI if it exists
+ if (currentUI != null) {
+ root.remove(currentUI);
+ }
+
+ // create main UI with new panel
+ Panel gamePanel = new Panel(root.getSize());
+ currentUI = new MainUI(
+ new ExitHook() {
+ public void exit() {
+ mainExitHook.exit();
+ }
+ },
+ gamePanel
+ );
+
+ currentUI.setSize(root.getSize());
+ currentUI.setPreferredSize(root.getSize());
+
+ root.add(currentUI, BorderLayout.CENTER);
+
+ currentUI.setVisible(true);
+ gamePanel.setVisible(true);
+
+ root.validate();
+ root.repaint();
+ }
+}
diff --git a/src/main/java/uk/co/notori/gol/MainUI.java b/src/main/java/uk/co/notori/gol/MainUI.java
new file mode 100644
index 0000000..dbb3ddd
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/MainUI.java
@@ -0,0 +1,329 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The main UI for the Game of Life app
+ */
+public class MainUI extends JPanel {
+ private static final Logger log = LoggerFactory.getLogger(MainUI.class);
+
+ public final Panel panel;
+ private final JLabel generationLabel;
+ private final JLabel populationLabel;
+
+ public MainUI(MainScreen.ExitHook exitHook, Panel panel) {
+ super();
+
+ this.panel = panel;
+
+ setLayout(new BorderLayout());
+
+ // Panel area
+ add(panel, BorderLayout.CENTER);
+
+ // calculate appropriate font sizes based on screen dimensions,
+ // not tested on multiple devicesbut in theory works.
+ Dimension screenSize = getScreenSize();
+ int labelFontSize = calculateFontSize(screenSize, 14);
+ int buttonFontSize = calculateFontSize(screenSize, 12);
+ int speedFontSize = calculateFontSize(screenSize, 10);
+ int infoFontSize = calculateFontSize(screenSize, 10);
+
+ // info panel (top)
+ JPanel infoPanel = new JPanel();
+ infoPanel.setLayout(new GridLayout(1, 2));
+
+ generationLabel = new JLabel("Generation: 0", JLabel.CENTER);
+ generationLabel.setFont(new Font("SansSerif", Font.BOLD, labelFontSize));
+ infoPanel.add(generationLabel);
+
+ populationLabel = new JLabel("Population: 0", JLabel.CENTER);
+ populationLabel.setFont(new Font("SansSerif", Font.BOLD, labelFontSize));
+ infoPanel.add(populationLabel);
+
+ add(infoPanel, BorderLayout.NORTH);
+
+ // control panel (bottom)
+ JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
+
+ JLabel creditsLabel = new JLabel("Kindle Game Of Life - made by notori :)", JLabel.CENTER);
+ creditsLabel.setFont(new Font("SansSerif", Font.ITALIC, infoFontSize));
+ creditsLabel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
+ bottomPanel.add(creditsLabel, BorderLayout.NORTH);
+
+ // main control buttons in a grid
+ JPanel controlPanel = new JPanel();
+ controlPanel.setLayout(new GridLayout(1, 3, 10, 10)); // 1 row, 3 columns with gaps
+ controlPanel.setOpaque(true);
+
+ // speed buttons in their own panel
+ JPanel speedPanel = new JPanel();
+ speedPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 8, 5));
+ speedPanel.setOpaque(true);
+ speedPanel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createLineBorder(Color.BLACK, 1),
+ BorderFactory.createEmptyBorder(5, 5, 5, 5)
+ ));
+
+ JLabel speedLabel = new JLabel("Speed:", JLabel.CENTER);
+ speedLabel.setFont(new Font("SansSerif", Font.BOLD, speedFontSize));
+ speedPanel.add(speedLabel);
+
+ // declare all buttons
+ JButton playButton = new JButton("Play");
+ JButton pauseButton = new JButton("Pause");
+ JButton resetButton = new JButton("Reset");
+ JButton slowButton = new JButton("S");
+ JButton mediumButton = new JButton("M");
+ JButton fastButton = new JButton("F");
+ JButton exitButton = new JButton("Exit");
+
+ // configure control buttons
+ playButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ playButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.play();
+ }
+ });
+ controlPanel.add(playButton);
+ styleControlButton(playButton);
+
+ pauseButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ pauseButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.pause();
+ }
+ });
+ controlPanel.add(pauseButton);
+ styleControlButton(pauseButton);
+
+ resetButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ resetButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.reset();
+ }
+ });
+ controlPanel.add(resetButton);
+ styleControlButton(resetButton);
+
+ // configure speed buttons
+ slowButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ slowButton.setPreferredSize(new Dimension(50, 50));
+ slowButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(500);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(slowButton);
+ styleSpeedButton(slowButton);
+
+ mediumButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ mediumButton.setPreferredSize(new Dimension(50, 50));
+ mediumButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(300);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(mediumButton);
+ styleSpeedButton(mediumButton);
+
+ fastButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ fastButton.setPreferredSize(new Dimension(50, 50));
+ fastButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(100);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(fastButton);
+ styleSpeedButton(fastButton);
+
+ // medium speed as default
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+
+ // exit button
+ exitButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ exitButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ exitHook.exit();
+ }
+ });
+ styleControlButton(exitButton);
+
+ JPanel exitPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ exitPanel.add(exitButton);
+
+ // add control panels to the container
+ JPanel controlContainer = new JPanel(new BorderLayout());
+ controlContainer.add(controlPanel, BorderLayout.NORTH);
+ controlContainer.add(speedPanel, BorderLayout.CENTER);
+
+ // add all panels to the bottom panel
+ bottomPanel.add(controlContainer, BorderLayout.CENTER);
+ bottomPanel.add(exitPanel, BorderLayout.SOUTH);
+
+ // add bottom panel to main layout
+ add(bottomPanel, BorderLayout.SOUTH);
+
+ // another very ugly fix for weird component visibility behaviour
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ // Force the buttons to be visible
+ refreshButton(playButton);
+ refreshButton(pauseButton);
+ refreshButton(resetButton);
+ refreshButton(slowButton);
+ refreshButton(mediumButton);
+ refreshButton(fastButton);
+ refreshButton(exitButton);
+
+ controlPanel.invalidate();
+ controlPanel.validate();
+ controlPanel.repaint();
+
+ speedPanel.invalidate();
+ speedPanel.validate();
+ speedPanel.repaint();
+
+ bottomPanel.invalidate();
+ bottomPanel.validate();
+ bottomPanel.repaint();
+
+ creditsLabel.setVisible(true);
+ creditsLabel.invalidate();
+ creditsLabel.validate();
+ creditsLabel.repaint();
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during button refresh", e);
+ }
+ }
+ },
+ 1000
+ );
+
+ updateCounters(panel.getGeneration(), panel.getPopulation());
+
+ // another very ugly fix for weird component visibility behaviour
+ new javax.swing.Timer(800, new ActionListener() {
+ private int count = 0;
+ public void actionPerformed(ActionEvent e) {
+ if (count++ < 5) {
+ bottomPanel.invalidate();
+ bottomPanel.validate();
+ bottomPanel.repaint();
+ } else {
+ ((javax.swing.Timer)e.getSource()).stop();
+ }
+ }
+ }).start();
+
+ log.debug("MainUI initialized with buttons and panels");
+ }
+
+ /**
+ * Helper method to refresh button visibility
+ */
+ private void refreshButton(JButton button) {
+ button.setVisible(true);
+ button.invalidate();
+ button.validate();
+ button.repaint();
+ }
+
+ /**
+ * Helper method to style control buttons
+ */
+ private void styleControlButton(JButton button) {
+ button.setOpaque(true);
+ button.setBorderPainted(true);
+ button.setContentAreaFilled(true);
+ button.setFocusable(true);
+ button.setVisible(true);
+
+ button.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+ }
+
+ /**
+ * Helper method to style speed buttons
+ */
+ private void styleSpeedButton(JButton button) {
+ button.setOpaque(true);
+ button.setBorderPainted(true);
+ button.setContentAreaFilled(true);
+ button.setFocusable(true);
+ button.setVisible(true);
+
+ button.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ button.setMargin(new Insets(8, 8, 8, 8));
+ button.setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ /**
+ * Helper method to update speed button selection
+ */
+ private void updateSpeedButtonSelection(JButton slowButton, JButton mediumButton, JButton fastButton) {
+ // reset all buttons to normal state
+ slowButton.setFont(new Font(slowButton.getFont().getName(), Font.PLAIN, slowButton.getFont().getSize()));
+ mediumButton.setFont(new Font(mediumButton.getFont().getName(), Font.PLAIN, mediumButton.getFont().getSize()));
+ fastButton.setFont(new Font(fastButton.getFont().getName(), Font.PLAIN, fastButton.getFont().getSize()));
+
+ slowButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+ mediumButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+ fastButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ // determine which button is selected
+ JButton selectedButton;
+ if (panel.getDelay() == 500) {
+ selectedButton = slowButton;
+ } else if (panel.getDelay() == 100) {
+ selectedButton = fastButton;
+ } else {
+ selectedButton = mediumButton;
+ }
+
+ // make selected button stand out with border
+ selectedButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 3));
+ }
+
+ private Dimension getScreenSize() {
+ if (Panel.rootSize != null) {
+ return Panel.rootSize;
+ }
+ return new Dimension(800, 600); // default fallback
+ }
+
+ private int calculateFontSize(Dimension screenSize, int defaultSize) {
+ if (Util.isKindle()) {
+ if (screenSize.width <= 600) {
+ return Math.max(defaultSize - 4, 8);
+ } else if (screenSize.width <= 800) {
+ return Math.max(defaultSize - 2, 10);
+ }
+ }
+ return defaultSize;
+ }
+
+ public void updateCounters(int generation, int population) {
+ generationLabel.setText("Generation: " + String.valueOf(generation));
+ populationLabel.setText("Population: " + String.valueOf(population));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Panel.java b/src/main/java/uk/co/notori/gol/Panel.java
new file mode 100644
index 0000000..00c8d51
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Panel.java
@@ -0,0 +1,249 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.util.Random;
+
+/**
+ * The grid for Conway's Game of Life
+ */
+public class Panel extends JPanel implements MouseListener {
+ public static Dimension rootSize;
+
+ private static final int DEFAULT_WIDTH = 40;
+ private static final int DEFAULT_HEIGHT = 40;
+ private static final int CELL_SIZE = 15;
+ private static final Color ALIVE_COLOR = Color.BLACK;
+ private static final Color DEAD_COLOR = Color.WHITE;
+ private static final Color GRID_COLOR = Color.GRAY;
+
+ private boolean[][] grid;
+ private boolean[][] nextGrid;
+ private int width;
+ private int height;
+ private boolean running;
+ private javax.swing.Timer timer;
+ private int generation;
+ private int population;
+
+ private int updateCounter = 0;
+ // update UI every 7 frames approx. every other second at medium speed
+ private static final int UPDATE_FREQUENCY = 7;
+
+ private Random random = new Random();
+
+ /**
+ * Create game Panel
+ */
+ public Panel(Dimension rootSize) {
+ super();
+
+ Panel.rootSize = rootSize;
+
+ // calculate width and height based on screen size
+ // create space for buttons
+ this.width = rootSize.width / CELL_SIZE;
+ this.height = (rootSize.height - 100) / CELL_SIZE;
+
+ grid = new boolean[width][height];
+ nextGrid = new boolean[width][height];
+
+ // size of the grid
+ setPreferredSize(new Dimension(width * CELL_SIZE, height * CELL_SIZE));
+ setLayout(null);
+
+ running = false;
+ generation = 0;
+ population = 0;
+
+ // Set up timer for animation
+ ActionListener animationListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (running) {
+ evolve();
+ updatePopulation();
+ generation++;
+ repaint();
+
+ updateCounter++;
+
+ // update UI counters so it's not visually annoying
+ if (updateCounter >= UPDATE_FREQUENCY) {
+ updateCounter = 0;
+ // update the UI with new generation and population count
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ }
+ }
+ }
+ };
+ timer = new javax.swing.Timer(300, animationListener);
+
+ initializeRandomGrid();
+
+ addMouseListener(this);
+
+ setVisible(true);
+ setOpaque(true);
+
+ timer.setInitialDelay(1000);
+ timer.start();
+ running = true;
+ }
+
+ /**
+ * Initialize the grid with random cells
+ */
+ public void initializeRandomGrid() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ grid[x][y] = random.nextDouble() < 0.3;
+ }
+ }
+ updatePopulation();
+ generation = 0;
+ updateCounter = 0;
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ repaint();
+ }
+
+ public void clearGrid() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ grid[x][y] = false;
+ }
+ }
+ updatePopulation();
+ generation = 0;
+ updateCounter = 0;
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ repaint();
+ }
+
+
+ private void evolve() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ int neighbors = countNeighbors(x, y);
+ if (grid[x][y]) {
+ nextGrid[x][y] = neighbors == 2 || neighbors == 3;
+ } else {
+ nextGrid[x][y] = neighbors == 3;
+ }
+ }
+ }
+
+ boolean[][] temp = grid;
+ grid = nextGrid;
+ nextGrid = temp;
+ }
+
+ private int countNeighbors(int x, int y) {
+ int count = 0;
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx == 0 && dy == 0) continue;
+
+ int nx = (x + dx + width) % width;
+ int ny = (y + dy + height) % height;
+
+ if (grid[nx][ny]) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ private void updatePopulation() {
+ population = 0;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (grid[x][y]) {
+ population++;
+ }
+ }
+ }
+ }
+
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ // Draw the grid
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (grid[x][y]) {
+ g.setColor(ALIVE_COLOR);
+ } else {
+ g.setColor(DEAD_COLOR);
+ }
+ g.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
+
+ g.setColor(GRID_COLOR);
+ g.drawRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
+ }
+ }
+ }
+
+ public void play() {
+ if (!running) {
+ running = true;
+ timer.start();
+ }
+ }
+
+ public void pause() {
+ if (running) {
+ running = false;
+ timer.stop();
+
+ // force a final update when pausing
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ }
+ }
+
+ public void reset() {
+ pause();
+ initializeRandomGrid();
+ generation = 0;
+ updatePopulation();
+ }
+
+ // empty implementations
+ public void mouseClicked(MouseEvent e) {}
+ public void mousePressed(MouseEvent e) {}
+ public void mouseReleased(MouseEvent e) {}
+ public void mouseEntered(MouseEvent e) {}
+ public void mouseExited(MouseEvent e) {}
+
+ public boolean isRunning() {
+ return running;
+ }
+
+ public int getGeneration() {
+ return generation;
+ }
+
+ public int getPopulation() {
+ return population;
+ }
+
+ public void setSpeed(int delay) {
+ timer.setDelay(delay);
+ }
+
+ public int getDelay() {
+ return timer.getDelay();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Util.java b/src/main/java/uk/co/notori/gol/Util.java
new file mode 100644
index 0000000..6d57e61
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Util.java
@@ -0,0 +1,134 @@
+package uk.co.notori.gol;
+
+import com.amazon.kindle.booklet.AbstractBooklet;
+import com.amazon.kindle.booklet.BookletContext;
+import com.amazon.kindle.restricted.content.catalog.ContentCatalog;
+import com.amazon.kindle.restricted.runtime.Framework;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+import java.awt.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Date;
+
+public class Util {
+ private static boolean kindle;
+
+ public static boolean isKindle() {
+ return kindle;
+ }
+
+ public static void setKindle(boolean kindleValue) {
+ kindle = kindleValue;
+ }
+
+
+ public static BookletContext getBookletContext(AbstractBooklet booklet) {
+ BookletContext bc = null;
+ Method[] methods = AbstractBooklet.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getReturnType() == BookletContext.class) {
+ // Double check that it takes no arguments, too...
+ Class<?>[] params = methods[i].getParameterTypes();
+ if (params.length == 0) {
+ try {
+ bc = (BookletContext) methods[i].invoke(booklet, (Object[]) null);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ break;
+ }
+ }
+ }
+ return bc;
+ }
+
+ public static Container getUIContainer(AbstractBooklet booklet) throws InvocationTargetException, IllegalAccessException {
+ Method getUIContainer = null;
+
+ // Should be the only method returning a Container in BookletContext...
+ Method[] methods = BookletContext.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ if (method.getReturnType() == Container.class) {
+ // Double check that it takes no arguments, too...
+ Class<?>[] params = method.getParameterTypes();
+ if (params.length == 0) {
+ getUIContainer = method;
+ break;
+ }
+ }
+ }
+
+ if (getUIContainer != null) {
+ BookletContext bc = Util.getBookletContext(booklet);
+ Container rootContainer = (Container) getUIContainer.invoke(bc, (Object[]) null);
+ return rootContainer;
+ } else {
+ return null;
+ }
+ }
+
+ // And this was always obfuscated...
+ // NOTE: Pilfered from KPVBooklet (https://github.com/koreader/kpvbooklet/blob/master/src/com/github/chrox/kpvbooklet/ccadapter/CCAdapter.java)
+ /**
+ * Perform CC request of type "query" and "change"
+ * @param req_type request type of "query" or "change"
+ * @param req_json request json string
+ * @return return json object
+ */
+ private static JSONObject ccPerform(String req_type, String req_json) {
+ ContentCatalog CC = Framework.getService(ContentCatalog.class);
+ try {
+ Method perform = null;
+
+ // Enumeration approach
+ Class<?>[] signature = {String.class, String.class, int.class, int.class};
+ Method[] methods = ContentCatalog.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ Class<?>[] params = method.getParameterTypes();
+ if (params.length == signature.length) {
+ int j = 0;
+ while (j < signature.length && params[j].isAssignableFrom(signature[j])) {
+ j++;
+ }
+ if (j == signature.length) {
+ perform = method;
+ break;
+ }
+ }
+ }
+
+ if (perform != null) {
+ return (JSONObject) perform.invoke(CC, new Object[]{req_type, req_json, Integer.valueOf(200), Integer.valueOf(5)});
+ } else {
+ System.err.println("Failed to find perform method!");
+ return new JSONObject();
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t.toString());
+ }
+ }
+
+ public static void updateCCDB(String tag, String path) {
+ long lastAccess = System.currentTimeMillis() / 1000L;
+ String escapedPath = JSONObject.escape(path);
+
+ // Query for the file
+ String json_query = "{\"filter\":{\"Equals\":{\"value\":\"" + escapedPath + "\",\"path\":\"location\"}},\"type\":\"QueryRequest\",\"maxResults\":1,\"sortOrder\":[{\"order\":\"descending\",\"path\":\"lastAccess\"},{\"order\":\"ascending\",\"path\":\"titles[0].collation\"}],\"startIndex\":0,\"id\":1,\"resultType\":\"fast\"}";
+ JSONObject json = Util.ccPerform("query", json_query);
+ JSONArray values = (JSONArray) json.get("values");
+ JSONObject value = (JSONObject) values.get(0);
+ String uuid = (String) value.get("uuid");
+
+ // Update the file metadata
+ String json_change = "{\"commands\":[{\"update\":{\"uuid\":\"" + uuid + "\",\"lastAccess\":" + lastAccess + ",\"displayTags\":[\"" + tag + "\"]" + "}}],\"type\":\"ChangeRequest\",\"id\":1}";
+ Util.ccPerform("change", json_change);
+ }
+}