Skip to content

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

License

Notifications You must be signed in to change notification settings

cyface-de/android-backend

Repository files navigation

Cyface Android SDK

badge badge badge

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

Integration Guide

This library is published to the Github Package Registry.

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

  1. Make sure you are authenticated to the repository:

    • You need a Github account with read-access to this Github repository

    • Create a 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")
             }
         }
     }
  2. 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"
     }
  3. Set the $cyfaceAndroidBackendVersion gradle variable to the latest version.

API Usage Guide

Collector Compatibility

This SDK is compatible with our Data Collector Version 5.

Resource Files

The following steps are required before you can start coding.

] ==== 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:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <service android:name="de.cyface.synchronization.CyfaceAuthenticatorService"
            android:exported="false"
            android:process=":sync">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
        </service>

        <service
            android:name="de.cyface.synchronization.CyfaceSyncService"
            android:exported="false"
            android:process=":sync">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/sync_adapter" />
        </service>
    </application>

</manifest>

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.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <!-- This overwrites the provider in the SDK. This way the app can be installed next to other
        SDK using apps. The "authorities" must match the one in your AndroidManifest.xml! -->
        <provider
            android:name="de.cyface.persistence.content.StubProvider"
            android:authorities="your.domain.app.provider"
            android:exported="false"
            android:process=":persistence_process"
            android:syncable="true"
            tools:replace="android:authorities" />
    </application>

</manifest>
  • 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.

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:

<?xml version="1.0" encoding="UTF-8" ?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentAuthority="your.domain.app.provider"
    android:accountType="your.domain.app"
    android:userVisible="false"
    android:supportsUploading="true"
    android:allowParallelSyncs="false"
    android:isAlwaysSyncable="true" />

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

This is only required for MovebisDataCapturingService.

Implement Event Handling Strategy

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

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:

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 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

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

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

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.

public class DataCapturingButton implements DataCapturingListener {

    PersistenceLayer<DefaultPersistenceBehaviour> 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.");
            }
        }
    }
}

This is only required for CyfaceDataCapturingService.

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

public class CustomApplication extends Application {

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

Initialize Settings

We use DataStore to store user preferences.

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

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

This is only required for CyfaceDataCapturingService.

Create an account for synchronization and start WifiSurveyor:

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<Bundle>() {
                @Override
                public void run(AccountManagerFuture<Bundle> 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

This is only required for MovebisDataCapturingService.

Start/Stop UI Location Updates

This is only required for MovebisDataCapturingService.

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.

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

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

Start/Stop Capturing

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

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

If you want to pause a measurement you can use:

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

You now need to use the DefaultPersistenceLayer to access and control captured measurement data.

class measurementControlOrAccessClass {

    PersistenceLayer<DefaultPersistenceBehaviour> 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 Measurements contain details, e.g. the Measurement Distance.

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 Implement Data Capturing Listener.

Load Finished Measurements

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

class measurementControlOrAccessClass {
    void loadMeasurements() {

        persistence.loadMeasurements(MeasurementStatus.FINISHED);
    }
}

Load Tracks

The loadTracks() method returns a chronologically ordered list of Tracks.

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

A Track contains the chronologically ordered ParcelableGeoLocations captured.

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

class measurementControlOrAccessClass {
    void loadTrack() {

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

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

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

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 Implement Data Capturing Listener for sample code.

Delete Measurements

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

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<DefaultPersistenceBehaviour> persistenceLayer;
        final long measurementId;

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

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

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

Load Events

The loadEvents() method returns a chronologically ordered list of Events.

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

class measurementControlOrAccessClass {
    void loadEvents() {

        // To retrieve all Events of that Measurement
        //noinspection UnusedAssignment
        List<Event> 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

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

Developer Guide

This section is only relevant for developers of this library.

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 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 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 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 Cyface Collector & informs about the upload progress.

Testutils

The SharedTestUtils contains integration test code used from multiple modules.

Release a new version

  • 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 Github Actions to the Github Registry

  • The tag is automatically marked as a 'new Release' on Github

Known Issues

The AVD Cache leads to Install_failed_Update_Incompatible after a few builds. - we opened an issue here: ReactiveCircus/android-emulator-runner#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

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/.

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages