The main.sh
script downloads some source code, then passes control to
docker
which runs python.Dockerfile
. This configures dependencies,
patches Python, and does the build.
You can choose which Android ABIs to build for, or what Python versions
to build, by passing command line parameters. Run main.sh -h
for
details.
The shell script does nearly all of the downloading up-front. This
allows the Docker-based build process to make the best use possible of
the Docker cache. The Dockerfile does include some apt-get
calls,
which I consider an acceptable compromise of this design goal.
The Dockerfile patches the source code using sed
, a custom Python
script called patches/all/ignore_some_tests.py
, and patches that we apply
using quilt
.
It uses sed
when making changes that I do not intend to send
upstream. It is easy to use sed
to make one-line changes to
various files, and these changes are resilient to the lines
moving around slightly.
The ignore_some_tests.py
script makes a lot of changes to the Python
test suite, focusing on removing tests that do not make sense within
the context of an Android app. Most of these relate to disabling the
use of Python subprocesses to run parts of the test suite. Launching
subprocesses works properly within an Android app on some API
versions. However, the libpython
that we build requires setting the
PYTHONHOME
environment variable at the moment, so it was easier to
disable these tests than to ensure that variable is threaded through
appropriately. Another difficulty is that in more recent versions of
Android, launching subprocesses requires additional work to comply
with new sandboxing
restrictions.
Because there are a lot of tests that needed to be changed, and at the
moment I don't plan to upstream this, I consider this similar to the
use of sed
, but more powerful.
It also uses a patch which is added to the Python source tree using
quilt
. This is a patch which allows Python to use the Android system
certificates to validate TLS/SSL connections. It will probably make
sense to upstream this after some revision; however, it will not
necessarily land in the Python 3.7 branch even when upstreamed. To
learn more about using quilt, read this documentation about
quilt. If we need more patches to Python that are substantial and
may be upstreamed, relying more on quilt
might be wise.
If you attempt to run the full Python standard library test suite, it should all pass. Note that the Docker-based build also manually removes some parts of the Python standard library test suite to accommodate this goal! You can find an app to run the Python standard library test suite in Python-Android-sample-apps.
In these steps, you will:
- Download the Android SDK.
- Download/configure an appropriate version of Java.
- Configure an Android emulator.
- Generate a Python-based Android app using cookiecutter.
- Download a Python Android support ZIP file, and add that to your app. (You can build it yourself if you prefer.)
- Run the app on the Android emulator.
This will require approximately 5GB of disk space and downloads. It will require about 30 minutes of time. I have tested these instructions on macOS and Ubuntu 18.04.
On macOS, run the following commands.
$ mkdir -p ~/android/sdk && cd ~/android/sdk
$ curl -O https://dl.google.com/android/repository/sdk-tools-darwin-4333796.zip
$ unzip sdk*zip
If you’re on Linux, you’d need to use a different URL, e.g. https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip .
These URLs have existed since approximately 2017, and they have a built-in autoupdater, so I expect them to keep working for quite a few years longer.
We need the skins
folder from the JetBrains Android Studio IDE. Run the following
commands to extract them.
$ git clone --depth 1 --no-checkout https://github.com/JetBrains/android ~/android/sdk/android-ide-git
$ cd ~/android/sdk/android-ide-git
$ git archive HEAD:artwork/resources/device-art-resources -o device-art-resources.zip
$ mkdir ~/android/sdk/skins
$ cd ~/android/sdk/skins
$ unzip ~/android/sdk/android-ide-git/device-art-resources.zip
$ rm -rf ~/android/sdk/android-ide-git
Ensure you have Java 8. Look at the output of this command.
$ java -version
If macOS shows a pop-up explaining that Java is not installed, offering "More Info" and "OK," click "OK."
On macOS, if you don’t have Java, or the version is not Java 8, run these commands:
$ brew tap adoptopenjdk/openjdk
$ brew cask install adoptopenjdk8
See also: https://stackoverflow.com/a/55775566
$ export ANDROID_SDK_ROOT="${HOME}/android/sdk"
$ PATH="$PATH:${ANDROID_SDK_ROOT}/tools/bin:${ANDROID_SDK_ROOT}/emulator:${ANDROID_SDK_ROOT}/platform-tools"
$ mkdir -p ~/.android
$ touch ~/.android/repositories.cfg
$ sdkmanager --update
$ sdkmanager --licenses
$ sdkmanager 'platforms;android-28' 'system-images;android-28;default;x86' 'emulator' 'platform-tools'
Open a new terminal window/tab and run the following.
$ export ANDROID_SDK_ROOT="${HOME}/android/sdk"
$ PATH="$PATH:${ANDROID_SDK_ROOT}/tools/bin:${ANDROID_SDK_ROOT}/emulator:${ANDROID_SDK_ROOT}/platform-tools"
$ avdmanager --verbose create avd --name robotfriend --abi x86 --package 'system-images;android-28;default;x86' --device pixel
$ echo 'disk.dataPartition.size=4096M' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ echo 'hw.keyboard=yes' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ echo 'skin.dynamic=yes' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ echo 'skin.name=pixel_3a' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ echo 'skin.path=skins/pixel_3a' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ echo 'showDeviceFrame=yes' >> $HOME/.android/avd/robotfriend.avd/config.ini
$ emulator @robotfriend
The emulator command will open an Android emulator, and will block your terminal window.
Note: If you find your emulator lacks Internet access, and you are OK using a third-party DNS server, you can run this:
$ emulator @robotFriend -dns-server 1.1.1.1,8.8.8.8
In your original terminal, run the following commands.
$ python3 -m pip install --user cookiecutter
$ mkdir -p ~/projects/sample-app
$ cd ~/projects/sample-app
$ python3 -m cookiecutter https://github.com/beeware/briefcase-android-gradle-template
Now, in a web browser, visit this URL:
https://drive.google.com/uc?export=download&id=1Bsr_3VMkEez5VWHq2tjjwl8xwHpffIcb
And download 3.7.zip.
Back in a terminal, run:
$ cd MyApp # or whatever you said for project_name above
$ unzip ~/Downloads/3.7.zip
Finally, we need to create some sample Python code.
I'm assuming you called your app my_app
in the earlier sections.
Create a file called app/src/main/assets/python/my_app/__init__.py
with the following content:
from rubicon.java import JavaClass, JavaInterface
IPythonApp = JavaInterface('org/beeware/android/IPythonApp')
class Application(IPythonApp):
def onCreate(self):
print('called Python onCreate()')
def onStart(self):
print('called Python onStart()')
def onResume(self):
print('called Python onResume()')
Create another file called app/src/main/assets/my_app/__main__.py
with the following content.
from . import Application
from rubicon.java import JavaClass
activity_class = JavaClass('org/beeware/android/MainActivity')
app = Application()
activity_class.setPythonApp(app)
print('Python app launched & stored in Android Activity class')
Run this in a terminal.
$ ./gradlew installDebug
After about 3 minutes of waiting, the command should successfully exit. Note that this command will be faster any future times you run it.
In the emulator, find the circle icon at the bottom, next to the back icon. Drag the circle icon up, and look for MyApp. Click it.
After about 10 seconds, you will see your app name visible. This means the app is launched.
Now, let’s look through the Android log to find evidence that our app launched. We’re looking for “called Python onCreate()” in the following output.
$ adb logcat -d | grep -i python
TA-DA! It works.
You may notice that the Android image looks somewhat unstyled. This is the fastest to download Android image; it contains all of fully open source Android APIs, but it lacks Google’s additional APIs. I’ve tested it, and the app displays properly this way. Based on my research I expect that all APIs we will wrap will continue to work properly with this Android image.