Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/cyface-de/android-backend

This is the Cyface Android SDK which enables apps to capture sensor data on Android smartphones for traffic and infrastructure analysis.
https://github.com/cyface-de/android-backend

android-library cyface sdk

Last synced: about 1 month ago
JSON representation

This is the Cyface Android SDK which enables apps to capture sensor data on Android smartphones for traffic and infrastructure analysis.

Awesome Lists containing this project

README

        

= Cyface Android SDK

image:https://github.com/cyface-de/android-backend/actions/workflows/gradle_build.yml/badge.svg[link="https://github.com/cyface-de/android-backend/actions/workflows/gradle_build.yml"]
image:https://github.com/cyface-de/android-backend/actions/workflows/gradle_connected-tests.yml/badge.svg?branch=release-6[link="https://github.com/cyface-de/android-backend/actions/workflows/gradle_connected-tests.yml"]
image:https://github.com/cyface-de/android-backend/actions/workflows/gradle_publish.yml/badge.svg[link="https://github.com/cyface-de/android-backend/actions/workflows/gradle_publish.yml"]

This project contains the Cyface Android SDK which is used by Cyface applications to capture data on Android devices.

* <>
* <>
* <>
* <>
** <>
** <>
* <>

[[integration-guide]]
== Integration Guide

This library is published to the Github Package Registry.

To use it as a dependency in your app you need to:

. Make sure you are authenticated to the repository:
** You need a Github account with read-access to this Github repository
** Create a https://github.com/settings/tokens[personal access token on Github] with "read:packages" permissions
** Create or adjust a `gradle.properties` file in the project root containing:

+
----
githubUser=YOUR_USERNAME
githubToken=YOUR_ACCESS_TOKEN
----
** Add the custom repository to your app's `build.gradle`:

+
----
repositories {
// Other maven repositories, e.g.:
google()
mavenCentral()
gradlePluginPortal()
// Repository for this library
maven {
url = uri("https://maven.pkg.github.com/cyface-de/android-backend")
credentials {
username = project.findProperty("githubUser")
password = project.findProperty("githubToken")
}
}
}
----
. Add this package as a dependency to your app's `build.gradle`:
+
----
dependencies {
implementation "de.cyface:datacapturing:$cyfaceAndroidBackendVersion"
implementation "de.cyface:synchronization:$cyfaceAndroidBackendVersion"
implementation "de.cyface:persistence:$cyfaceAndroidBackendVersion"
}
----

. Set the `$cyfaceAndroidBackendVersion` gradle variable to the https://github.com/cyface-de/android-backend/releases[latest version].

[[api-usage-guide]]
== API Usage Guide

* <>
* <>
** <>
* <>
** <>
** <>
*** <>
** <>
** <>
** <>
** <>
** <>
** <>
** <>
* <>
** <>
** <>
* <>
** <>
** <>
** <>
** <>
** <>
* <>

[[collector-compatibility]]
=== Collector Compatibility

This SDK is compatible with our https://github.com/cyface-de/data-collector/releases/tag/5.0.0[Data Collector Version 5].

[[resource-files]]
=== Resource Files

The following steps are required before you can start coding.

[[authenticator-and-sync-service]]]
==== Authenticator- and Sync Service

The SDK provides the CyfaceAuthenticatorService and CyfaceSyncService which authenticates with and uploads data
to a Cyface Collector API. The SDK implementing app can also implement services for different APIs.

Register the Authenticator- and Sync service which should be called by the system in the SDK
implementing app's `AndroidManifest.xml`, e.g. for the default implementations:

[source,xml]
----













----

[[content-provider-authority]]
==== Content Provider Authority

This SDK uses Android's `SyncAdapter` to sync data. The `StubProvider` is a `ContentProvider` stub
which the `SyncAdapter` requires, even when we don't use it to access the data of this SDK.

Therefor, you need to set a provider and to make sure you use the same provider everywhere:

* The `AndroidManifest.xml` is required to override the default content provider as
declared by the persistence project. This needs to be done by each SDK integrating
application separately.

[source,xml]
----




----

* Define your authority which you must use as parameter in `new Cyface-/CustomDataCapturingService()`
(see sample below). This must be the same as defined in the `AndroidManifest.xml` above.

[source,java]
----
public class Constants {
public final static String AUTHORITY = "your.domain.app.provider"; // replace this
}
----

* Create a resource file `src/main/res/xml/sync_adapter.xml` and use the same provider:

[source,xml]
----

----

[[service-initialization]]
=== Service Initialization

The core of our SDK is the `DataCapturingService` which controls the capturing process.

We provide a default interface for this service: `CyfaceDataCapturingService`.
Unless you need a custom `DataCapturingService` extension, use this one.

NOTE: This documentation is out of date as it describes a former extension `MovebisDataCapturingService`
in the samples but the interface for `CyfaceDataCapturingService` is mostly the same.

The following steps are required to communicate with this service.

These instructions assume a `DataCapturingButton` is used to display the current capturing status
and to control the capture status.

[[implement-ui-listener]]
==== Implement UI Listener

This is only required for `MovebisDataCapturingService`.

[[implement-event-handling-strategy]]
==== Implement Event Handling Strategy

This interface allows us to inject your custom strategies into our SDK.

[[custom-capturing-notification]]
===== Custom Capturing Notification

To continuously run an Android service, without the system killing said service,
it needs to show a notification to the user in the Android status bar.

The Cyface data capturing runs as such a service and thus needs to display such a notification.
Applications using the Cyface SDK may configure style and behaviour of this notification by
providing an implementation of `de.cyface.datacapturing.EventHandlingStrategy` to the constructor
of the `de.cyface.datacapturing.DataCapturingService`.

An example implementation is provided by `de.cyface.datacapturing.IgnoreEventsStrategy`.
The most important step is to implement the method
`de.cyface.datacapturing.EventHandlingStrategy#buildCapturingNotification(DataCapturingBackgroundService)`.

This can look like:

[source,java]
----
public class EventHandlingStrategyImpl implements EventHandlingStrategy {

@Override
public @NonNull Notification buildCapturingNotification(final @NonNull DataCapturingBackgroundService context) {
final String channelId = "channel";
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && notificationManager.getNotificationChannel(channelId)==null) {
final NotificationChannel channel = new NotificationChannel(channelId, "Cyface Data Capturing", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}

return new NotificationCompat.Builder(context, channelId)
.setContentTitle("Cyface")
.setSmallIcon(R.drawable.your_icon) // see "attention" notes below
.setContentText("Running Data Capturing")
.setOngoing(true)
.setAutoCancel(false)
.build();
}
}
----

Further details about how to create a proper notification are available via the https://developer.android.com/guide/topics/ui/notifiers/notifications[Google developer documentation].
The most likely adaptation an application using the Cyface SDK for Android should do, is use the `android.app.Notification.Builder.setContentIntent(PendingIntent)` to call the applications main activity if the user presses the notification.

*ATTENTION:*

* Service notifications require an application wide unique identifier.
This identifier is 74.656.
Due to limitations in the Android framework, this is not configurable.
You must not use the same notification identifier for any other notification displayed by your app!
* If you want to use a *vector xml drawable as Notification icon* make sure to do the following:
+
Even with `vectorDrawables.useSupportLibrary` enabled the vector drawable won't work as a notification icon (`notificationBuilder.setSmallIcon()`)
on devices with API < 21. We assume that's because of the way we need to inject your custom notification.
A simple fix is to have the xml in `res/drawable-anydpi-v21/icon.xml` and to generate notification icon PNGs under the same resource name in the usual paths (`+res/drawable-**dpi/icon.png+`).

[[start-service]]
==== Start Service

To save resources your should create your service when the view is created
and reuse this instance when you need to communicate with it.

[source,java]
----
class MainFragment extends Fragment {

private MovebisDataCapturingService dataCapturingService;
private DataCapturingButton dataCapturingButton;

@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {

final static int SENSOR_FREQUENCY = 100;
dataCapturingService = new MovebisDataCapturingService(context,
uiListener, locationUpdateRate, eventHandlingStrategy, capturingListener, SENSOR_FREQUENCY);
}

// Depending on your implementation you need to register the DataCapturingService in your DataCapturingButton:
@Override
public void onResume() {
super.onResume();
// If you want to receive events for the synchronization status
dataCapturingService.addConnectionStatusListener(this);

dataCapturingButton.onResume(dataCapturingService);
}

// If you registered to receive events for the synchronization status
@Override
public void onPause() {
dataCapturingService.removeConnectionStatusListener(this);
super.onPause();
}

@Override
public void onDestroyView() {
try {
// As required by the `WiFiSurveyor.startSurveillance()`
dataCapturingService.shutdownDataCapturingService();
} catch (SynchronisationException e) {
Log.w(TAG, "Failed to shut down CyfaceDataCapturingService. ", e);
}
// If you registered to receive events for the synchronization status
dataCapturingService.removeConnectionStatusListener(this);
super.onDestroyView();
}
}
----

[[reconnect-to-service]]
==== Reconnect to Service

When your UI resumes you need to reconnect to your service:

The `reconnect()` method returns true when there was a capturing running during reconnect.
This way we can use the `isRunning()` result from within `reconnect()` and avoid duplicate
`isRunning()` calls.

[source,java]
----
public class DataCapturingButton implements DataCapturingListener {

PersistenceLayer persistence =
new DefaultPersistenceLayer<>(context, new DefaultPersistenceBehaviour());

public void onResume(@NonNull final CyfaceDataCapturingService dataCapturingService) {
this.dataCapturingService = dataCapturingService;
dataCapturingService.addDataCapturingListener(this);

if (dataCapturingService.reconnect(IS_RUNNING_CALLBACK_TIMEOUT)) {
// Your logic, e.g.:
setButtonStatus(button, OPEN);
} else {
// Attention: reconnect() only returns true if there is an OPEN measurement
// To check for PAUSED measurements use the persistence layer.
if (persistenceLayer.hasMeasurement(PAUSED)) {
// Your logic, e.g.:
setButtonStatus(button, PAUSED);
} else {
// Your logic, e.g.:
setButtonStatus(button, FINISHED);
}
}
}

public void onPause() {
dataCapturingService.removeDataCapturingListener(this);
}

@Override
public void onDestroyView() {
// Unbinds the services. They continue to run in the background but won't send any updates to this button.
if (dataCapturingService != null) {
try {
dataCapturingService.disconnect();
} catch (DataCapturingException e) {
// This just tells us there is no running capturing in the background, see [MOV-588]
Log.d(TAG, "No need to unbind as the background service was not running.");
}
}
}
}
----

[[link-your-login-activity]]
==== Link your Login Activity

This is only required for `CyfaceDataCapturingService`.

Define which Activity should be launched to request the user to log in:

[source,java]
----
public class CustomApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
CyfaceAuthenticator.LOGIN_ACTIVITY = LoginActivity.class;
}
}
----

[[initialize-settings]]
==== Initialize Settings

We use DataStore to store user preferences.

Initialize these settings exactly once per file per process, for UI process:

[source,kotlin]
----
class CustomApplication : Application() {

/**
* The settings used by both, UIs and libraries.
*/
private val lazyAppSettings by lazy { // android-utils
AppSettings(this)
}

override fun onCreate() {
super.onCreate()

// Initialize DataStore once for all settings
appSettings = lazyAppSettings
TrackingSettings.initialize(this) // energy_settings
CyfaceAuthenticator.settings = DefaultSynchronizationSettings( // synchronization
this,
"https://example.com/api/v4", // Set the Data Collector URL
// Movebis variant can replace oauth config with `JsonObject()`
OAuth2.oauthConfig(BuildConfig.oauthRedirect, BuildConfig.oauthDiscovery)
)
}
}
----

