All Manuals > LispWorks® User Guide and Reference Manual > 16 Android interface

16.4 The Othello demo for Android

The Othello demo is a simple Android app showing the basics of using the LispWorks for Android Runtime. It is a full Android project that can be imported into Android Studio.

The application plays the Othello game as an example of an application. When delivering "with Lisp" (see 16.4.2 Delivering LispWorks to the project below), it also allows the user to type and evaluate Lisp forms. This is useful during development.

The example also demonstrates how to create two separate APK files, one for ARM 64-bit machines and one for ARM 32-bit. This is useful to reduce the size of the APK that users need to load. For playing with the demonstration, you need only one of the architectures, so can skip steps that are specific to one of the architectures. See 16.1.2 ABI splitting using flavors in the OthelloDemo for a discussion of this mechanism in the demonstration.

There following file contains information about the example:

(example-edit-file "android/README.txt")

To try the demo, you need to do these steps:

  1. Create an Android Studio project containing the demo Android code.
  2. Deliver the LispWorks application to it.
  3. Build and install the application and run it.

These steps are described in detail in the following sections.

16.4.1 Creating an Android Studio project

The Android project code is in the OthelloDemo directory, which is the directory examples/android/OthelloDemo inside the LispWorks distribution. You need to make a project with this code.

First, copy the contents of OthelloDemo directory recursively to some other directory where Android Studio can write (this is needed because the examples directory is supposed to be read-only). The new copy is referred to below as the "project directory".

In Android Studio, select File -> New -> Import Project..., or the "Import project" item in the "Welcome to Android Studio" dialog, (in Android Studio 3.3.1, the exact text of the item is "Import project (Gradle, Eclipse ADT, etc.)"). This raises a dialog asking for the project to import. Enter the full path of the project directory that you copied above. Once you have imported the project, it can be built and run, but it does not have the Lisp parts yet, so the application just gives an error on start up that it fails to find the library. You will need to deliver the Lisp part as described in 16.4.2 Delivering LispWorks to the project below before the application will work.

16.4.2 Delivering LispWorks to the project

To deliver LispWorks, copy one of the build script files deliver-android-othello.lisp or deliver-android-othello-with-lisp.lisp from the examples/android directory in the LispWorks distribution. In the copied file, change the value of the variable *project-path* to point to your project directory, that is the copy of OthelloDemo from the previous section. For example:

(defvar *project-path* "~/my-workspace/LispWorksRuntimeDemo/")

You will then use your edited copy of the build script as the -build command line argument to LispWorks.

The Android delivery images are called lispworks-8-0-0-arm-linux-android, lispworks-8-0-0-arm64-linux-android, lispworks-8-0-0-x86-linux-android and lispworks-8-0-0-amd64-linux-android, which must be run on an appropriate architecture on Linux or macOS. The images can be run using an emulator such as QEMU if necessary using the script examples/android/run-lw-android.sh as follows:

run-lw-android.sh -build <path-to-a-modified-copy-of>/deliver-android-othello.lisp

If you run this on an x86 Linux machine it will also build images for the 32-bit and 64-bit x86 architectures. To run these with the x86 Android Emulator, you will also need to uncomment the x86 flavors in the Gradle file app/build.gradle inside the Android project.

See deliver-to-android-project for details of the delivery process. Note that the script run-lw-android.sh tries to deliver all four combinations of 64-bit/32-bit and x86/ARM . This creates two files for each architecture (relative to the project directory):

ARM 32-bit

app/src/armeabiv7a/assets/libLispWorks.so.armeabiv7a.lwheap
app/src/armeabiv7a/jniLibs/armeabi-v7a/libLispWorks.so

ARM 64-Bit

app/src/arm64v8a/assets/libLispWorks.so.arm64v8a.lwheap
app/src/arm64v8a/jniLibs/arm64-v8a/libLispWorks.so

x86 32-bit

app/src/x86/assets/libLispWorks.so.x86.lwheap
app/src/x86/jniLibs/x86/libLispWorks.so

