The National Library of Finland's fork of the Palace Project Android client, which is itself the Lyrasis fork of the NYPL's Library Simplified Android client.
Image by Predrag Kezic from Pixabay
The contents of this repository provide the E-kirjasto Android client application.
Application | Module | Description |
---|---|---|
E-kirjasto | simplified-app-ekirjasto | The DRM-enabled application |
The (mostly) original Palace Project app is under simplified-app-palace
,
but it's only there for reference, and it's not used in any way for E-kirjasto.
The short version is to clone the repository recursively (including submodules),
then copy local.properties.example
to local.properties
,
and fill in the correct values.
Make sure you clone this repository with git clone --recursive
.
If you forgot to use --recursive
, then execute:
$ git submodule update --init
Install the Android SDK and Android Studio. We don't support the use of any other IDE at the moment.
Install a reasonably modern JDK, at least JDK 17. We don't recommend building on anything newer than the current LTS JDK for everyday usage.
Any of the following JDKs should work:
The JAVA_HOME
environment variable must be set correctly. You can check what it is set to in
most shells with echo $JAVA_HOME
. If that command does not show anything, adding the following
line to $HOME/.profile
and then executing source $HOME/.profile
or opening a new shell
should suffice:
# Replace NNN with your particular version of 17.
export JAVA_HOME=/path/to/jdk-17+NNN
You can verify that everything is set up correctly by inspecting the results of both
java -version
and javac -version
:
$ java -version
openjdk version "17.0.8" 2023-07-18
OpenJDK Runtime Environment (build 17.0.8+7)
OpenJDK 64-Bit Server VM (build 17.0.8+7, mixed mode)
If you wish to generate a signed APK for publishing the application, you will need to copy
a keystore to release.jks
and set the following values correctly in
local.properties
:
# Replace STOREPASSWORD, KEYPASSWORD, and KEYALIAS appropriately.
# Do NOT use quotes around values.
ekirjasto.storePassword=STOREPASSWORD
ekirjasto.keyPassword=KEYPASSWORD
ekirjasto.keyAlias=KEYALIAS
Note that APK files are only signed with the release keystore if the code is
built in release mode (debug builds are signed with debug.keystore
that's
included in the repository). In other words, you need to use either of these
commands to produce signed release APK files:
$ ./gradlew clean assembleRelease test
$ ./gradlew clean assemble test
Unlike the Palace Project, E-kirjasto does not use Adobe DRM. Instead, E-kirjasto only uses Readium LCP (liblcp) for content DRM.
The repository uses the test AAR for liblcp by default.
In order to build the app using the production version of liblcp, copy the following to local.properties and replace "test" with the correct production AAR path:
ekirjasto.liblcp.repositorylayout=/[organisation]/[module]/android/aar/test/[revision].[ext]
The app only allows logging in with strong authencation using Suomi.fi. For development, a test version of the Suomi.fi login is used, but reviewers need to be able to login into the production version of the app.
To allow this, the app has a test login accessible using a deep link.
First, set values like these in local.properties
:
ekirjasto.testLogin.enabled=true
ekirjasto.testLogin.username=TestUser
ekirjasto.testLogin.pin.base64=MTIzNDU2Nzg5MA==
Then, after closing the app (it cannot be open in the background), you can access the test login page using this deep link:
- ekirjasto://test-login
After inputting the test login credentials you defined in local.properties
,
the app will switch to the development backend and restart the app.
This will allow Google Play app reviewers to use the production app,
without using the production servers or the production Suomi.fi login.
main
is the main development branch, and is only updated through pull requests.
Release branch names follow the convention: release/<version>
(e.g. release/1.2.3
).
The repository uses continuous integration to aid development and to automate releases.
See [.github/workflows/README.md] for more information about the CI workflows.
Please see RELEASING.md for documentation on E-kirjasto's release process.
The project, as a whole, roughly follows an MVC architecture distributed over the application modules. The controller in the application is task-based and executes all tasks on a background thread to avoid any possibility of blocking the Android UI thread.
Newer application modules, roughly follow an MVVM architecture. The View Model in the application exposes reactive properties and executes all tasks on a background thread. The View observes those properties and updates on the Android UI thread.
The project makes various references to APIs and SPIs. API stands for application programming interface and SPI stands for service provider interface.
An API module defines a user-visible contract (or specification) for a module; it defines the data types and abstract interfaces via which the user is expected to make calls in order to make use of a module. An API module is typically paired with an implementation module that provides concrete implementations of the API interface types. A good example of this is the accounts database: The Accounts database API declares a set of data types and interfaces that describe how an accounts database should behave. The Accounts database implementation module provides an implementation of the described API. Keeping the API specification strictly separated from the implementation in this manner has a number of benefits:
-
Substitutability: When an API has a sufficiently detailed specification, it's possible to replace an implementation module with a superior implementation without having to modify code that makes calls to the API.
-
Testability: Keeping API types strictly separated from implementation types tends to lead to interfaces that are easy to mock.
-
Understandability: Users of modules can go straight to the API specifications to find out how to use them. This cuts down on the amount of archaeological work necessary to learn how to use the application's internal interfaces.
An SPI module is similar to an API in that it provides a specification, however the defined interfaces are expected to be implemented by users rather than called by users directly. An implementor of an SPI is known as a service provider.
A good example of an SPI is the Account provider source SPI; the SPI defines an interface that is expected to be implemented by account provider sources. The file-based source module is capable of delivering account provider descriptions from a bundled asset file. The registry source implementation is capable of fetching account provider descriptions from the NYPL's registry server. Neither the SPI or the implementation modules are expected to be used by application programmers directly: Instead, implementation modules are loaded using ServiceLoader by the Account provider registry, and users interact with the registry via a published registry API. This same design pattern is used by the NYPL AudioBook API to provide a common API into which new audio book players and parsers can be introduced without needing to modify application code at all.
Modules should make every attempt not to specify explicit dependencies on implementation modules.
API and implementation modules should typically only depend on other API modules, leaving the choice
of implementation modules to the final application assembly. In other words, a module should say
"I can work with any module that provides this API" rather than "I depend on implementation M
of a particular API". Following this convention allows us to replace module implementation without
having to modify lots of different parts of the application; it allows us to avoid
strong coupling between modules.
Most of the modularity concepts described here were pioneered by the OSGi module system and so, although the Library Simplified application is not an OSGi application, much of the design and architecture conforms to conventions followed by OSGi applications. Further reading can be found on the OSGi web site.
NOTE:
This section is partially outdated. Version numbers are defined in the ekirjasto-android-platform repository.
The build is driven by the build.gradle.kts file in the root of the project,
with the build.gradle.kts
files in each module typically only listing dependencies (the actual
dependency definitions are defined in the root build.gradle.kts
file to avoid duplicating version
numbers over the whole project). Metadata used to publish builds (such as Maven group IDs, version
numbers, etc) is defined in the gradle.properties
file in each module. The gradle.properties
file in the root of the project defines default values that are overridden as necessary by each
module.
The app uses Transifex for localizations.
Transifex overrides Android's default localization system (by overriding getString()
etc.),
and loads localized strings from txstrings.json
files in the app's assets folder.
These files are generated by the Transifex CLI tool and should not be modified manually.
The Transifex token is needed at runtime for release builds, but local translations
will work without it (the token will be replaced by an empty string).
The token should be placed in local.properties
with the following line:
transifex.token=...
The main CI build workflow automatically uploads new strings to Transifex. So, every time a PR is merged to the main branch, all new localizations are pushed to Transifex.
To manually upload strings for translation or to download translated strings,
the scripts/transifex.sh
wrapper script should be used.
Uploading strings for translation requires both the Transifex token and secret, while downloading already translated strings only requires the token:
- The Transifex token is read from the
local.properties
file mentioned above.- Alternatively, the
TRANSIFEX_TOKEN
environment variable can be used.
- Alternatively, the
- The Transifex token is also read from the
local.properties
file.- Set the secret as
transifex.secret=...
inlocal.properties
. - Alternatively, the
TRANSIFEX_TOKEN
environment variable can be used. - If the secret is not given, uploading strings for translation will be skipped.
- Set the secret as
If the token and secret are in place in local.properties
, just run:
./scripts/transifex.sh
Or optionally use environment variables to specify the token and secret:
TRANSIFEX_TOKEN="..." TRANSIFEX_SECRET="..." ./scripts/transifex.sh
We aggregate all unit tests in the simplified-tests module. Tests should be written using the JUnit 5 library, although at the time of writing we have one test that still requires JUnit 4 due to the use of Robolectric.
The project is heavily modularized in order to keep the separate application components as loosely coupled as possible. New features should typically be implemented as new modules.
The above table is generated with ReadMe.java.
The codebase uses ktlint to enforce a consistent
code style. It's possible to ensure that any changes you've made to the code
continue to pass ktlint
checks by running the ktlintFormat
task to reformat
source code:
$ ./gradlew ktlintFormat
Copyright 2015 The New York Public Library, Astor, Lenox, and Tilden Foundations,
and The National Library of Finland (Kansalliskirjasto)
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.