[[start-wifisurveyor]]
==== Start WifiSurveyor

This is only required for `CyfaceDataCapturingService`.

Create an account for synchronization and start `WifiSurveyor`:

[source,java]
----
public class MainFragment extends Fragment implements ConnectionStatusListener {

@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
try {
// dataCapturingService = ... - see above

// Needs to be called after `new CyfaceDataCapturingService()`
startSynchronization(context);

// If you want to receive events for the synchronization status
dataCapturingService.addConnectionStatusListener(this);
} catch (final SetupException | CursorIsNullException e) {
throw new IllegalStateException(e);
}
}

@SuppressWarnings("WeakerAccess")
public void startSynchronization(final Context context) {
final AccountManager accountManager = AccountManager.get(context);
final boolean validAccountExists = accountWithTokenExists(accountManager);

if (validAccountExists) {
try {
dataCapturingService.startWifiSurveyor();
} catch (SetupException e) {
throw new IllegalStateException(e);
}
return;
}

// Login via LoginActivity, create account and using dynamic tokens
// The LoginActivity is called by Android which handles the account creation
accountManager.addAccount(ACCOUNT_TYPE, AUTH_TOKEN_TYPE, null, null,
getMainActivityFromContext(context), new AccountManagerCallback() {
@Override
public void run(AccountManagerFuture future) {
try {
// noinspection unused - this allows us to detect when LoginActivity is closed
final Bundle bundle = future.getResult();

// The LoginActivity created a temporary account which cannot yet be used for synchronization.
// As the login was successful we now register the account correctly:
final AccountManager accountManager = AccountManager.get(context);
final Account account = accountManager.getAccountsByType(ACCOUNT_TYPE)[0];
dataCapturingService.getWifiSurveyor().makeAccountSyncable(account, syncEnabledPreference);

dataCapturingService.startWifiSurveyor();
} catch (OperationCanceledException e) {
// This closes the app when the LoginActivity is closed
getMainActivityFromContext(context).finish();
} catch (AuthenticatorException | IOException | SetupException e) {
throw new IllegalStateException(e);
}
}
}, null);
}

private static boolean accountWithTokenExists(final AccountManager accountManager) {
final Account[] existingAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
Validate.isTrue(existingAccounts.length < 2, "More than one account exists.");
return existingAccounts.length != 0
&& accountManager.peekAuthToken(existingAccounts[0], AUTH_TOKEN_TYPE) != null;
}
}
----

[[de-register-jwt-auth-tokens]]
==== De-/Register JWT Auth Tokens

This is only required for `MovebisDataCapturingService`.

[[start-stop-ui-location-updates]]
==== Start/Stop UI Location Updates

This is only required for `MovebisDataCapturingService`.

[[implement-data-capturing-listener]]
==== Implement Data Capturing Listener

This interface informs your app about data capturing events. Implement the interface to update your UI depending on these events.

[NOTE]
====
Please use `dataCapturingService.loadCurrentlyCapturedMeasurement()` instead of `persistenceLayer.loadCurrentlyCapturedMeasurement()`
to load the measurement data for the currently captured measurement which uses a cache.

This way the database access is reduced which is especially important when executing this frequently,
like in the example below - on each location update.
====

Here is a basic example implementation.

[source,java]
----
class DataCapturingButton implements DataCapturingListener {

@Override
public void onNewGeoLocationAcquired(GeoLocation geoLocation) {

// To identify invalid ("unclean") location, check geoLocation.isValid()

// Load updated measurement distance
final Measurement measurement;
try {
measurement = dataCapturingService.loadCurrentlyCapturedMeasurement();
} catch (final NoSuchMeasurementException | CursorIsNullException e) {
throw new IllegalStateException(e);
}

final double distance = measurement.getDistance();
// Your logic, e.g. update the UI with the current distance
}

// The other interface methods
}
----

[[control-capturing]]
=== Control Capturing

Now you can actually use the `DataCapturingService` instance to capture data.

[[start-stop-capturing]]
==== Start/Stop Capturing

To capture a measurement you need to start the capturing and stop it after some time:

[source,java]
----
public class DataCapturingButton implements DataCapturingListener {
public void onClick(View view) {

dataCapturingService.isRunning(IS_RUNNING_CALLBACK_TIMEOUT, TimeUnit.MILLISECONDS, new IsRunningCallback() {
@Override
public void isRunning() {
Validate.isTrue(buttonStatus == OPEN, "DataCapturingButton is out of sync.");
stopCapturing();
}

@Override
public void timedOut() {
Validate.isTrue(buttonStatus != OPEN, "DataCapturingButton is out of sync.");

try {
// If Measurement is paused, resume the measurement on a normal click
if (persistenceLayer.hasMeasurement(PAUSED)) {
resumeCapturing();
return;
}
startCapturing();

} catch (final CursorIsNullException e) {
throw new IllegalStateException(e);
}

}
});
}

private void startCapturing() {
dataCapturingService.start(Modality.BICYCLE, new StartUpFinishedHandler(
MessageCodes.getServiceStartedActionId(context.getPackageName())) {
@Override
public void startUpFinished(final long measurementIdentifier) {
// Your logic, e.g.:
setButtonStatus(button, OPEN);
}
});
}

private void stopCapturing() {
dataCapturingService.stop(new ShutDownFinishedHandler(MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED) {
@Override
public void shutDownFinished(final long measurementIdentifier) {
// Your logic, e.g.:
setButtonStatus(button, FINISHED);
setButtonEnabled(true);
}
});
}

@Overwrite
public void onCapturingStopped() {
setButtonStatus(button, FINISHED);
}
}
----

[[pause-resume-capturing]]
==== Pause/Resume Capturing

If you want to pause a measurement you can use:

[source,java]
----
public class DataCapturingButton implements DataCapturingListener {
public void onLongClick(View view) {
dataCapturingService.isRunning(IS_RUNNING_CALLBACK_TIMEOUT, TimeUnit.MILLISECONDS, new IsRunningCallback() {@Override
public void isRunning() {
Validate.isTrue(buttonStatus == OPEN, "DataCapturingButton is out of sync.");
pauseCapturing();
}

@Override
public void timedOut() {
Validate.isTrue(buttonStatus != OPEN, "DataCapturingButton is out of sync.");

try {
// If Measurement is paused, stop the measurement on long press
if (persistenceLayer.hasMeasurement(PAUSED)) {
stopCapturing();
return;
}
startCapturing();

} catch (final CursorIsNullException e) {
throw new IllegalStateException(e);
}
}
});
return true;
}

private void pauseCapturing() {
dataCapturingService.pause(new ShutDownFinishedHandler(MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED) {
@Override
public void shutDownFinished(final long measurementIdentifier) {
// Your logic, e.g.:
setButtonStatus(button, PAUSED);
setButtonEnabled(true);
}
});
}

private void resumeCapturing() {
dataCapturingService.resume(new StartUpFinishedHandler(MessageCodes.getServiceStartedActionId(context.getPackageName())) {
@Override
public void startUpFinished(final long measurementIdentifier) {
setButtonStatus(button, OPEN);
}
});
}
}
----

[[access-measurements]]
=== Access Measurements

You now need to use the `DefaultPersistenceLayer` to access and control captured _measurement data_.

[source,java]
----
class measurementControlOrAccessClass {

PersistenceLayer persistence =
new DefaultPersistenceLayer<>(context, new DefaultPersistenceBehaviour());
}
----

* Use `persistenceLayer.loadMeasurement(mid)` to load a specific measurement
* Use `loadMeasurements()` or `loadMeasurements(MeasurementStatus)` to load multiple measurements (of a specific state)

Loaded ``Measurement``s contain details, e.g. the <>.

[NOTE]
====
The attributes of a Measurement which is not yet finished change
over time so you need to make sure you reload it.
You can find an example for this in <>.
====