x86 64-Bit

app/src/x86_64/assets/libLispWorks.so.x86_64.lwheap
app/src/x86_64/jniLibs/x86_64/libLispWorks.so

If you cannot access the project directory from the Linux or macOS machine:

  1. In a suitable directory that is accessible on the Linux or macOS machine (the "deliv directory"), create four sub-directories called armeabiv7a, arm64v8a, x86 and x86_64. These specific names are recognized by deliver-to-android-project.
  2. Change *project-path* in the build script to the "deliv directory" and deliver using run-lw-android.sh as above. deliver-to-android-project will recognize the sub-directories and write the files into them.
  3. If you can access the "deliv directory" from the the machine on which you run Android Studio, then use a sourceSets block in the app/build.gradle file in the Android Studio project directory to map the source sets names arm64v8a, armeabiv7a, x86 and x86_64 to the sub-directories in the "deliv directory". The example already contains such a block which is commented out. Uncomment it and edit it as needed.

    If you cannot access the "deliv directory" from the the machine on which you run Android Studio, then recursively copy the contents of the sub-directory arm64v8a from the "deliv directory" to the app/src/arm64v8a sub-directory inside the Android Studio project directory, and similarly for the sub-directories armeabiv7a, x86 and x86_64, so you will end up with the same files that are listed above for the four architectures.

16.4.3 Running the application

Once you have the project with the LispWorks files, you can build, install it on the device and run it as any other Android project. When it runs, It first shows a splash screen (the LispWorks splash screen image) and then the first screen displays an Othello board, where you can play against the computer (you play black), by touching the square where you want to add your piece.

The display has two elements in addition to the board:

It also has a menu (which maybe partly displayed on the action bar), with these items:

Restart
Restart the game.
Undo

Undo the last move. You can undo repeatedly to the beginning of the game.

When the computer plays, each undo undoes to the state before your last move.

When the computer does not play, it undoes one move.

When delivering "with Lisp" the menu also has these items:

Lisp Panel
Takes you to the Lisp Panel screen, which allows you to evaluate Lisp forms. See below in the description of the Lisp Panel.
Command history

Takes you a list of the forms that that you evaluated. It is initialized by a few demo forms. See below about the History list.

Othello Server
Raises a submenu with three items: Java server, full proxy and lazy proxy. Switching between these changes the mechanism by which Java calls into Lisp. The behavior of the game is exactly the same, only the output to the Lisp Panel or Output is different. This feature is for demonstrating different techniques of calling from Java to Lisp. See discussion of the code for details.

When delivering "without Lisp" the menu also has these items:

Output
Takes you to the "output" screen.
16.4.3.1 The Lisp Panel screen

The Lisp Panel contains a row of buttons, a text view for input, and the bottom is a text view for output. This screen is available only when delivering "with Lisp". When delivering "without Lisp", there is the Output screen instead.

The buttons are:

Clear
Clears all the output from the output pane.
Evaluate the string

Send the current text in the input pane to Lisp by a direct call to eval-for-android.

eval-for-android is defined in (example-edit-file "android/android-othello-user"). It reads the string and evaluates it. If it is successful, it prints to the output pane the form, anything the form printed, and the result(s). If there is an error, it logs the error and prints the error message to the output pane.

History

Takes you to another screen which displays a list of the forms that were evaluated. The list is initialized by some forms which demonstrate some features of the multiprocessing on Android. See below in the section 16.4.3.2 Prepared forms. Whenever you evaluate a form by pressing Evaluate the string, it adds the form to the history in the beginning of it. If the form matches exactly a form which is already in the list, the old item is removed.

In the history list, when you touch an item it is inserted into the input pane, and the application switches to the Lisp Panel. It does not evaluate the form at that point.

You can also reach the history list from the menu in the Othello screen.

