diff options
| author | notori <188390306+n0tori@users.noreply.github.com> | 2025-03-13 12:28:08 +0000 |
|---|---|---|
| committer | notori <188390306+n0tori@users.noreply.github.com> | 2025-03-13 12:28:08 +0000 |
| commit | 3f80ccbfc0dd2f3b4295e506e7ce5931b720e2ee (patch) | |
| tree | 17b44f56b7aaec5247dbed939e2f567a55b83ce0 | |
project files
| -rw-r--r-- | .gitignore | 7 | ||||
| -rw-r--r-- | install.sh | 22 | ||||
| -rw-r--r-- | pom.xml | 254 | ||||
| -rw-r--r-- | src/kindle/install/appreg.install.sql | 19 | ||||
| -rw-r--r-- | src/kindle/install/install.sh | 48 | ||||
| -rw-r--r-- | src/kindle/install/whispertouch.install.sql | 1 | ||||
| -rw-r--r-- | src/kindle/libotautils5 | 382 | ||||
| -rw-r--r-- | src/kindle/uninstall/appreg.uninstall.sql | 8 | ||||
| -rw-r--r-- | src/kindle/uninstall/uninstall.sh | 37 | ||||
| -rw-r--r-- | src/main/filtered/version.txt | 3 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/KGOLBooklet.java | 138 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Main.java | 35 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/MainScreen.java | 185 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/MainUI.java | 329 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Panel.java | 249 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Util.java | 134 | ||||
| -rw-r--r-- | uninstall.sh | 22 |
17 files changed, 1873 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c01833 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +concierge-1.0.0.jar +json_simple-1.1.jar +kaf.jar +kafui.jar +utilities.jar +kindletool.exe +target
\ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ea48d4a --- /dev/null +++ b/install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Script to copy install binary to D:/mrpackages and eject the drive + +# Copy the install binary +cp target/Update_KGameOfLife_GameOfLife_install.bin D:/mrpackages/ + +# Eject the drive +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + eject D: +elif [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + diskutil eject D: +elif [[ "$OSTYPE" == "msys"* ]] || [[ "$OSTYPE" == "cygwin"* ]] || [[ "$OSTYPE" == "win32"* ]]; then + # Windows + powershell -command "& {(New-Object -comObject Shell.Application).Namespace(17).ParseName('D:').InvokeVerb('Eject')}" +else + echo "Unsupported OS for drive ejection. Please eject drive D: manually." +fi + +echo "Install binary copied to D:/mrpackages and drive ejected (if supported)."
\ No newline at end of file @@ -0,0 +1,254 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>uk.co.notori</groupId> + <artifactId>GameOfLife</artifactId> + <packaging>jar</packaging> + <version>1.0-SNAPSHOT</version> + <name>uk.co.notori.gol</name> + <properties> + <timestamp>${maven.build.timestamp}</timestamp> + <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format> + </properties> + <build> + <resources> + <resource> + <directory>src/main/filtered</directory> + <filtering>true</filtering> + </resource> + </resources> + <finalName>GameOfLife</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.0</version> + <configuration> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <id>unpack-dependencies</id> + <phase>prepare-package</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <excludes>**</excludes> + <excludes>META-INF/MANIFEST.MF</excludes> + <outputDirectory>${project.build.directory}/classes</outputDirectory> + <overWriteReleases>false</overWriteReleases> + <overWriteSnapshots>false</overWriteSnapshots> + <overWriteIfNewer>true</overWriteIfNewer> + <includeScope>runtime</includeScope> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.1.1</version> + <configuration> + <excludes> + <exclude>**/log4j.properties</exclude> + </excludes> + <archive> + <manifest> + <!-- Jar file entry point --> + <mainClass>uk.co.notori.gol.KGOLBooklet</mainClass> + <addClasspath>true</addClasspath> + <classpathPrefix>lib/</classpathPrefix> + </manifest> + </archive> + </configuration> + </plugin> + + <plugin> + <artifactId>maven-resources-plugin</artifactId> + <version>3.1.0</version> + <executions> + <execution> + <id>copy-kindle-install</id> + <phase>install</phase> + <goals> + <goal>copy-resources</goal> + </goals> + + <configuration> + <outputDirectory>${project.build.directory}/booklet/install</outputDirectory> + <resources> + <resource> + <directory>src/kindle/install</directory> + <includes> + <include>*.*</include> + </includes> + </resource> + <resource> + <directory>src/kindle</directory> + <includes> + <include>libotautils5</include> + </includes> + </resource> + <resource> + <directory>${project.build.directory}</directory> + <includes> + <include>GameOfLife.jar</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + <execution> + <id>copy-kindle-uninstall</id> + <phase>install</phase> + <goals> + <goal>copy-resources</goal> + </goals> + + <configuration> + <outputDirectory>${project.build.directory}/booklet/uninstall</outputDirectory> + <resources> + <resource> + <directory>src/kindle/uninstall</directory> + <includes> + <include>*.*</include> + </includes> + </resource> + <resource> + <directory>src/kindle</directory> + <includes> + <include>libotautils5</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> + + + <!-- package into install bundles --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.5.0</version> + <executions> + <execution> + <id>kindletool.install</id> + <phase>install</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <executable>kindletool</executable> + <workingDirectory>${pom.build.directory}</workingDirectory> + + <commandlineArgs>create ota2 -xPackageName=${project.artifactId} + -xPackageVersion=${project.version}_${buildNumber} + -xPackageAuthor=ieb -xPackageMaintainer=ieb -X + -d kindle5 -s 1679530004 + -C ${project.build.directory}/booklet/install + ${project.build.directory}/Update_KGameOfLife_${project.artifactId}_install.bin + </commandlineArgs> + + </configuration> + </execution> + <execution> + <id>kindletool.hotfix</id> + <phase>install</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <executable>kindletool</executable> + <workingDirectory>${project.build.outputDirectory}</workingDirectory> + <commandlineArgs>create ota2 -d paperwhite2 -d basic -d voyage -d paperwhite3 + -d oasis -d basic2 -d oasis2 -d paperwhite4 -d basic3 + -d oasis3 -O -s 3556150002 -C ${project.build.directory}/booklet/install + ${project.build.directory}/Update_KGameOfLife_${project.artifactId}_install-hotfix.bin + </commandlineArgs> + </configuration> + </execution> + <execution> + <id>kindletool.uninstall</id> + <phase>install</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <executable>kindletool</executable> + <workingDirectory>${project.build.outputDirectory}</workingDirectory> + <commandlineArgs>create ota2 -xPackageName=${project.artifactId} + -xPackageVersion=${project.version}_${buildNumber} -xPackageAuthor=ieb + -xPackageMaintainer=ieb -X -d kindle5 + -C ${project.build.directory}/booklet/uninstall + ${project.build.directory}/Update_KGameOfLife_${project.artifactId}_uninstall.bin + </commandlineArgs> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.jmdns</groupId> + <artifactId>jmdns</artifactId> + <version>3.5.5</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.25</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.13.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>kindle-sdk</groupId> + <artifactId>json_simple</artifactId> + <version>1.1</version> + <scope>system</scope> + <systemPath>${project.basedir}/../lib/json_simple-1.1.jar</systemPath> + </dependency> + <dependency> + <groupId>kindle-sdk</groupId> + <artifactId>concierge</artifactId> + <version>1.0.0</version> + <scope>system</scope> + <systemPath>${project.basedir}/../lib/concierge-1.0.0.jar</systemPath> + </dependency> + <dependency> + <groupId>kindle-sdk</groupId> + <artifactId>kaf</artifactId> + <version>1.0.0</version> + <scope>system</scope> + <systemPath>${project.basedir}/../lib/kaf.jar</systemPath> + </dependency> + <dependency> + <groupId>kindle-sdk</groupId> + <artifactId>utilities</artifactId> + <version>1.0.0</version> + <scope>system</scope> + <systemPath>${project.basedir}/../lib/utilities.jar</systemPath> + </dependency> + <dependency> + <groupId>kindle-sdk</groupId> + <artifactId>kafui</artifactId> + <version>1.0.0</version> + <scope>system</scope> + <systemPath>${project.basedir}/../lib/kafui.jar</systemPath> + </dependency> + </dependencies> +</project> 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); + } +} diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..66e7fbd --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Script to copy uninstall binary to D:/mrpackages and eject the drive + +# Copy the uninstall binary +cp target/Update_KGameOfLife_GameOfLife_uninstall.bin D:/mrpackages/ + +# Eject the drive +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + eject D: +elif [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + diskutil eject D: +elif [[ "$OSTYPE" == "msys"* ]] || [[ "$OSTYPE" == "cygwin"* ]] || [[ "$OSTYPE" == "win32"* ]]; then + # Windows + powershell -command "& {(New-Object -comObject Shell.Application).Namespace(17).ParseName('D:').InvokeVerb('Eject')}" +else + echo "Unsupported OS for drive ejection. Please eject drive D: manually." +fi + +echo "Uninstall binary copied to D:/mrpackages and drive ejected (if supported)."
\ No newline at end of file |