[[load-finished-measurements]]
==== Load Finished Measurements

Finished measurements are measurements which are stopped (i.e. not paused or ongoing).

[source,java]
----
class measurementControlOrAccessClass {
void loadMeasurements() {

persistence.loadMeasurements(MeasurementStatus.FINISHED);
}
}
----

[[load-tracks]]
==== Load Tracks

The `loadTracks()` method returns a chronologically ordered list of ``Track``s.

Each time a measurement is paused and resumed, a new `Track` is started for the same measurement.

A `Track` contains the chronologically ordered ``ParcelableGeoLocation``s captured.

You can either load the raw track or a "cleaned" version of it. See the `DefaultLocationCleaning` class for details.

[source,java]
----
class measurementControlOrAccessClass {
void loadTrack() {

// Raw track:
List tracks = persistence.loadTracks(measurementId);

// or, "cleaned" track:
List tracks = persistence.loadTracks(measurementId, new DefaultLocationCleaningStrategy());

//noinspection StatementWithEmptyBody
if (tracks.size() > 0 ) {
// your logic
}
}
}
----

[[load-measurement-distance]]
==== Load Measurement Distance

To display the distance for an ongoing measurement (which is updated about once per second) you need to call
`dataCapturingService.loadCurrentlyCapturedMeasurement()` regularly, e.g. on each location update to always have the most recent information.

For this you need to implement the `DataCapturingListener` interface to be notified on `onNewGeoLocationAcquired(GeoLocation)` events.

See <> for sample code.

[[delete-measurements]]
==== Delete Measurements

To delete the measurement data stored on the device for finished or synchronized measurements use:

[source,java]
----
class measurementControlOrAccessClass {

void deleteMeasurement(final long measurementId) {
// To make sure you don't delete the ongoing measurement because this leads to an exception
Measurement currentlyCapturedMeasurement;
try {
currentlyCapturedMeasurement = persistenceLayer.loadCurrentlyCapturedMeasurement();
} catch (NoSuchMeasurementException e) {
// do nothing
}

if (currentlyCapturedMeasurement == null || currentlyCapturedMeasurement.getIdentifier() != measurementId) {
new DeleteFromDBTask()
.execute(new DeleteFromDBTaskParams(persistenceLayer, this, measurementId));
} else {
Log.d(TAG, "Not deleting currently captured measurement: " + measurementId);
}
}

private static class DeleteFromDBTaskParams {
final PersistenceLayer persistenceLayer;
final long measurementId;

DeleteFromDBTaskParams(final DefaultPersistenceLayer persistenceLayer,
final long measurementId) {
this.persistenceLayer = persistenceLayer;
this.measurementId = measurementId;
}
}

private class DeleteFromDBTask extends AsyncTask {
protected Void doInBackground(final DeleteFromDBTaskParams... params) {
final PersistenceLayer persistenceLayer = params[0].persistenceLayer;
final long measurementId = params[0].measurementId;
persistenceLayer.delete(measurementId);
}

protected void onPostExecute(Void v) {
// Your logic
}
}
}
----

[[load-events]]
==== Load Events

The `loadEvents()` method returns a chronologically ordered list of ``Event``s.

These Events log `Measurement` related interactions of the user, e.g.:

* EventType.LIFECYCLE_START, EventType.LIFECYCLE_PAUSE, EventType.LIFECYCLE_RESUME, EventType.LIFECYCLE_STOP
whenever a user starts, pauses, resumes or stops the Measurement.
* EventType.MODALITY_TYPE_CHANGE at the start of a Measurement to define the Modality used in the Measurement
and when the user selects a new `Modality` type during an ongoing (or paused) Measurement.
The later is logged when `persistenceLayer.changeModalityType(Modality newModality)` is called with a different Modality than the current one.
* The `Event` class contains a `getValue()` attribute which contains the `newModality`
in case of a `EventType.MODALITY_TYPE_CHANGE` or else `Null`