Bug form logs
Invokes com.lispworks.Manager.showBugFormLogs. This shows another screen with a list of the logged errors displaying the error string for each item. Touching an item opens another screen with bug form log of this error.
Clear logs
Clears all the bug form logs, including removing the files.

The input pane below the buttons is just a passive text view, in which you can type Lisp forms, and evaluate by touching the Evaluate the string button.

The bottom part of the Lisp Panel, in the Output screen when delivering "without Lisp", is the output pane. It prints the output of evaluation as above. It also prints whenever you touch a square in the Othello board. When the Full or Lazy proxy is used for communication, it also prints this fact.

16.4.3.2 Prepared forms

Initially, the History list contains the forms described below. When using forms, note that evaluating a form moves it to the top of the list. When you should evaluate more than one of these forms in order, you will need to look down the list for each one in turn.

The idea is that you can try these forms, and then modify them to check and perform things that you need to do when debugging your application.

Forms:

  1. (mp:ps) 
    

    Shows the Lisp processes. Initially there are at least the idle process and the GUI process which displays as "created by foreign code".

  2. (setq *computer-plays-waste-time-in-seconds* 2) 
    

    That causes the computer to pretend that it takes it time to compute a move. When playing against the computer after setting this, you will see that after your move, the display says "Computer to play" for two seconds before it actually plays. Set *computer-plays-waste-time-in-seconds* back to nil to make it behave normally.

  3. (defun eval-and-print (form)
      (let ((res (eval form)))
        (lw-ji:send-message-to-java-host
         (princ-to-string res) :reset)))
    

    Defines a function to be used by the next two forms. Note that it uses send-message-to-java-host to print, which comes in the output and works on any thread. When it is on the current thread it will end up printing before the printing of the evaluation, but on another thread it is random which output comes first.

  4. (eval-and-print '(mp:get-current-process)) 
    

    Use the function defined above to print the process in the current thread. That is the GUI process.

  5. (mp:funcall-async 'eval-and-print '(mp:get-current-process)) 
    

    Use the function eval-and-print defined above to print the process on which funcall-async executes the function. This will be one of the Background Execute processes.

  6. (progn 
      (defun loop-executing-events ()
        (loop
         (let ((event (mp:process-wait-for-event)))
           (lw-ji:format-to-java-host "~%got event ~s" event)
           (let ((res (mp:general-handle-event event)))
             (lw-ji:format-to-java-host
              "~%Handling got ~s" res)))))
      (setq loop-executing-events-process
            (mp:process-run-function "Loop Execute Events" ()
                                     'loop-executing-events))) 
    

    Create a process called "Loop Execute Events" and set loop-executing-events-process to it. The process has a process function loop-executing-events which read events and handles them using process-wait-for-event and format-to-java-host. It prints "got event <event>" and then "handling got <result of handling>". Note the usage of format-to-java-host, which prints to the output pane too (it actually calls send-message-to-java-host).

  7. (mp:process-send loop-executing-events-process '(mp:get-current-process)) 
    

    Sends to the "Loop Execute Events" process (that started in the previous step) an event, which cause get-current-process to be called, and hence return the process. You should see "got event (MP:GET-CURRENT-PROCESS)" and "Handling got <process name>".

  8. (othello-user-change-a-square 5 2) 
    

    Changes square 5 (sixth from the left in the top row) to color 2 (black). This function is defined in (example-edit-file "android/android-othello-user") and is part of the "interface" that the Lisp Othello code uses to tell Java to change the board.

  9. (mp:process-run-function
     "multiplier" ()
     #'(lambda()
         (setq *finish-multiply* nil)
         (dotimes (x 100)
           (sleep 1)
           (when *finish-multiply* (return))
           (lw-ji:format-to-java-host
            \"~%~d * ~d = ~d\"
            x x (* x x))))) 
    

    Starts a process that performs "a lengthy computation" (simulated by using (sleep 1)) and prints results while doing it. In each "step in the computation" (the cl:dotimes iteration) it prints the square of the iteration number. To stop it, evaluate the next form.

  10. (setq *finish-multiply* t) 
    

    Tell the "multiplier" process (see above) to stop.

  11. (mp:process-run-function 
     "Error"
     () #'(lambda () (open "junk;;file::name")))
    

    Starts another process that gets an error (because the argument to cl:open is an illegal pathname). It prints that it got the error, and you can use the Bug form logs button to look at the bug form log.

  12. (raise-alert-dialog
     "What do you want to eat?" +
     :ok-title "Chicken " 
     :ok-callback '(raise-alert-dialog "Here is some chicken") + 
     :cancel-title "Salad "
     :cancel-callback '(raise-alert-dialog "We do not have salad"))
    

    Raises an alert dialog using raise-alert-dialog which is defined in dialog.lisp. Note that this works because the LispPanel class uses com.lispworks.Manager.setCurrentActivity to set the current activity.

  13. (raise-a-toast "Bla Bla Bla" :gravity :left) 
    

    Raises an Android "toast" at the middle of the left side, using raise-a-toast which is defined in toast.lisp.

16.4.4 Lisp interface usage in the Java code

The Othello Demo Java code is in the package com.lispworks.example.othellodemo. LispWorks interfaces in Java are all in the package com.lispworks. The methods appear in full, to make it is easy to see where there is a call to the LispWorks interface.

16.4.4.1 Class Othello

Othello is a subclass of Activity that displays the screen with the Othello board. The display is all in standard Java. The board is made of a grid of 64 ImageView panes, each one displaying one of three images (blank, white, black). Each view has an OnClickListener(SquareListener) that remembers its index and passes it when clicked.

The Java code does not know anything about the game that is being played, and does not keep a record of the state of the game. That is all done in Lisp.

The Java code processes user gestures concerning the game (touching the board, and touching any of the buttons and items Computer plays, undo move, restart) by calling methods on an object that implements the nested interface OthelloServer, which is kept in mOthelloServer. The object can be either a Lisp proxy, or of the nested class JavaOthelloServer. All of these objects do exactly the same thing (calling the Lisp functions defined in (example-edit-file "misc/othello")), and the purpose of having all these options is to demonstrate different techniques to call into Lisp. There is also a nested class ErrorOthelloServer in case LispWorks does not work, which displays the error. mOthelloServer is set by the method setupServer.

The nested class JavaOthelloServer is plain Java with methods that call into Lisp using the 15.3.1 Direct calls interface (com.lispworks.LispCalls.callIntV and com.lispworks.LispCalls.callVoidV). This has the advantage that on the Lisp side all you have to do is to ensure that the functions are not shaken, which you can do with hcl:deliver-keep-symbols (see the Delivery User Guide). It has the disadvantage that you hardwire Lisp function names in Java (though the names can be variables too).

The other two possible implementations of the OthelloServer are Lisp proxies which are defined in Lisp (in examples/android/android-othello-user.lisp). See the discussion of the Lisp code for more details. The code in setupServer demonstrates two techniques of using the proxy definitions: either calling a Lisp function that makes a proxy (using com.lispworks.LispCalls.callObjectV to call create-lisp-othello-server), or using com.lispworks.LispCalls.createLispProxy with the name of the proxy definition (lisp-othello-server-lazy) to create a proxy.

To actually respond to moves, the Othello class exports 3 methods ("updateState", "signalBadMove" and "change") which are called directly from Lisp to change the board and the status text.

When an Othello instance is created, it calls setupAndInit to do anything with Lisp (mainly call mOthelloServer.init). Before doing anything that may interact with Lisp, it checks the status of Lisp using com.lispworks.Manager.status. If Lisp is not ready and there was no error, it calls com.lispworks.Manager.init to initialize LispWorks, passing it a Runnable that with call setupAndInit again to actually do the initialization. In the Demo the Lisp side will already be initialized, because it is done by the LispWorksRuntimeDemo activity, but the Othello class avoids relying on it.

When LispWorks is ready, setupAndInit sets up the server by calling setupServer and initializes the game by calling mOthelloServer.init.

If there is an error, setupAndInit gets the error details using com.lispworks.Manager.mInitErrorString, and com.lispworks.Manager.init_result_code and adds a message, set mOthelloServer to ErrorOthelloServer, and then shows the Lisp Panel which will be displaying the error.

There is also an onCreateOptionsMenu method which checks whether Lisp is working and can evaluate forms (using LispPanel.canEvaluate), and accordingly decides which menu to use.

16.4.4.2 Class LispPanel

LispPanel is a subclass of Activity that displays the Lisp panel, or just the output when delivering "without Lisp" (see 16.4.2 Delivering LispWorks to the project).

The main purpose of the Lisp Panel is to evaluate Lisp forms, which it does by calling the Lisp function eval-for-android using com.lispworks.LispCalls.callIntV. That can work only if eval-for-android is defined, so LispPanel has a method canEvaluate that works by checking if eval-for-android is defined using com.lispworks.LispCalls.checkLispSymbol. If eval-for-android is fbound, LispPanel displays in full, otherwise it shows only output TextView.

LispPanel is also responsible for displaying messages in its output TextView. To achieve that, it uses com.lispworks.Manager.setTextView. Once it sets the TextView, all calls to com.lispworks.Manager.addMessage and calls to the Lisp functions send-message-to-java-host and format-to-java-host put their output in this TextView.

Other usage of the com.lispworks package in LispPanel are:

16.4.4.3 Class MyApplication

MyApplication is not actually used in the demo. It is a demonstration of how to initialize LispWorks when the application starts, by calling com.lispworks.Manager.init in the onCreate of the application. The demo itself does not use this mechanism. Instead the SplashScreen activity does it, and the Othello activity also checks using com.lispworks.Manager.status, and if LispWorks needs initializing does it.

16.4.4.4 Class LispWorksRuntimeDemo

Display a splash screen and initialize the Lisp side, by checking com.lispworks.Manager.status and using com.lispworks.Manager.init if needed. The purpose of this class is just to give an example of displaying a splash screen while initializing Lisp. It is not really needed, because the Othello class checks too (in setupAndInit). On Eclipse the name of this class is the default project name.

16.4.4.5 Class History

A simple class to display Lisp forms. Does not do anything related to Lisp.

16.4.4.6 Class SquareLayout

A simple class to make a square layout for displaying the Othello board. Does not do anything related to Lisp.

16.4.5 Java and Android interface in the Lisp code

The file:

(example-edit-file "misc/othello")

is a generic implementation of the playing Othello part, and has nothing to do with Java or Android.

The Lisp code that interacts with Java and Android to play Othello and evaluate the forms is in:

(example-edit-file "android/android-othello-user")

The Java callers to update the game are defined by a define-java-caller form. All these methods need to be called on the GUI thread (because they interact with GUI elements), so the actual functions that are called from the Othello code are defined to call the Java callers using android-funcall-in-main-thread.

The function eval-for-android is what the Java code uses to evaluate Lisp forms. The function has no Java-specific features, but it has error handling and binding of some of the top-level variables like cl:* to make it more usable in repeated calls from "outside".

The code also defines two proxy definitions that implement the Othello.OthelloServer interface which responds to user gestures. To demonstrate the various features of proxies, there are two definitions which achieve exactly the same thing. The full proxy definition (lisp-othello-server-full) specifies functions for all the methods that the interface defines. The lazy (programmer) proxy definition does not define any method. Instead it has a default function that decides what to do based on the method name.

Note that the Othello logic can also be run via a desktop application using:

(example-edit-file "capi/applications/simple-othello")

The two files:

(example-edit-file "android/dialog")

and:

(example-edit-file "android/toast")

define the functions raise-alert-dialog and raise-a-toast respectively, to demonstrate using Android code directly from Lisp. See the comments in these files.


LispWorks® User Guide and Reference Manual - 01 Dec 2021 19:30:21