diff --git a/packages/activity_recognition_flutter/CHANGELOG.md b/packages/activity_recognition_flutter/CHANGELOG.md index d28e96bed..80b5232f7 100644 --- a/packages/activity_recognition_flutter/CHANGELOG.md +++ b/packages/activity_recognition_flutter/CHANGELOG.md @@ -1,3 +1,24 @@ +## 6.0.0 + +Breaking changes: + +- Requires Flutter 3.44 and Dart 3.10 +- Android package has been renamed to `dk.carp.activity_recognition_flutter`. +- Apps that referenced the old package in their manifest or ProGuard rules need to update those references +- `ActivityEvent` now takes the timestamp (optional) +- Minimum iOS deployment target is 15.0 and the Android `compileSdk` is 36 +- CocoaPods is no longer supported on iOS +- Added Swift Package Manager (SPM) +- Moved Android impls to Kotlin +- Plugin now declares its own permissions, broadcast receiver and foreground + service in its manifest +- Removed `ActivityRecognizedService` +- Removed `ActivityType.INVALID` +- Removed `ActivityEvent.fromString` +- Fixed activity timestamps source to the platform's `CMMotionActivity.startDate` on + iOS, `ActivityRecognitionResult.time` on Android +- Fixed Android events deregister on stream cancel + ## 5.0.0 - upgraded Android SDK level diff --git a/packages/activity_recognition_flutter/README.md b/packages/activity_recognition_flutter/README.md index a82ba6b0f..a42642038 100644 --- a/packages/activity_recognition_flutter/README.md +++ b/packages/activity_recognition_flutter/README.md @@ -4,29 +4,28 @@ Activity recognition plugin for Android and iOS. Only working while App is running (= not terminated by the user or OS). +The communication with the native platforms is defined in [`pigeons/messages.dart`](pigeons/messages.dart) +and generated with [Pigeon](https://pub.dev/packages/pigeon). Regenerate it after +changing that file with: -## Configuration +```sh +dart run pigeon --input pigeons/messages.dart +``` -### Android +The iOS implementation is a Swift package (`ios/activity_recognition_flutter/`). +CocoaPods is no longer supported, so consuming apps must use Swift Package +Manager. See [iOS](#ios) below. -Add the following entries inside the `` tag: -```xml - - - -``` +## Configuration -Next, add the plugin's service inside the `` tag: +### Android -```xml - - - -``` +The permissions the plugin requires, and the broadcast receiver and +foreground service it relies on, are declared by the plugin. + +You still have to request the `ACTIVITY_RECOGNITION` runtime permission before +listening to the stream -- see [Usage](#usage) below. #### Known Android quirks @@ -37,7 +36,19 @@ This package uses the Android Embedding API v2. In order to use this in pre-Flut ### iOS -An iOS app linked on or after iOS 10.0 must include usage description keys in its `Info.plist` file for the types of data it needs. Failure to include these keys will cause the app to crash. +This plugin ships only as a Swift package, so your app must use Swift Package +Manager. It is enabled by default on Flutter 3.44 and later; if you turned it +off, re-enable it with: + +```sh +flutter config --enable-swift-package-manager +``` + +If your app still has CocoaPods integration, see +[Flutter Swift Package Manager guide](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers) +for details. + +An iOS app linked on must include usage description keys in its `Info.plist` file for the types of data it needs. Failure to include these keys will cause the app to crash. To access motion and fitness data specifically, it must include `NSMotionUsageDescription`, like this: ```xml @@ -63,7 +74,6 @@ Each detected activity will have an activity type, which is one of the following * TILTING * UNKNOWN * WALKING -* INVALID (used for parsing errors) - -As well as a confidence expressed in percentages (i.e. a value from 0-100). +As well as a confidence expressed in percentages (i.e. a value from 0-100), and +the timestamp of the detection as reported by the platform. diff --git a/packages/activity_recognition_flutter/android/build.gradle b/packages/activity_recognition_flutter/android/build.gradle deleted file mode 100644 index c1103af3a..000000000 --- a/packages/activity_recognition_flutter/android/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -group 'dk.cachet.activity_recognition_flutter' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 33 - - defaultConfig { - minSdkVersion 26 - } - lintOptions { - disable 'InvalidPackage' - } - namespace "dk.cachet.activity_recognition_flutter" -} - -dependencies { - implementation 'com.google.android.gms:play-services-location:19.0.1' -} diff --git a/packages/activity_recognition_flutter/android/build.gradle.kts b/packages/activity_recognition_flutter/android/build.gradle.kts new file mode 100644 index 000000000..c2ff7c1a3 --- /dev/null +++ b/packages/activity_recognition_flutter/android/build.gradle.kts @@ -0,0 +1,63 @@ +group = "dk.carp.activity_recognition_flutter" +version = "1.0" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +plugins { + id("com.android.library") +} + +// AGP 9+ has built-in Kotlin support; on older AGP the Kotlin Gradle plugin +// must be applied explicitly. Version is supplied by the consuming app. +// See https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-plugin-authors +val agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.substringBefore('.').toInt() +if (agpMajor < 9) { + apply(plugin = "org.jetbrains.kotlin.android") +} + +android { + namespace = "dk.carp.activity_recognition_flutter" + + // Kept at the lowest currently supported level on purpose: a library forces + // every app that depends on it to compile against at least this SDK. + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin") + } + } + + defaultConfig { + // API 26 is required by the plugin itself: notification channels and + // Context.startForegroundService were both added in Android 8. + minSdk = 26 + } + + lint { + disable += "InvalidPackage" + } +} + +// Configured reflectively rather than through the `kotlin { }` accessor: the +// Kotlin plugin is applied after this script is compiled (by Flutter, or +// conditionally above), so the type-safe accessor does not exist here. +project.extensions.configure(org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension::class.java) { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + implementation("com.google.android.gms:play-services-location:21.3.0") +} diff --git a/packages/activity_recognition_flutter/android/gradle.properties b/packages/activity_recognition_flutter/android/gradle.properties deleted file mode 100644 index 94adc3a3f..000000000 --- a/packages/activity_recognition_flutter/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties b/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 02767eb1c..000000000 --- a/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/packages/activity_recognition_flutter/android/settings.gradle b/packages/activity_recognition_flutter/android/settings.gradle deleted file mode 100644 index f7af86bb7..000000000 --- a/packages/activity_recognition_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'activity_recognition_flutter' diff --git a/packages/activity_recognition_flutter/android/settings.gradle.kts b/packages/activity_recognition_flutter/android/settings.gradle.kts new file mode 100644 index 000000000..9939b49d6 --- /dev/null +++ b/packages/activity_recognition_flutter/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "activity_recognition_flutter" diff --git a/packages/activity_recognition_flutter/android/src/main/AndroidManifest.xml b/packages/activity_recognition_flutter/android/src/main/AndroidManifest.xml index 1601def3a..33c2c53f3 100644 --- a/packages/activity_recognition_flutter/android/src/main/AndroidManifest.xml +++ b/packages/activity_recognition_flutter/android/src/main/AndroidManifest.xml @@ -1,5 +1,20 @@ - + + + + + + + + + + + + diff --git a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.java b/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.java deleted file mode 100644 index 6df5f1cd3..000000000 --- a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.java +++ /dev/null @@ -1,174 +0,0 @@ -package dk.cachet.activity_recognition_flutter; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import com.google.android.gms.location.ActivityRecognition; -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.android.gms.tasks.Task; - -import java.util.HashMap; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.EventChannel; - -/** - * ActivityRecognitionFlutterPlugin - */ -@SuppressLint("LongLogTag") -public class ActivityRecognitionFlutterPlugin implements FlutterPlugin, EventChannel.StreamHandler, ActivityAware, SharedPreferences.OnSharedPreferenceChangeListener { - private EventChannel channel; - private EventChannel.EventSink eventSink; - private Activity androidActivity; - private Context androidContext; - public static final String DETECTED_ACTIVITY = "detected_activity"; - public static final String ACTIVITY_RECOGNITION = "activity_recognition_flutter"; - - private final String TAG = "activity_recognition_flutter"; - - /** - * The main function for starting activity tracking. - * Handling events is done inside [ActivityRecognizedService] - */ - private void startActivityTracking() { - // Start the service - Intent intent = new Intent(androidActivity, ActivityRecognizedBroadcastReceiver.class); - - Log.d(TAG, "SDK = " + Build.VERSION.SDK_INT); - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT >= 31) { - flags |= PendingIntent.FLAG_IMMUTABLE; - } - PendingIntent pendingIntent = PendingIntent.getBroadcast(androidActivity, 0, intent, flags); - - // Frequency in milliseconds - long frequency = 5 * 1000; - Task task = ActivityRecognition.getClient(androidContext) - .requestActivityUpdates(frequency, pendingIntent); - - task.addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(Void e) { - Log.d(TAG, "Successfully registered ActivityRecognition listener."); - } - }); - task.addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - Log.d(TAG, "Failed to registered ActivityRecognition listener."); - } - }); - } - - /** - * EventChannel.StreamHandler interface below - */ - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { - channel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), ACTIVITY_RECOGNITION); - channel.setStreamHandler(this); - } - - // Unchecked HashMap cast. Using instanceof does not clear the warning. - @SuppressWarnings("unchecked") - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - HashMap args = (HashMap) arguments; - boolean fg = (boolean) args.get("foreground"); - if(fg) { - startForegroundService(); - } - Log.d(TAG, "Foreground mode: " + fg); - - eventSink = events; - startActivityTracking(); - } - - @RequiresApi(api = Build.VERSION_CODES.O) - void startForegroundService() { - Intent intent = new Intent(androidActivity, ForegroundService.class); - - // Tell the service we want to start it - intent.setAction("start"); - - // Pass the notification title/text/icon to the service - intent.putExtra("title", "MonsensoMonitor") - .putExtra("text", "Monsenso Foreground Service") - .putExtra("icon", R.drawable.common_full_open_on_phone) - .putExtra("importance", 3) - .putExtra("id", 10); - - // Start the service - androidContext.startForegroundService(intent); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setStreamHandler(null); - } - - @Override - public void onCancel(Object arguments) { - channel.setStreamHandler(null); - } - - /** - * ActivityAware interface below - */ - @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - androidActivity = binding.getActivity(); - androidContext = binding.getActivity().getApplicationContext(); - - SharedPreferences prefs = androidContext.getSharedPreferences(ACTIVITY_RECOGNITION, Context.MODE_PRIVATE); - prefs.registerOnSharedPreferenceChangeListener(this); - // Log.d(TAG, "onAttachedToActivity"); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - androidActivity = null; - androidContext = null; - } - - @Override - public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - androidActivity = binding.getActivity(); - androidContext = binding.getActivity().getApplicationContext(); - - } - - @Override - public void onDetachedFromActivity() { - androidActivity = null; - androidContext = null; - } - - /** - * Shared preferences changed, i.e. latest activity - */ - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - String result = sharedPreferences - .getString(DETECTED_ACTIVITY, "error"); - // Log.d("onSharedPreferenceChange", result); - if (key!= null && key.equals(DETECTED_ACTIVITY)) { - // Log.d(TAG, "Detected activity: " + result); - eventSink.success(result); - } - } - } diff --git a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.java b/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.java deleted file mode 100644 index 5c7433148..000000000 --- a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.java +++ /dev/null @@ -1,14 +0,0 @@ -package dk.cachet.activity_recognition_flutter; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -public class ActivityRecognizedBroadcastReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - ActivityRecognizedService.enqueueWork(context, intent); - } -} diff --git a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedService.java b/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedService.java deleted file mode 100644 index be4e3b273..000000000 --- a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ActivityRecognizedService.java +++ /dev/null @@ -1,79 +0,0 @@ -package dk.cachet.activity_recognition_flutter; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.core.app.JobIntentService; - -import com.google.android.gms.location.ActivityRecognitionResult; -import com.google.android.gms.location.DetectedActivity; - -import java.util.List; - -public class ActivityRecognizedService extends JobIntentService { - - static void enqueueWork(Context context, Intent work) { - enqueueWork(context, ActivityRecognizedService.class, 1, work); - } - - @Override - public int onStartCommand(@Nullable Intent intent, int flags, int startId) { - return super.onStartCommand(intent, flags, startId); - } - - // This method is called when service starts instead of onHandleIntent - protected void onHandleWork(@Nullable Intent intent) { - onHandleIntent(intent); - } - - // remove override and make onHandleIntent private. - private void onHandleIntent(@Nullable Intent intent) { - ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent); - List activities = result.getProbableActivities(); - - DetectedActivity mostLikely = activities.get(0); - - for (DetectedActivity a : activities) { - if (a.getConfidence() > mostLikely.getConfidence()) { - mostLikely = a; - } - } - - String type = getActivityString(mostLikely.getType()); - int confidence = mostLikely.getConfidence(); - - String data = type + "," + confidence; - - Log.d("onHandleIntent", data); - - // Same preferences as in ActivityRecognitionFlutterPlugin.java - SharedPreferences preferences = - getApplicationContext().getSharedPreferences( - ActivityRecognitionFlutterPlugin.ACTIVITY_RECOGNITION, MODE_PRIVATE); - - preferences.edit().clear() - .putString( - ActivityRecognitionFlutterPlugin.DETECTED_ACTIVITY, - data - ) - .apply(); - } - - public static String getActivityString(int type) { - if (type == DetectedActivity.IN_VEHICLE) return "IN_VEHICLE"; - if (type == DetectedActivity.ON_BICYCLE) return "ON_BICYCLE"; - if (type == DetectedActivity.ON_FOOT) return "ON_FOOT"; - if (type == DetectedActivity.RUNNING) return "RUNNING"; - if (type == DetectedActivity.STILL) return "STILL"; - if (type == DetectedActivity.TILTING) return "TILTING"; - if (type == DetectedActivity.WALKING) return "WALKING"; - - // Default case - return "UNKNOWN"; - } -} - diff --git a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ForegroundService.java b/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ForegroundService.java deleted file mode 100644 index 05b9a07dc..000000000 --- a/packages/activity_recognition_flutter/android/src/main/java/dk/cachet/activity_recognition_flutter/ForegroundService.java +++ /dev/null @@ -1,95 +0,0 @@ -package dk.cachet.activity_recognition_flutter; - -import android.content.Intent; -import android.content.Context; -import android.app.Service; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.os.IBinder; -import android.os.Bundle; -import android.annotation.TargetApi; - -public class ForegroundService extends Service { - - public ForegroundService() { - super(); - } - - @Override - public void onCreate() { - super.onCreate(); - Bundle bundle = new Bundle(); - bundle.putString("title", "Foreground service"); - bundle.putString("text", "Foreground monitoring service"); - startPluginForegroundService(bundle); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; - } - - @TargetApi(26) - private void startPluginForegroundService(Bundle extras) { - Context context = getApplicationContext(); - - // Delete notification channel if it already exists - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.deleteNotificationChannel("foreground.service.channel"); - - // Get notification channel importance - Integer importance; - - try { - importance = Integer.parseInt((String) extras.get("importance")); - } catch (NumberFormatException e) { - importance = 1; - } - - switch (importance) { - case 2: - importance = NotificationManager.IMPORTANCE_DEFAULT; - break; - case 3: - importance = NotificationManager.IMPORTANCE_HIGH; - break; - default: - importance = NotificationManager.IMPORTANCE_LOW; - // We are not using IMPORTANCE_MIN because we want the notification to be visible - } - - // Create notification channel - NotificationChannel channel = new NotificationChannel("foreground.service.channel", "Background Services", importance); - channel.setDescription("Enables background processing."); - getSystemService(NotificationManager.class).createNotificationChannel(channel); - - // Get notification icon -// int icon = getResources().getIdentifier((String) extras.get("icon"), "drawable", context.getPackageName()); - int icon = R.drawable.common_full_open_on_phone; - - // Make notification - Notification notification = new Notification.Builder(context, "foreground.service.channel") - .setContentTitle((CharSequence) extras.get("title")) - .setContentText((CharSequence) extras.get("text")) - .setOngoing(true) - .setSmallIcon(icon == 0 ? 17301514 : icon) // Default is the star icon - .build(); - - // Get notification ID - Integer id; - try { - id = Integer.parseInt((String) extras.get("id")); - } catch (NumberFormatException e) { - id = 0; - } - - // Put service in foreground and show notification (id of 0 is not allowed) - startForeground(id != 0 ? id : 197812504, notification); - } - - @Override - public IBinder onBind(Intent intent) { - throw new UnsupportedOperationException("Not yet implemented"); - } -} diff --git a/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.kt b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.kt new file mode 100644 index 000000000..61372ceac --- /dev/null +++ b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.kt @@ -0,0 +1,136 @@ +package dk.carp.activity_recognition_flutter + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.google.android.gms.location.ActivityRecognition +import io.flutter.embedding.engine.plugins.FlutterPlugin + +/** + * Activity recognition for Android, backed by the Google Play services + * Activity Recognition API. + * + * The Dart <-> native contract is generated by Pigeon from `pigeons/messages.dart`; + * see `Messages.g.kt`. Configuration arrives over a host API and detections are + * pushed back over an event channel. + * + * Detections are delivered by the system to [ActivityRecognizedBroadcastReceiver], + * which may run without a Flutter engine attached, so they reach this plugin + * through [DetectedActivityStore]. + */ +class ActivityRecognitionFlutterPlugin : + FlutterPlugin, + ActivityRecognitionHostApi, + SharedPreferences.OnSharedPreferenceChangeListener { + + private companion object { + const val TAG = "ActivityRecognition" + + /** How often the system is asked for a new detection. */ + const val DETECTION_INTERVAL_MS = 5_000L + } + + private val mainHandler = Handler(Looper.getMainLooper()) + private val streamHandler = ActivityStreamHandler() + + private var applicationContext: Context? = null + private var preferences: SharedPreferences? = null + private var eventSink: PigeonEventSink? = null + private var runForegroundService = false + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + val context = binding.applicationContext + applicationContext = context + + ActivityRecognitionHostApi.setUp(binding.binaryMessenger, this) + StreamActivitiesStreamHandler.register(binding.binaryMessenger, streamHandler) + + preferences = DetectedActivityStore.preferences(context).also { + it.registerOnSharedPreferenceChangeListener(this) + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + stopTracking() + + preferences?.unregisterOnSharedPreferenceChangeListener(this) + preferences = null + + ActivityRecognitionHostApi.setUp(binding.binaryMessenger, null) + applicationContext = null + } + + // MARK: - ActivityRecognitionHostApi + + override fun configure(configuration: TrackingConfiguration) { + runForegroundService = configuration.runForegroundService + } + + // MARK: - Tracking + + private fun startTracking() { + val context = applicationContext ?: return + + if (runForegroundService) { + context.startForegroundService(Intent(context, ForegroundService::class.java)) + } + + ActivityRecognition.getClient(context) + .requestActivityUpdates(DETECTION_INTERVAL_MS, detectionIntent(context)) + .addOnSuccessListener { Log.d(TAG, "Registered for activity updates.") } + .addOnFailureListener { Log.e(TAG, "Could not register for activity updates.", it) } + } + + private fun stopTracking() { + val context = applicationContext ?: return + + ActivityRecognition.getClient(context).removeActivityUpdates(detectionIntent(context)) + + if (runForegroundService) { + context.stopService(Intent(context, ForegroundService::class.java)) + } + } + + private fun detectionIntent(context: Context): PendingIntent { + val intent = Intent(context, ActivityRecognizedBroadcastReceiver::class.java) + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // The Activity Recognition API writes the detection result into this + // intent, so it has to stay mutable on API 31 and above. + flags = flags or PendingIntent.FLAG_MUTABLE + } + + return PendingIntent.getBroadcast(context, 0, intent, flags) + } + + // MARK: - Detections arriving from the broadcast receiver + + override fun onSharedPreferenceChanged(preferences: SharedPreferences?, key: String?) { + if (key != DetectedActivityStore.KEY_DETECTED_ACTIVITY) return + + val sink = eventSink ?: return + val activity = preferences?.let(DetectedActivityStore::read) ?: return + + // Event sinks have to be driven from the main thread, and preference + // changes are published on whichever thread wrote them. + mainHandler.post { sink.success(activity) } + } + + private inner class ActivityStreamHandler : StreamActivitiesStreamHandler() { + override fun onListen(p0: Any?, sink: PigeonEventSink) { + eventSink = sink + startTracking() + } + + override fun onCancel(p0: Any?) { + stopTracking() + eventSink = null + } + } +} diff --git a/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.kt b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.kt new file mode 100644 index 000000000..4de3ee847 --- /dev/null +++ b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ActivityRecognizedBroadcastReceiver.kt @@ -0,0 +1,41 @@ +package dk.carp.activity_recognition_flutter + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.google.android.gms.location.ActivityRecognitionResult +import com.google.android.gms.location.DetectedActivity + +/** + * Receives detections from the Activity Recognition API and hands the most + * probable one to [DetectedActivityStore]. + */ +class ActivityRecognizedBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (!ActivityRecognitionResult.hasResult(intent)) return + + val result = ActivityRecognitionResult.extractResult(intent) ?: return + val mostProbable = result.probableActivities.maxByOrNull { it.confidence } ?: return + + DetectedActivityStore.write( + context, + PlatformActivity( + type = mostProbable.toPlatformActivityType(), + confidence = mostProbable.confidence.toLong(), + timestamp = result.time, + ), + ) + } +} + +private fun DetectedActivity.toPlatformActivityType(): PlatformActivityType = when (type) { + DetectedActivity.IN_VEHICLE -> PlatformActivityType.IN_VEHICLE + DetectedActivity.ON_BICYCLE -> PlatformActivityType.ON_BICYCLE + DetectedActivity.ON_FOOT -> PlatformActivityType.ON_FOOT + DetectedActivity.RUNNING -> PlatformActivityType.RUNNING + DetectedActivity.STILL -> PlatformActivityType.STILL + DetectedActivity.TILTING -> PlatformActivityType.TILTING + DetectedActivity.WALKING -> PlatformActivityType.WALKING + else -> PlatformActivityType.UNKNOWN +} diff --git a/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/DetectedActivityStore.kt b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/DetectedActivityStore.kt new file mode 100644 index 000000000..b2d1a0474 --- /dev/null +++ b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/DetectedActivityStore.kt @@ -0,0 +1,48 @@ +package dk.carp.activity_recognition_flutter + +import android.content.Context +import android.content.SharedPreferences + +/** + * Carries detections from [ActivityRecognizedBroadcastReceiver] to + * [ActivityRecognitionFlutterPlugin]. + * + * The receiver is started by the system and may run while no Flutter engine is + * attached, so the two sides cannot hold references to each other. Writing the + * latest detection to shared preferences lets the plugin observe it whenever it + * happens to be listening. + */ +internal object DetectedActivityStore { + private const val PREFERENCES_NAME = "activity_recognition_flutter" + + const val KEY_DETECTED_ACTIVITY = "detected_activity" + + fun preferences(context: Context): SharedPreferences = + context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + fun write(context: Context, activity: PlatformActivity) { + preferences(context).edit() + // Clearing first guarantees a change notification even when the same + // activity is detected twice in a row. + .clear() + .putString(KEY_DETECTED_ACTIVITY, encode(activity)) + .apply() + } + + fun read(preferences: SharedPreferences): PlatformActivity? = + preferences.getString(KEY_DETECTED_ACTIVITY, null)?.let(::decode) + + private fun encode(activity: PlatformActivity): String = + "${activity.type.raw},${activity.confidence},${activity.timestamp}" + + private fun decode(value: String): PlatformActivity? { + val parts = value.split(",") + if (parts.size != 3) return null + + val type = parts[0].toIntOrNull()?.let(PlatformActivityType::ofRaw) ?: return null + val confidence = parts[1].toLongOrNull() ?: return null + val timestamp = parts[2].toLongOrNull() ?: return null + + return PlatformActivity(type = type, confidence = confidence, timestamp = timestamp) + } +} diff --git a/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ForegroundService.kt b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ForegroundService.kt new file mode 100644 index 000000000..5ec8831ad --- /dev/null +++ b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/ForegroundService.kt @@ -0,0 +1,59 @@ +package dk.carp.activity_recognition_flutter + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.os.IBinder + +/** + * Keeps the process alive so that detections keep arriving while the app is in + * the background. + * + * Started by [ActivityRecognitionFlutterPlugin] only when the Dart side asks for + * it through `runForegroundService`. The notification can be customised through + * the intent extras below. + */ +class ForegroundService : Service() { + + companion object { + const val EXTRA_TITLE = "title" + const val EXTRA_TEXT = "text" + const val EXTRA_ICON = "icon" + const val EXTRA_ID = "id" + + private const val CHANNEL_ID = "activity_recognition_flutter.foreground" + private const val DEFAULT_NOTIFICATION_ID = 197812504 + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + "Activity recognition", + // Low importance keeps the notification silent while still visible, + // which is what a background monitoring service should be. + NotificationManager.IMPORTANCE_LOW, + ).apply { + description = "Keeps detecting your activity while the app is in the background." + }, + ) + + val icon = intent?.getIntExtra(EXTRA_ICON, 0) ?: 0 + val notification = Notification.Builder(this, CHANNEL_ID) + .setContentTitle(intent?.getStringExtra(EXTRA_TITLE) ?: "Activity recognition") + .setContentText(intent?.getStringExtra(EXTRA_TEXT) ?: "Detecting your activity.") + .setSmallIcon(if (icon != 0) icon else android.R.drawable.ic_menu_compass) + .setOngoing(true) + .build() + + val notificationId = intent?.getIntExtra(EXTRA_ID, 0) ?: 0 + startForeground(if (notificationId != 0) notificationId else DEFAULT_NOTIFICATION_ID, notification) + + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null +} diff --git a/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/Messages.g.kt b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/Messages.g.kt new file mode 100644 index 000000000..16e58f6fd --- /dev/null +++ b/packages/activity_recognition_flutter/android/src/main/kotlin/dk/carp/activity_recognition_flutter/Messages.g.kt @@ -0,0 +1,458 @@ +// Autogenerated from Pigeon (v27.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package dk.carp.activity_recognition_flutter + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true + } + if (a is Array<*> && b is Array<*>) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true + } + if (a is List<*> && b is List<*>) { + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true + } + if (a is Map<*, *> && b is Map<*, *>) { + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) + } + return a == b + } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : RuntimeException() + +/** + * The activity types that can be reported by the native platforms. + * + * Android reports these directly; the smaller set of iOS `CMMotionActivity` + * flags is mapped onto them natively, so the wire format is identical on + * both platforms. + */ +enum class PlatformActivityType(val raw: Int) { + IN_VEHICLE(0), + ON_BICYCLE(1), + ON_FOOT(2), + RUNNING(3), + STILL(4), + TILTING(5), + UNKNOWN(6), + WALKING(7); + + companion object { + fun ofRaw(raw: Int): PlatformActivityType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * A single activity detection as reported by the platform. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformActivity ( + /** The detected activity. */ + val type: PlatformActivityType, + /** Confidence of the detection, in percent (0-100). */ + val confidence: Long, + /** + * When the activity was detected, in milliseconds since the Unix epoch. + * + * Produced natively so the timestamp reflects the detection itself rather + * than the moment the event happened to arrive in Dart. + */ + val timestamp: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformActivity { + val type = pigeonVar_list[0] as PlatformActivityType + val confidence = pigeonVar_list[1] as Long + val timestamp = pigeonVar_list[2] as Long + return PlatformActivity(type, confidence, timestamp) + } + } + fun toList(): List { + return listOf( + type, + confidence, + timestamp, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as PlatformActivity + return MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.confidence, other.confidence) && MessagesPigeonUtils.deepEquals(this.timestamp, other.timestamp) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.type) + result = 31 * result + MessagesPigeonUtils.deepHash(this.confidence) + result = 31 * result + MessagesPigeonUtils.deepHash(this.timestamp) + return result + } + override fun toString(): String { + return "PlatformActivity(type=$type, confidence=$confidence, timestamp=$timestamp)" + } +} + +/** + * Options applied to activity tracking before the event stream is started. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class TrackingConfiguration ( + /** + * Whether Android should run a foreground service so that detections keep + * arriving while the app is backgrounded. + * + * Ignored on iOS, where `CMMotionActivityManager` already delivers updates + * in the background. + */ + val runForegroundService: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): TrackingConfiguration { + val runForegroundService = pigeonVar_list[0] as Boolean + return TrackingConfiguration(runForegroundService) + } + } + fun toList(): List { + return listOf( + runForegroundService, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as TrackingConfiguration + return MessagesPigeonUtils.deepEquals(this.runForegroundService, other.runForegroundService) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.runForegroundService) + return result + } + override fun toString(): String { + return "TrackingConfiguration(runForegroundService=$runForegroundService)" + } +} +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + PlatformActivityType.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformActivity.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + TrackingConfiguration.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PlatformActivityType -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + is PlatformActivity -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is TrackingConfiguration -> { + stream.write(131) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) + +/** + * Control channel. Called before listening to [ActivityStreamApi]. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface ActivityRecognitionHostApi { + /** Applies [configuration] to the next tracking session. */ + fun configure(configuration: TrackingConfiguration) + + companion object { + /** The codec used by ActivityRecognitionHostApi. */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `ActivityRecognitionHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: ActivityRecognitionHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.activity_recognition_flutter.ActivityRecognitionHostApi.configure$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val configurationArg = args[0] as TrackingConfiguration + val wrapped: List = try { + api.configure(configurationArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} + +private class MessagesPigeonStreamHandler( + val wrapper: MessagesPigeonEventChannelWrapper +) : EventChannel.StreamHandler { + var pigeonSink: PigeonEventSink? = null + + override fun onListen(p0: Any?, sink: EventChannel.EventSink) { + pigeonSink = PigeonEventSink(sink) + wrapper.onListen(p0, pigeonSink!!) + } + + override fun onCancel(p0: Any?) { + pigeonSink = null + wrapper.onCancel(p0) + } +} + +interface MessagesPigeonEventChannelWrapper { + open fun onListen(p0: Any?, sink: PigeonEventSink) {} + + open fun onCancel(p0: Any?) {} +} + +class PigeonEventSink(private val sink: EventChannel.EventSink) { + fun success(value: T) { + sink.success(value) + } + + fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + sink.error(errorCode, errorMessage, errorDetails) + } + + fun endOfStream() { + sink.endOfStream() + } +} + +/** Data channel carrying the detected activities. */ +abstract class StreamActivitiesStreamHandler : MessagesPigeonEventChannelWrapper { + companion object { + fun register(messenger: BinaryMessenger, streamHandler: StreamActivitiesStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.activity_recognition_flutter.ActivityStreamApi.streamActivities" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) + } + } +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} + +override fun onCancel(p0: Any?) {} +} + diff --git a/packages/activity_recognition_flutter/example/android/app/build.gradle b/packages/activity_recognition_flutter/example/android/app/build.gradle deleted file mode 100644 index 432eb5a42..000000000 --- a/packages/activity_recognition_flutter/example/android/app/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "dk.cachet.activity_recognition_flutter_example" - testNamespace "dk.cachet.activity_recognition_flutter_example.test" - compileSdkVersion flutter.compileSdkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "dk.cachet.activity_recognition_flutter_example" - minSdkVersion 26 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} diff --git a/packages/activity_recognition_flutter/example/android/app/build.gradle.kts b/packages/activity_recognition_flutter/example/android/app/build.gradle.kts new file mode 100644 index 000000000..e58ee42ab --- /dev/null +++ b/packages/activity_recognition_flutter/example/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android Gradle Plugin. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "dk.carp.activity_recognition_flutter_example" + testNamespace = "dk.carp.activity_recognition_flutter_example.test" + + // permission_handler_android requires compiling against SDK 37. + compileSdk = 37 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "dk.carp.activity_recognition_flutter_example" + // The plugin needs notification channels and startForegroundService. + minSdk = 26 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } + + lint { + disable += "InvalidPackage" + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml index 23d7a4b6b..33bc229c9 100644 --- a/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml @@ -42,17 +42,8 @@ android:name="flutterEmbedding" android:value="2" /> - - - - + - - - - diff --git a/packages/activity_recognition_flutter/example/android/app/src/main/java/dk/cachet/activity_recognition_flutter_example/MainActivity.java b/packages/activity_recognition_flutter/example/android/app/src/main/java/dk/cachet/activity_recognition_flutter_example/MainActivity.java deleted file mode 100644 index 57917d8d1..000000000 --- a/packages/activity_recognition_flutter/example/android/app/src/main/java/dk/cachet/activity_recognition_flutter_example/MainActivity.java +++ /dev/null @@ -1,6 +0,0 @@ -package dk.cachet.activity_recognition_flutter_example; - -import io.flutter.embedding.android.FlutterActivity; - -public class MainActivity extends FlutterActivity { -} diff --git a/packages/activity_recognition_flutter/example/android/app/src/main/kotlin/dk/carp/activity_recognition_flutter_example/MainActivity.kt b/packages/activity_recognition_flutter/example/android/app/src/main/kotlin/dk/carp/activity_recognition_flutter_example/MainActivity.kt new file mode 100644 index 000000000..a3ea46619 --- /dev/null +++ b/packages/activity_recognition_flutter/example/android/app/src/main/kotlin/dk/carp/activity_recognition_flutter_example/MainActivity.kt @@ -0,0 +1,5 @@ +package dk.carp.activity_recognition_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/activity_recognition_flutter/example/android/build.gradle b/packages/activity_recognition_flutter/example/android/build.gradle deleted file mode 100644 index bc157bd1a..000000000 --- a/packages/activity_recognition_flutter/example/android/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/packages/activity_recognition_flutter/example/android/build.gradle.kts b/packages/activity_recognition_flutter/example/android/build.gradle.kts new file mode 100644 index 000000000..dbee657bb --- /dev/null +++ b/packages/activity_recognition_flutter/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/activity_recognition_flutter/example/android/gradle.properties b/packages/activity_recognition_flutter/example/android/gradle.properties index a6738207f..442df403d 100644 --- a/packages/activity_recognition_flutter/example/android/gradle.properties +++ b/packages/activity_recognition_flutter/example/android/gradle.properties @@ -1,4 +1,8 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true -android.enableJetifier=true -android.enableR8=true +# This builtInKotlin flag was added automatically by Flutter migrator +# Kept false: AGP 9's built-in Kotlin is Kotlin 2.2.10, below Flutter's minimum +# of 2.2.20, so the Kotlin Gradle plugin is declared in settings.gradle.kts. +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/packages/activity_recognition_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/activity_recognition_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index 8f3239ab6..a97e89ca1 100644 --- a/packages/activity_recognition_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/activity_recognition_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/packages/activity_recognition_flutter/example/android/settings.gradle b/packages/activity_recognition_flutter/example/android/settings.gradle deleted file mode 100644 index 19494fa21..000000000 --- a/packages/activity_recognition_flutter/example/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.2" - id "com.android.application" version '8.9.0' apply false - id "org.jetbrains.kotlin.android" version "1.9.20" apply false -} - -include ":app" \ No newline at end of file diff --git a/packages/activity_recognition_flutter/example/android/settings.gradle.kts b/packages/activity_recognition_flutter/example/android/settings.gradle.kts new file mode 100644 index 000000000..ef9c87aae --- /dev/null +++ b/packages/activity_recognition_flutter/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.2" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/packages/activity_recognition_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/activity_recognition_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 8c6e56146..ab8e063fe 100644 --- a/packages/activity_recognition_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/activity_recognition_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/packages/activity_recognition_flutter/example/ios/Flutter/Debug.xcconfig b/packages/activity_recognition_flutter/example/ios/Flutter/Debug.xcconfig index e8efba114..592ceee85 100644 --- a/packages/activity_recognition_flutter/example/ios/Flutter/Debug.xcconfig +++ b/packages/activity_recognition_flutter/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/activity_recognition_flutter/example/ios/Flutter/Release.xcconfig b/packages/activity_recognition_flutter/example/ios/Flutter/Release.xcconfig index 399e9340e..592ceee85 100644 --- a/packages/activity_recognition_flutter/example/ios/Flutter/Release.xcconfig +++ b/packages/activity_recognition_flutter/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/activity_recognition_flutter/example/ios/Podfile b/packages/activity_recognition_flutter/example/ios/Podfile deleted file mode 100644 index 279576f38..000000000 --- a/packages/activity_recognition_flutter/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 19babf78e..1af15e909 100644 --- a/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,14 +3,14 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 24A7651E5407004ACA8F4574 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D3861AE45E9E4A0F6101B5C /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -33,11 +33,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 55DAE46F7EFFFC65CAD56BA0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* activity_recognition_flutter */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = activity_recognition_flutter; path = ../../ios/activity_recognition_flutter; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7BA3D2FF28AEF196044136AD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -45,8 +46,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9D3861AE45E9E4A0F6101B5C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B82047B9D55539FAB3D824E9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,26 +53,19 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 24A7651E5407004ACA8F4574 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2BBABEDF8625AFA845500189 /* Pods */ = { - isa = PBXGroup; - children = ( - B82047B9D55539FAB3D824E9 /* Pods-Runner.debug.xcconfig */, - 7BA3D2FF28AEF196044136AD /* Pods-Runner.release.xcconfig */, - 55DAE46F7EFFFC65CAD56BA0 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* activity_recognition_flutter */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -88,8 +80,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 2BBABEDF8625AFA845500189 /* Pods */, - A12F4221223302A8D533B1EC /* Frameworks */, ); sourceTree = ""; }; @@ -116,14 +106,6 @@ path = Runner; sourceTree = ""; }; - A12F4221223302A8D533B1EC /* Frameworks */ = { - isa = PBXGroup; - children = ( - 9D3861AE45E9E4A0F6101B5C /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -131,20 +113,21 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 388E0730D4C55E1ABD759B49 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 5A4B0B838E074EF09BAFC73E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -173,6 +156,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -197,28 +183,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 388E0730D4C55E1ABD759B49 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -235,23 +199,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 5A4B0B838E074EF09BAFC73E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -343,7 +290,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -374,7 +321,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.activityRecognitionFlutterExample; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.activityRecognitionFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -430,7 +377,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -480,7 +427,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -513,7 +460,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.activityRecognitionFlutterExample; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.activityRecognitionFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -544,7 +491,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.activityRecognitionFlutterExample; + PRODUCT_BUNDLE_IDENTIFIER = dk.carp.activityRecognitionFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -576,6 +523,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c53e2b314..5db441f58 100644 --- a/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/activity_recognition_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + - - diff --git a/packages/activity_recognition_flutter/example/ios/Runner/AppDelegate.swift b/packages/activity_recognition_flutter/example/ios/Runner/AppDelegate.swift index b63630348..c30b367ec 100644 --- a/packages/activity_recognition_flutter/example/ios/Runner/AppDelegate.swift +++ b/packages/activity_recognition_flutter/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,16 @@ -import UIKit import Flutter +import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/packages/activity_recognition_flutter/example/ios/Runner/Info.plist b/packages/activity_recognition_flutter/example/ios/Runner/Info.plist index a8a3846a2..491b7da34 100644 --- a/packages/activity_recognition_flutter/example/ios/Runner/Info.plist +++ b/packages/activity_recognition_flutter/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -24,6 +26,29 @@ NSMotionUsageDescription Tracks the user activity + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +66,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/activity_recognition_flutter/example/pubspec.yaml b/packages/activity_recognition_flutter/example/pubspec.yaml index 479b39522..caf70b110 100644 --- a/packages/activity_recognition_flutter/example/pubspec.yaml +++ b/packages/activity_recognition_flutter/example/pubspec.yaml @@ -7,7 +7,7 @@ description: Demonstrates how to use the activity_recognition_flutter plugin. publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=3.8.0 <4.0.0" dependencies: flutter: @@ -23,12 +23,20 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.8 + + # Used to request the runtime activity recognition permission on Android. + permission_handler: ^13.0.0 dev_dependencies: flutter_test: sdk: flutter - permission_handler: ^7.0.0 flutter: + # The plugin ships only as a Swift package, so pin the example to Swift + # Package Manager instead of relying on the global `flutter config` value. + # Without this, a machine that has SPM disabled would fall back to CocoaPods + # and re-add the Pods include to ios/Flutter/{Debug,Release}.xcconfig. + config: + enable-swift-package-manager: true uses-material-design: true diff --git a/packages/activity_recognition_flutter/ios/Assets/.gitkeep b/packages/activity_recognition_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.h b/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.h deleted file mode 100644 index 4642b53c6..000000000 --- a/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface ActivityRecognitionFlutterPlugin : NSObject -@end diff --git a/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.m b/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.m deleted file mode 100644 index 9f649c299..000000000 --- a/packages/activity_recognition_flutter/ios/Classes/ActivityRecognitionFlutterPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "ActivityRecognitionFlutterPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "activity_recognition_flutter-Swift.h" -#endif - -@implementation ActivityRecognitionFlutterPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftActivityRecognitionFlutterPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/packages/activity_recognition_flutter/ios/Classes/SwiftActivityRecognitionFlutterPlugin.swift b/packages/activity_recognition_flutter/ios/Classes/SwiftActivityRecognitionFlutterPlugin.swift deleted file mode 100644 index 5eacf8eb7..000000000 --- a/packages/activity_recognition_flutter/ios/Classes/SwiftActivityRecognitionFlutterPlugin.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Flutter -import UIKit -import CoreMotion - - -public class SwiftActivityRecognitionFlutterPlugin: NSObject, FlutterPlugin { - - public static func register(with registrar: FlutterPluginRegistrar) { - let handler = ActivityStreamHandler()  -    let channel = FlutterEventChannel(name: "activity_recognition_flutter", binaryMessenger: registrar.messenger()) -    channel.setStreamHandler(handler) - } -} -public class ActivityStreamHandler: NSObject, FlutterStreamHandler { - - private let activityManager = CMMotionActivityManager() - - public func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? { - activityManager.startActivityUpdates(to: OperationQueue.init()) { (activity) in - if let a = activity { - - let type = self.extractActivityType(a: a) - let confidence = self.extractActivityConfidence(a: a) - let data = "\(type),\(confidence)" - - /// Send event to flutter - eventSink(data) - } - } - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - activityManager.stopActivityUpdates() - return nil - } - - func extractActivityType(a: CMMotionActivity) -> String { - var type = "UNKNOWN" - switch true { - case a.stationary: - type = "STILL" - case a.walking: - type = "WALKING" - case a.running: - type = "RUNNING" - case a.automotive: - type = "IN_VEHICLE" - case a.cycling: - type = "ON_BICYCLE" - default: - type = "UNKNOWN" - } - return type - } - - func extractActivityConfidence(a: CMMotionActivity) -> Int { - var conf = -1 - - switch a.confidence { - case CMMotionActivityConfidence.low: - conf = 10 - case CMMotionActivityConfidence.medium: - conf = 50 - case CMMotionActivityConfidence.high: - conf = 100 - default: - conf = -1 - } - return conf - } - -} diff --git a/packages/activity_recognition_flutter/ios/activity_recognition_flutter.podspec b/packages/activity_recognition_flutter/ios/activity_recognition_flutter.podspec deleted file mode 100644 index c2773180d..000000000 --- a/packages/activity_recognition_flutter/ios/activity_recognition_flutter.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint activity_recognition_flutter.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'activity_recognition_flutter' - s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' -end diff --git a/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Package.swift b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Package.swift new file mode 100644 index 000000000..12001c609 --- /dev/null +++ b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "activity_recognition_flutter", + platforms: [ + .iOS("15.0") + ], + products: [ + .library(name: "activity-recognition-flutter", targets: ["activity_recognition_flutter"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "activity_recognition_flutter", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ], + resources: [ + // If your plugin requires a privacy manifest, for example if it uses any required + // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's + // privacy impact, and then uncomment these lines. For more information, see + // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + // .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.swift b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.swift new file mode 100644 index 000000000..6e6860a71 --- /dev/null +++ b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/ActivityRecognitionFlutterPlugin.swift @@ -0,0 +1,90 @@ +import CoreMotion +import Flutter +import UIKit + +/// Activity recognition for iOS, backed by `CMMotionActivityManager`. +/// +/// The Dart <-> native contract is generated by Pigeon from `pigeons/messages.dart`; +/// see `Messages.g.swift`. Configuration arrives over a host API and detections are +/// pushed back over an event channel. +public class ActivityRecognitionFlutterPlugin: NSObject, FlutterPlugin, ActivityRecognitionHostApi { + private let streamHandler = ActivityStreamHandler() + + public static func register(with registrar: FlutterPluginRegistrar) { + let plugin = ActivityRecognitionFlutterPlugin() + + ActivityRecognitionHostApiSetup.setUp( + binaryMessenger: registrar.messenger(), + api: plugin + ) + StreamActivitiesStreamHandler.register( + with: registrar.messenger(), + streamHandler: plugin.streamHandler + ) + + registrar.publish(plugin) + } + + // MARK: - ActivityRecognitionHostApi + + /// `runForegroundService` is Android-only: `CMMotionActivityManager` already + /// delivers updates while the app is in the background, so there is nothing + /// to configure on iOS. + func configure(configuration: TrackingConfiguration) throws {} +} + +/// Bridges `CMMotionActivityManager` updates onto the Pigeon event channel. +final class ActivityStreamHandler: StreamActivitiesStreamHandler { + private let activityManager = CMMotionActivityManager() + private let updateQueue = OperationQueue() + + override func onListen(withArguments arguments: Any?, sink: PigeonEventSink) { + guard CMMotionActivityManager.isActivityAvailable() else { + sink.error( + code: "UNAVAILABLE", + message: "Motion activity is not available on this device.", + details: nil + ) + sink.endOfStream() + return + } + + activityManager.startActivityUpdates(to: updateQueue) { activity in + guard let activity else { return } + + let event = PlatformActivity( + type: Self.activityType(of: activity), + confidence: Self.confidence(of: activity), + timestamp: Int64(activity.startDate.timeIntervalSince1970 * 1000) + ) + + // Event sinks must be driven from the platform thread. + DispatchQueue.main.async { sink.success(event) } + } + } + + override func onCancel(withArguments arguments: Any?) { + activityManager.stopActivityUpdates() + } + + /// `CMMotionActivity` exposes independent flags that may be set at the same + /// time, so they are checked in priority order and the first match wins. + private static func activityType(of activity: CMMotionActivity) -> PlatformActivityType { + if activity.stationary { return .still } + if activity.walking { return .walking } + if activity.running { return .running } + if activity.automotive { return .inVehicle } + if activity.cycling { return .onBicycle } + return .unknown + } + + /// Maps the three-level iOS confidence onto the 0-100 scale Android reports. + private static func confidence(of activity: CMMotionActivity) -> Int64 { + switch activity.confidence { + case .low: return 10 + case .medium: return 50 + case .high: return 100 + @unknown default: return 0 + } + } +} diff --git a/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/Messages.g.swift b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/Messages.g.swift new file mode 100644 index 000000000..f47783ec0 --- /dev/null +++ b/packages/activity_recognition_flutter/ios/activity_recognition_flutter/Sources/activity_recognition_flutter/Messages.g.swift @@ -0,0 +1,446 @@ +// Autogenerated from Pigeon (v27.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(Swift.type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +enum MessagesPigeonInternal { + static func isNullish(_ value: Any?) -> Bool { + guard let innerValue = value else { + return true + } + + if case Optional.some(Optional.none) = value { + return true + } + + return innerValue is NSNull + } + static func doubleEquals(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs + } + + static func doubleHash(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } + } + + static func deepEquals(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + + case is (Void, Void): + return true + + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEquals(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEquals(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEquals(lhsKey, rhsKey) { + if deepEquals(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEquals(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + + default: + return false + } + } + + static func deepHash(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHash(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHash(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHash(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHash(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHash(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } + } else { + hasher.combine(0) + } + } + +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + + +/// The activity types that can be reported by the native platforms. +/// +/// Android reports these directly; the smaller set of iOS `CMMotionActivity` +/// flags is mapped onto them natively, so the wire format is identical on +/// both platforms. +enum PlatformActivityType: Int, CaseIterable { + case inVehicle = 0 + case onBicycle = 1 + case onFoot = 2 + case running = 3 + case still = 4 + case tilting = 5 + case unknown = 6 + case walking = 7 +} + +/// A single activity detection as reported by the platform. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformActivity: Hashable, CustomStringConvertible { + /// The detected activity. + var type: PlatformActivityType + /// Confidence of the detection, in percent (0-100). + var confidence: Int64 + /// When the activity was detected, in milliseconds since the Unix epoch. + /// + /// Produced natively so the timestamp reflects the detection itself rather + /// than the moment the event happened to arrive in Dart. + var timestamp: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformActivity? { + let type = pigeonVar_list[0] as! PlatformActivityType + let confidence = pigeonVar_list[1] as! Int64 + let timestamp = pigeonVar_list[2] as! Int64 + + return PlatformActivity( + type: type, + confidence: confidence, + timestamp: timestamp + ) + } + func toList() -> [Any?] { + return [ + type, + confidence, + timestamp, + ] + } + static func == (lhs: PlatformActivity, rhs: PlatformActivity) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.type, rhs.type) && MessagesPigeonInternal.deepEquals(lhs.confidence, rhs.confidence) && MessagesPigeonInternal.deepEquals(lhs.timestamp, rhs.timestamp) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformActivity") + MessagesPigeonInternal.deepHash(value: type, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: confidence, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: timestamp, hasher: &hasher) + } + + public var description: String { + return "PlatformActivity(type: \(String(describing: type)), confidence: \(String(describing: confidence)), timestamp: \(String(describing: timestamp)))" + } +} + +/// Options applied to activity tracking before the event stream is started. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct TrackingConfiguration: Hashable, CustomStringConvertible { + /// Whether Android should run a foreground service so that detections keep + /// arriving while the app is backgrounded. + /// + /// Ignored on iOS, where `CMMotionActivityManager` already delivers updates + /// in the background. + var runForegroundService: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TrackingConfiguration? { + let runForegroundService = pigeonVar_list[0] as! Bool + + return TrackingConfiguration( + runForegroundService: runForegroundService + ) + } + func toList() -> [Any?] { + return [ + runForegroundService + ] + } + static func == (lhs: TrackingConfiguration, rhs: TrackingConfiguration) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.runForegroundService, rhs.runForegroundService) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("TrackingConfiguration") + MessagesPigeonInternal.deepHash(value: runForegroundService, hasher: &hasher) + } + + public var description: String { + return "TrackingConfiguration(runForegroundService: \(String(describing: runForegroundService)))" + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformActivityType(rawValue: enumResultAsInt) + } + return nil + case 130: + return PlatformActivity.fromList(self.readValue() as! [Any?]) + case 131: + return TrackingConfiguration.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? PlatformActivityType { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformActivity { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? TrackingConfiguration { + super.writeByte(131) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +var messagesPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: MessagesPigeonCodecReaderWriter()); + +/// Control channel. Called before listening to [ActivityStreamApi]. +/// +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ActivityRecognitionHostApi { + /// Applies [configuration] to the next tracking session. + func configure(configuration: TrackingConfiguration) throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ActivityRecognitionHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `ActivityRecognitionHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ActivityRecognitionHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Applies [configuration] to the next tracking session. + let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.activity_recognition_flutter.ActivityRecognitionHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + configureChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let configurationArg = args[0] as! TrackingConfiguration + do { + try api.configure(configuration: configurationArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + configureChannel.setMessageHandler(nil) + } + } +} + +private class PigeonStreamHandler: NSObject, FlutterStreamHandler { + private let wrapper: PigeonEventChannelWrapper + private var pigeonSink: PigeonEventSink? = nil + + init(wrapper: PigeonEventChannelWrapper) { + self.wrapper = wrapper + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) + -> FlutterError? + { + pigeonSink = PigeonEventSink(events) + wrapper.onListen(withArguments: arguments, sink: pigeonSink!) + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + pigeonSink = nil + wrapper.onCancel(withArguments: arguments) + return nil + } +} + +class PigeonEventChannelWrapper { + func onListen(withArguments arguments: Any?, sink: PigeonEventSink) {} + func onCancel(withArguments arguments: Any?) {} +} + +class PigeonEventSink { + private let sink: FlutterEventSink + + init(_ sink: @escaping FlutterEventSink) { + self.sink = sink + } + + func success(_ value: ReturnType) { + sink(value) + } + + func error(code: String, message: String?, details: Any?) { + sink(FlutterError(code: code, message: message, details: details)) + } + + func endOfStream() { + sink(FlutterEndOfEventStream) + } + +} + +/// Data channel carrying the detected activities. +class StreamActivitiesStreamHandler: PigeonEventChannelWrapper { + static func register(with messenger: FlutterBinaryMessenger, + instanceName: String = "", + streamHandler: StreamActivitiesStreamHandler) { + var channelName = "dev.flutter.pigeon.activity_recognition_flutter.ActivityStreamApi.streamActivities" + if !instanceName.isEmpty { + channelName += ".\(instanceName)" + } + let internalStreamHandler = PigeonStreamHandler(wrapper: streamHandler) + let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: messagesPigeonMethodCodec) + channel.setStreamHandler(internalStreamHandler) + } +} + diff --git a/packages/activity_recognition_flutter/lib/activity_recognition_domain.dart b/packages/activity_recognition_flutter/lib/activity_recognition_domain.dart index db65d7620..acc0d5b75 100644 --- a/packages/activity_recognition_flutter/lib/activity_recognition_domain.dart +++ b/packages/activity_recognition_flutter/lib/activity_recognition_domain.dart @@ -1,8 +1,9 @@ part of activity_recognition; /// The different types of activities which can be detected. -/// These types is identical to the types detected on Android -/// and iOS types are mapped to these. +/// +/// These types are identical to the ones detected on Android; the smaller set +/// of activities iOS reports is mapped onto them natively. enum ActivityType { IN_VEHICLE, ON_BICYCLE, @@ -12,62 +13,45 @@ enum ActivityType { TILTING, UNKNOWN, WALKING, - INVALID // Used for parsing errors } -Map _activityMap = { - // Android - 'IN_VEHICLE': ActivityType.IN_VEHICLE, - 'ON_BICYCLE': ActivityType.ON_BICYCLE, - 'ON_FOOT': ActivityType.ON_FOOT, - 'RUNNING': ActivityType.RUNNING, - 'STILL': ActivityType.STILL, - 'TILTING': ActivityType.TILTING, - 'UNKNOWN': ActivityType.UNKNOWN, - 'WALKING': ActivityType.WALKING, - - // iOS - 'automotive': ActivityType.IN_VEHICLE, - 'cycling': ActivityType.ON_BICYCLE, - 'running': ActivityType.RUNNING, - 'stationary': ActivityType.STILL, - 'unknown': ActivityType.UNKNOWN, - 'walking': ActivityType.WALKING, +const Map _activityTypes = + { + PlatformActivityType.inVehicle: ActivityType.IN_VEHICLE, + PlatformActivityType.onBicycle: ActivityType.ON_BICYCLE, + PlatformActivityType.onFoot: ActivityType.ON_FOOT, + PlatformActivityType.running: ActivityType.RUNNING, + PlatformActivityType.still: ActivityType.STILL, + PlatformActivityType.tilting: ActivityType.TILTING, + PlatformActivityType.unknown: ActivityType.UNKNOWN, + PlatformActivityType.walking: ActivityType.WALKING, }; /// Represents an activity event detected on the phone. class ActivityEvent { /// The type of activity. - ActivityType type; + final ActivityType type; - /// The confidence of the dection in percentage. - int confidence; + /// The confidence of the detection in percent (0-100). + final int confidence; - /// The timestamp when detected. - late DateTime timeStamp; + /// The timestamp of the detection, as reported by the platform. + final DateTime timeStamp; /// The type of activity as a String. - String get typeString => type.toString().split('.').last; + String get typeString => type.name; - ActivityEvent(this.type, this.confidence) { - this.timeStamp = DateTime.now(); - } + ActivityEvent(this.type, this.confidence, [DateTime? timeStamp]) + : timeStamp = timeStamp ?? DateTime.now(); factory ActivityEvent.unknown() => ActivityEvent(ActivityType.UNKNOWN, 100); - /// Create an [ActivityEvent] based on the string format `type,confidence`. - factory ActivityEvent.fromString(String string) { - List tokens = string.split(","); - if (tokens.length < 2) return ActivityEvent.unknown(); - - ActivityType type = ActivityType.UNKNOWN; - if (_activityMap.containsKey(tokens.first)) { - type = _activityMap[tokens.first]!; - } - int conf = int.tryParse(tokens.last)!; - - return ActivityEvent(type, conf); - } + factory ActivityEvent._fromPlatform(PlatformActivity activity) => + ActivityEvent( + _activityTypes[activity.type] ?? ActivityType.UNKNOWN, + activity.confidence, + DateTime.fromMillisecondsSinceEpoch(activity.timestamp), + ); @override String toString() => 'Activity - type: $typeString, confidence: $confidence%'; diff --git a/packages/activity_recognition_flutter/lib/activity_recognition_flutter.dart b/packages/activity_recognition_flutter/lib/activity_recognition_flutter.dart index 4158d59c9..1867aa322 100644 --- a/packages/activity_recognition_flutter/lib/activity_recognition_flutter.dart +++ b/packages/activity_recognition_flutter/lib/activity_recognition_flutter.dart @@ -1,36 +1,73 @@ library activity_recognition; import 'dart:async'; -import 'package:flutter/services.dart'; + +import 'src/messages.g.dart'; part 'activity_recognition_domain.dart'; -/// Main entry to activity recognition API. Use as a singleton like +/// Main entry to the activity recognition API. Use as a singleton like /// /// `ActivityRecognition()` /// class ActivityRecognition { - static const EventChannel _eventChannel = - const EventChannel('activity_recognition_flutter'); - Stream? _stream; - static ActivityRecognition _instance = ActivityRecognition._(); + static final ActivityRecognition _instance = ActivityRecognition._(); + ActivityRecognition._(); /// Get the [ActivityRecognition] singleton. factory ActivityRecognition() => _instance; + final ActivityRecognitionHostApi _hostApi = ActivityRecognitionHostApi(); + + Stream? _stream; + /// Requests continuous [ActivityEvent] updates. /// - /// The Stream will output the *most probable* [ActivityEvent]. - /// By default the foreground service is enabled, which allows the - /// updates to be streamed while the app runs in the background. - /// The programmer can choose to not enable to foreground service. - Stream activityStream({bool runForegroundService = true}) { - if (_stream == null) { - _stream = _eventChannel - .receiveBroadcastStream({"foreground": runForegroundService}).map( - (json) => ActivityEvent.fromString(json)); - } - return _stream!; + /// The stream emits the *most probable* [ActivityEvent] detected by the phone. + /// + /// On Android a foreground service is started by default, which allows the + /// updates to keep arriving while the app runs in the background. Pass + /// `runForegroundService: false` to opt out. The flag is ignored on iOS, + /// where updates are delivered in the background regardless. + /// + /// The returned stream is a broadcast stream and is built once; later calls + /// return the same stream and do not re-apply [runForegroundService]. + Stream activityStream({bool runForegroundService = true}) => + _stream ??= _createStream(runForegroundService); + + Stream _createStream(bool runForegroundService) { + // `streamActivities` opens a new event channel on every call, so it is + // invoked exactly once and the resulting stream reused. + final Stream events = + streamActivities().map(ActivityEvent._fromPlatform); + + late final StreamController controller; + StreamSubscription? subscription; + + controller = StreamController.broadcast( + onListen: () async { + // The platform starts tracking as soon as the event channel is listened + // to, so the configuration has to be applied before subscribing. + await _hostApi.configure( + TrackingConfiguration(runForegroundService: runForegroundService), + ); + + // The listener may have cancelled while the configuration was in flight. + if (!controller.hasListener) return; + + subscription = events.listen( + controller.add, + onError: controller.addError, + onDone: controller.close, + ); + }, + onCancel: () async { + await subscription?.cancel(); + subscription = null; + }, + ); + + return controller.stream; } } diff --git a/packages/activity_recognition_flutter/lib/src/messages.g.dart b/packages/activity_recognition_flutter/lib/src/messages.g.dart new file mode 100644 index 000000000..1b59bd5d9 --- /dev/null +++ b/packages/activity_recognition_flutter/lib/src/messages.g.dart @@ -0,0 +1,320 @@ +// Autogenerated from Pigeon (v27.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; +} + +bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; + } + return a == b; +} + +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + + +/// The activity types that can be reported by the native platforms. +/// +/// Android reports these directly; the smaller set of iOS `CMMotionActivity` +/// flags is mapped onto them natively, so the wire format is identical on +/// both platforms. +enum PlatformActivityType { + inVehicle, + onBicycle, + onFoot, + running, + still, + tilting, + unknown, + walking, +} + +/// A single activity detection as reported by the platform. +class PlatformActivity { + PlatformActivity({ + required this.type, + required this.confidence, + required this.timestamp, + }); + + /// The detected activity. + PlatformActivityType type; + + /// Confidence of the detection, in percent (0-100). + int confidence; + + /// When the activity was detected, in milliseconds since the Unix epoch. + /// + /// Produced natively so the timestamp reflects the detection itself rather + /// than the moment the event happened to arrive in Dart. + int timestamp; + + List _toList() { + return [ + type, + confidence, + timestamp, + ]; + } + + Object encode() { + return _toList(); } + + static PlatformActivity decode(Object result) { + result as List; + return PlatformActivity( + type: result[0]! as PlatformActivityType, + confidence: result[1]! as int, + timestamp: result[2]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformActivity || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(type, other.type) && _deepEquals(confidence, other.confidence) && _deepEquals(timestamp, other.timestamp); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformActivity(type: $type, confidence: $confidence, timestamp: $timestamp)'; + } +} + +/// Options applied to activity tracking before the event stream is started. +class TrackingConfiguration { + TrackingConfiguration({ + required this.runForegroundService, + }); + + /// Whether Android should run a foreground service so that detections keep + /// arriving while the app is backgrounded. + /// + /// Ignored on iOS, where `CMMotionActivityManager` already delivers updates + /// in the background. + bool runForegroundService; + + List _toList() { + return [ + runForegroundService, + ]; + } + + Object encode() { + return _toList(); } + + static TrackingConfiguration decode(Object result) { + result as List; + return TrackingConfiguration( + runForegroundService: result[0]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! TrackingConfiguration || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(runForegroundService, other.runForegroundService); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'TrackingConfiguration(runForegroundService: $runForegroundService)'; + } +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformActivityType) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformActivity) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is TrackingConfiguration) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformActivityType.values[value]; + case 130: + return PlatformActivity.decode(readValue(buffer)!); + case 131: + return TrackingConfiguration.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); + +/// Control channel. Called before listening to [ActivityStreamApi]. +class ActivityRecognitionHostApi { + /// Constructor for [ActivityRecognitionHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ActivityRecognitionHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Applies [configuration] to the next tracking session. + Future configure(TrackingConfiguration configuration) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.activity_recognition_flutter.ActivityRecognitionHostApi.configure$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([configuration]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } +} + +/// Data channel carrying the detected activities. +/// Returns a broadcast [Stream] of events from the `streamActivities` event channel. +/// +/// Each call to this method creates a new [EventChannel], so it should +/// not be called multiple times for the same `instanceName`. To deliver +/// events to multiple listeners, call this method once and listen to the +/// returned broadcast stream multiple times instead. +Stream streamActivities( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel streamActivitiesChannel = + EventChannel('dev.flutter.pigeon.activity_recognition_flutter.ActivityStreamApi.streamActivities$instanceName', pigeonMethodCodec); + return streamActivitiesChannel.receiveBroadcastStream().map((dynamic event) { + return event as PlatformActivity; + }); +} + diff --git a/packages/activity_recognition_flutter/pigeons/messages.dart b/packages/activity_recognition_flutter/pigeons/messages.dart new file mode 100644 index 000000000..352acbaec --- /dev/null +++ b/packages/activity_recognition_flutter/pigeons/messages.dart @@ -0,0 +1,90 @@ +// Interface definition for the native activity recognition code. +// +// This file is the single source of truth for the Dart <-> native contract. +// It is not shipped with the package; it is consumed by Pigeon to generate +// the typed message plumbing for all three languages: +// +// lib/src/messages.g.dart +// ios/activity_recognition_flutter/Sources/activity_recognition_flutter/Messages.g.swift +// android/src/main/kotlin/dk/carp/activity_recognition_flutter/Messages.g.kt +// +// Regenerate after editing with: +// +// dart run pigeon --input pigeons/messages.dart + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartOptions: DartOptions(), + swiftOut: + 'ios/activity_recognition_flutter/Sources/activity_recognition_flutter/Messages.g.swift', + swiftOptions: SwiftOptions(), + kotlinOut: + 'android/src/main/kotlin/dk/carp/activity_recognition_flutter/Messages.g.kt', + kotlinOptions: KotlinOptions(package: 'dk.carp.activity_recognition_flutter'), + dartPackageName: 'activity_recognition_flutter', + ), +) +/// The activity types that can be reported by the native platforms. +/// +/// Android reports these directly; the smaller set of iOS `CMMotionActivity` +/// flags is mapped onto them natively, so the wire format is identical on +/// both platforms. +enum PlatformActivityType { + inVehicle, + onBicycle, + onFoot, + running, + still, + tilting, + unknown, + walking, +} + +/// A single activity detection as reported by the platform. +class PlatformActivity { + PlatformActivity({ + required this.type, + required this.confidence, + required this.timestamp, + }); + + /// The detected activity. + final PlatformActivityType type; + + /// Confidence of the detection, in percent (0-100). + final int confidence; + + /// When the activity was detected, in milliseconds since the Unix epoch. + /// + /// Produced natively so the timestamp reflects the detection itself rather + /// than the moment the event happened to arrive in Dart. + final int timestamp; +} + +/// Options applied to activity tracking before the event stream is started. +class TrackingConfiguration { + TrackingConfiguration({required this.runForegroundService}); + + /// Whether Android should run a foreground service so that detections keep + /// arriving while the app is backgrounded. + /// + /// Ignored on iOS, where `CMMotionActivityManager` already delivers updates + /// in the background. + final bool runForegroundService; +} + +/// Control channel. Called before listening to [ActivityStreamApi]. +@HostApi() +abstract class ActivityRecognitionHostApi { + /// Applies [configuration] to the next tracking session. + void configure(TrackingConfiguration configuration); +} + +/// Data channel carrying the detected activities. +@EventChannelApi() +abstract class ActivityStreamApi { + PlatformActivity streamActivities(); +} diff --git a/packages/activity_recognition_flutter/pubspec.yaml b/packages/activity_recognition_flutter/pubspec.yaml index 758f0dc06..14c399d87 100644 --- a/packages/activity_recognition_flutter/pubspec.yaml +++ b/packages/activity_recognition_flutter/pubspec.yaml @@ -1,11 +1,11 @@ name: activity_recognition_flutter description: Activity recognition plugin for Android and iOS. Provides event-based information about activities detected by the phone. -version: 5.0.0 +version: 6.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ^3.10.0 + flutter: ">=3.44.0" dependencies: flutter: @@ -14,12 +14,15 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^27.1.0 flutter: + config: + enable-swift-package-manager: true plugin: platforms: android: - package: dk.cachet.activity_recognition_flutter + package: dk.carp.activity_recognition_flutter pluginClass: ActivityRecognitionFlutterPlugin ios: pluginClass: ActivityRecognitionFlutterPlugin diff --git a/packages/activity_recognition_flutter/test/activity_recognition_flutter_test.dart b/packages/activity_recognition_flutter/test/activity_recognition_flutter_test.dart index 1c8f21859..b4c8354e1 100644 --- a/packages/activity_recognition_flutter/test/activity_recognition_flutter_test.dart +++ b/packages/activity_recognition_flutter/test/activity_recognition_flutter_test.dart @@ -1,20 +1,109 @@ +import 'package:activity_recognition_flutter/activity_recognition_flutter.dart'; +import 'package:activity_recognition_flutter/src/messages.g.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -void main() { - const MethodChannel channel = MethodChannel('activity_recognition_flutter'); +const String _configureChannel = + 'dev.flutter.pigeon.activity_recognition_flutter.ActivityRecognitionHostApi.configure'; + +final EventChannel _activityChannel = EventChannel( + 'dev.flutter.pigeon.activity_recognition_flutter.ActivityStreamApi.streamActivities', + pigeonMethodCodec, +); +void main() { TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return '42'; + group('ActivityEvent', () { + test('exposes the activity type as a string', () { + expect(ActivityEvent(ActivityType.WALKING, 75).typeString, 'WALKING'); + }); + + test('unknown() reports an unknown activity', () { + final event = ActivityEvent.unknown(); + + expect(event.type, ActivityType.UNKNOWN); + expect(event.confidence, 100); + }); + + test('falls back to the current time when none is given', () { + final before = DateTime.now(); + final event = ActivityEvent(ActivityType.STILL, 50); + + expect( + event.timeStamp.isBefore(before.subtract(const Duration(seconds: 1))), + isFalse, + ); + }); + + test('describes itself', () { + expect( + ActivityEvent(ActivityType.RUNNING, 80).toString(), + 'Activity - type: RUNNING, confidence: 80%', + ); }); }); - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); + group('activityStream', () { + late TestDefaultBinaryMessenger messenger; + final List configurations = []; + + setUp(() { + messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + messenger.setMockMessageHandler( + _configureChannel, + (ByteData? message) async { + final args = ActivityRecognitionHostApi.pigeonChannelCodec + .decodeMessage(message) as List; + configurations.add(args.first! as TrackingConfiguration); + + return ActivityRecognitionHostApi.pigeonChannelCodec + .encodeMessage([null]); + }, + ); + }); + + tearDown(() { + messenger.setMockMessageHandler(_configureChannel, null); + messenger.setMockStreamHandler(_activityChannel, null); + }); + + test('configures the platform and maps detections to ActivityEvent', + () async { + messenger.setMockStreamHandler( + _activityChannel, + MockStreamHandler.inline( + onListen: (Object? arguments, MockStreamHandlerEventSink events) { + events.success( + PlatformActivity( + type: PlatformActivityType.onBicycle, + confidence: 65, + timestamp: 1700000000000, + ), + ); + events.endOfStream(); + }, + ), + ); + + final events = await ActivityRecognition() + .activityStream(runForegroundService: false) + .take(1) + .toList(); + + // The tracking options have to reach the platform before it starts. + expect(configurations, hasLength(1)); + expect(configurations.single.runForegroundService, isFalse); + + expect(events, hasLength(1)); + expect(events.single.type, ActivityType.ON_BICYCLE); + expect(events.single.confidence, 65); + expect( + events.single.timeStamp, + DateTime.fromMillisecondsSinceEpoch(1700000000000), + ); + }); }); }