[source,java]
----
class measurementControlOrAccessClass {
void loadEvents() {

// To retrieve all Events of that Measurement
//noinspection UnusedAssignment
List events = persistence.loadEvents(measurementId);

// Or to retrieve only the Events of a specific EventType
events = persistence.loadEvents(measurementId, EventType.MODALITY_TYPE_CHANGE);

//noinspection StatementWithEmptyBody
if (events.size() > 0 ) {
// your logic
}
}
}
----

[[documentation-incomplete]]
=== Documentation Incomplete

This documentation still lacks of samples for the following features:

* ErrorHandler
* Force Synchronization
* ConnectionStatusListener implementation
* Disable synchronization
* Enable synchronization on metered connections
* Logout

[[migration-guide]]
== Migration Guide

* xref:documentation/migration-guide_6.0.0.adoc[Migrate to 6.0.0]

[[developer-guide]]
== Developer Guide

This section is only relevant for developers of this library.

[[architecture]]
=== Architecture

The SDK contains the following models:

==== Datacapturing

The `DataCapturingService` allows to control data capturing, persists the data & informs about the capturing progress.

==== Persistence

The `PersistenceLayer` serves as the https://developer.android.com/topic/architecture/data-layer[data layer] for SDK implementing apps.

The sub-package `model` contains the data types persisted.

The following data types are persisted in an SQLite database using the https://developer.android.com/jetpack/androidx/releases/room[Room API].

- Identifier: The device identifier
- Measurement: The data collected between capturing `start` and `stop`
- Event: Life-cycle changes (`start`/`pause`/`resume`/`stop`) or `modality` changes for one measurement
- Location: GNSS data captured for one measurement
- Pressure: Barometer data captured fro one measurement

The following data types are persisted in the https://github.com/cyface-de/protos[Cyface Binary Format] using Protobuf and stored in the file system:

- Accelerometer: `*.cyfa` files
- Gyroscope: `*.cyfr` files
- Magnetometer: `*.cyfd` files

The sub-package `dao` contains the local data sources for the data types above.

The sub-package `repository` contains the abstraction layer for data sources, allowing multiple data sources per type with a common interface (e.g. network and database/file (`dao`)).

The sub-package `serialization` contains the functionality to serialize all data of one measurement
into a compressed `*.ccyf` file which can be uploaded to the Cyface Collector.

The sub-package `content` implements a `ContentProvider` to allow `Synchronization`'s `SyncAdapter` to access and upload data.

==== Synchronization

The `CyfaceSyncService` uploads measurements to the https://github.com/cyface-de/data-collector[Cyface Collector] & informs about the upload progress.

==== Testutils

The `SharedTestUtils` contains integration test code used from multiple modules.

[[release-a-new-version]]
=== Release a new version

See https://github.com/cyface-de/data-collector#release-a-new-version[Cyface Collector Readme]

* `cyfaceAndroidBackendVersion` in root `build.gradle` is automatically set by the CI
* Just tag the release and push the tag to Github
* The Github package is automatically published when a new version is tagged and pushed by our
https://github.com/cyface-de/android-backend/actions[Github Actions] to
the https://github.com/cyface-de/android-backend/packages[Github Registry]
* The tag is automatically marked as a 'new Release' on https://github.com/cyface-de/android-backend/releases[Github]

[[known-issues]]
=== Known Issues

The AVD Cache leads to `Install_failed_Update_Incompatible` after a few builds.
- we opened an issue here: https://github.com/ReactiveCircus/android-emulator-runner/issues/319
- we could try to make the AVD cache only be used on main branch like
- see https://github.com/ankidroid/Anki-Android/pull/11032/files?diff=split&w=0
- but for now, we just disabled the AVD cache for the CI to be usable

[[license]]
== License
Copyright 2017-2023 Cyface GmbH

This file is part of the Cyface SDK for Android.

The Cyface SDK for Android is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The Cyface SDK for Android is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with the Cyface SDK for Android. If not, see http://www.gnu.org/licenses/.