Make sure you clone this repository with git clone --recursive
.
If you forgot to use --recursive
, then execute:
$ git submodule init
$ git submodule update --remote --recursive
$ echo "org.gradle.internal.publish.checksums.insecure=true" >> "$HOME/.gradle/gradle.properties"
$ ./gradlew clean assembleDebug test publishToMavenLocal
Astute readers may have noticed the org.gradle.internal.publish.checksums.insecure
property
in the initial build instructions. This is necessary because Gradle 6 currently publishes
checksums that Maven Central doesn't like.
Until Maven Central is updated to accept SHA256 and SHA512 checksums, this flag is necessary.
As all artifacts published to Maven Central are PGP signed, this is not a serious issue; PGP
signatures combine integrity checking and authentication, so checksum files are essentially
redundant nowadays.
The project is divided into separate modules. Programmers wishing to use the API will primarily be concerned with the Core API, but will also need to add providers to the classpaths of their projects in order to actually do useful work. The API is designed to make it easy to develop an event-driven user interface, but this project also includes a ready-made player UI that can be embedded into applications. Additionally, audio engine providers that do not, by themselves, handle downloads require callers to provide a download provider. Normally, this code would be provided directly by applications (as applications tend to have centralized code to handle downloads), but a simple implementation is available to ease integration.
The project currently uses com.io7m.changelog to manage release changelogs.
- Download (or synthesize) an audio book manifest. Hadrien Gardeur publishes many example manifests in formats supported by the API.
- Ask the API to parse the manifest.
- (Optional) Ask the API to perform license checks.
- (Optional) Configure any player extensions you may want to use.
- Ask the API to create an audio engine from the parsed manifest.
- Make calls to the resulting audio book to download and play individual parts of the book.
See the provided example project for a complete example that is capable of downloading and playing audio books.
At a minimum, applications will need the Core API, one or more manifest parser implementations, and one or more audio engine implementations. Use the following Gradle dependencies to get a manifest parser that can parse the Readium WebPub manifest format, and an audio engine that can play non-encrypted audio books:
ext {
nypl_audiobook_api_version = "4.0.0-SNAPSHOT"
}
dependencies {
implementation "org.librarysimplified.audiobook:org.librarysimplified.audiobook.manifest_parser.webpub:${nypl_audiobook_api_version}"
implementation "org.librarysimplified.audiobook:org.librarysimplified.audiobook.api:${nypl_audiobook_api_version}"
implementation "org.librarysimplified.audiobook:org.librarysimplified.audiobook.open_access:${nypl_audiobook_api_version}"
}
The API is expected to follow semantic versioning.
The API uses a service provider model in order to provide strong modularity and to decouple consumers of the API from specific implementations of the API. To this end, the API uses ServiceLoader internally in order to allow new implementations of both manifest parsers and audio engines to be registered and made available to client applications without requiring any changes to the application code.
An audio book is typically delivered to the client via a manifest. A manifest is normally a JSON description of the audio book that includes links to audio files, and other metadata. It is the responsibility of a manifest parser to turn a JSON AST into a typed manifest data structure defined in the Core API.
Programmers should make calls to the ManifestParsers
class, passing in a byte array representing (typically) the raw text of a JSON manifest. The methods return a
PlayerResult
value providing either the parsed manifest or a list of errors indicating why parsing
failed. The ManifestParsers
class asks each registered manifest parser
whether or not it can parse the given raw data and picks the first one that claims that it can.
Programmers are not intended to have to use instances of the PlayerManifestParserType
directly.
Programmers will generally not need to create new manifest parsers, but will instead use one or more of the provided implementations. However, applications needing to use a new and unsupported manifest format will need to provide and register new manifest parser implementations.
In order to add a new manifest parser, it's necessary to define a new class that implements
the PlayerManifestParserType
and defines a public, no-argument constructor. It's then necessary to register this class so that
ServiceLoader
can find it by creating a resource file at
META-INF/services/org.librarysimplified.audiobook.manifest_parser.api.ManifestParserProviderType
containing the fully
qualified name of the new class. The standard WebPubParserProvider
class and its associated service file
serve as minimal examples for new parser implementations. When a jar
(or aar
) file is placed on
the classpath containing both the class and the service file, ServiceLoader
will find the
implementation automatically when the user asks for parser implementations.
Parsers are responsible for examining the given JSON AST and telling the caller whether or not they think that they are capable of parsing the AST into a useful structure. For example, audio engine providers that require DRM might check the AST to see if the required DRM metadata structures are present. The Core API will ask each parser implementation in turn if the implementation can parse the given JSON, and the first implementation to respond in the affirmative will be used. Implementations should take care to be honest; an implementation that always claimed to be able to parse the given JSON would prevent other (possibly more suitable) implementations from being considered.
The API allows for opt-in license checking. Once a manifest has been parsed, programmers can execute license checks on the manifest to verify if the listening party actually has permission to hear the given audio book.
Individual license checks are provided as implementations of the
SingleLicenseCheckProviderType
type. Programmers should pass in a list of desired single license check providers
to the LicenseChecks
API for execution. The LicenseChecks
API returns a list of the results
of license checks, and provides a simple true/false
value indicating
whether or not playing should be permitted.
An audio engine is a component that actually downloads and plays a given audio book.
Given a parsed manifest, programmers should make calls to the methods
defined on the PlayerAudioEngines
class. Similarly to the PlayerManifests
class, the PlayerAudioEngines
class will ask each
registered audio engine implementation in turn if it is capable of
supporting the book described by the given manifest. Please consult the documentation for that
class for information on how to filter and/or prefer particular implementations. The
(somewhat arbitrary) default behaviour is to select all implementations that claim to be able to
support the given book, and then select the implementation that advertises the highest version number.
Implementations must implement the PlayerAudioEngineProviderType interface and register themselves in the same manner as manifest parsers.
Creating a new audio engine provider is a fairly involved process. The provided ExoPlayer-based implementation may serve as an example for new implementations.
In order to reduce duplication of code between audio engines, the downloading of books is abstracted out into a PlayerDownloadProviderType interface that audio engine implementations can call in order to perform the work of actually downloading books. Implementations of this interface are actually provided by the calling programmer as this kind of code is generally provided by the application using the audio engine.
Audio engines may support extensions
that allow for augmenting the behaviour of existing implementations. This
is primarily useful for, for example, adding unusual authentication
mechanisms that may be required by book distributors when downloading
book chapters. A list of extensions may be passed in to the create
method of the PlayerAudioBookProviderType
interface. Extensions must be explicitly passed in in order to be
used; passing in an empty list results in no extensions being used.
The API comes with a set of Android views and fragments that can be embedded into an application to provide a simple user interface for the player API.
- Declare an
Activity
that implements the PlayerFragmentListenerType. - Load a
PlayerFragment
instance into the activity.
Please consult the provided example project
and the documentation comments on the PlayerFragmentListenerType
for details.
The project contains numerous unit tests, many of which are designed to run both locally and on real or emulated devices. The reason for this is that, during development, it's desirable to be able to run the tests locally to quickly experiment with changes; running the entire suite on the local machine takes just a few seconds. However, prior to deployment, it's both desirable and necessary to run those same tests on a real device in order to shake out platform-specific bugs. Running tests on a real device is slow; it typically takes minutes to run the entire test suite and it would therefore make development rather painful if this was the only way to run the tests.
In order to implement this, the project implements tests that must
run locally and on devices as abstract classes ("contracts")
in src/main/java
in the org.librarysimplified.audiobook.tests
module. It then defines a set of classes that extend
the abstract test classes in src/test/java
in the
org.librarysimplified.audiobook.tests
module, and a set of classes that
extend the abstract test classes in src/androidTest/java
in the
org.librarysimplified.audiobook.tests.device
module. The latter are instrumented
device tests and will run on real or emulated devices. The former
classes will run the tests locally.
Some tests will only run on real devices because they have hard dependencies on the Android API. These tests do not have any corresponding abstract base classes.
The test suite contains tests that will exercise user interface code with Espresso. Unfortunately, Espresso appears to be rather fragile, and the following points must be observed if the test suite is to run correctly:
- Device animations must be switched off. This can be achieved manually
by changing all of the animation scale settings in the device's
developer options menu to 0. Alternatively, the following
adb
invocations achieve the same thing:
adb shell settings put global window_animation_scale 0 &
adb shell settings put global transition_animation_scale 0 &
adb shell settings put global animator_duration_scale 0 &
- The device must not be locked/sleeping during the test execution.
This is not mentioned in the Espresso documentation. If the device
is locked, many activities will not correctly go into the
RESUMED
state and the test code will not execute properly.