Adding OpenHaystack Mobile app

Co-Authored-By: Lukas Burg <lukas.burg@hemalu.de>
This commit is contained in:
MaxGranzow
2022-05-11 13:02:07 +02:00
committed by Alexander Heinrich
parent b65a6e6be0
commit 3d593a006c
182 changed files with 10499 additions and 0 deletions

46
openhaystack-mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 18116933e77adc82f80866c928266a5b4f1ed645
channel: stable
project_type: app

View File

@@ -0,0 +1,54 @@
# OpenHaystack Mobile
Seemoo Lab WS21/22 project: Porting OpenHaystack to Mobile
# About OpenHaystack
OpenHaystack is a project that allows location tracking of Bluetooth Low Energy (BLE) devices over Apples Find My Network.
See the [OpenHaystack GitHub page](https://github.com/seemoo-lab/openhaystack/) for more deatils on how it works.
# Development
This project is written in [Dart](https://dart.dev/), using the cross platform development framework [Flutter](https://flutter.dev/). This allows the creation of apps for all major platforms using a single code base.
## Requisites
To develop and build the project the following tools are needed and should be installed.
- [Flutter SDK](https://docs.flutter.dev/get-started/install)
- [Xcode](https://developer.apple.com/xcode/) (for iOS)
- [Android SDK / Studio](https://developer.android.com/studio/) (for Android)
- (optional) IDE Plugin (e.g. for [VS Code](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter))
To check the installation run `flutter doctor`. Before continuing review all displayed errors.
## Getting Started
First the necessary dependencies need to be installed. The IDE plugin may take care of this automatically.
```bash
$ flutter pub get
```
Then set the location proxy server URL in [reports_fetcher.dart](lib/findMy/reports_fetcher.dart) (replace `https://add-your-proxy-server-here/getLocationReports` with your custom URL).
To run the debug version of the app start a supported emulator and run
```bash
$ flutter run
```
When the app is running a new key pair can be created / imported in the app.
## Project Structure
The project follows the default structure for flutter applications. The `android`, `ios` and `web` folders contain native projects for the specified platform. Native code can be added here for example to access special APIs.
The business logic and UI can be found in the `lib` folder. This folder is furthermore separated into modules containing code regarding a common aspect.
The business logic for accessing and decrypting the location reports is separated in the `findMy` folder for easier reuse.
## Building
This project currently supports iOS and Android targets.
If you are building the project for the first time, you need to run
```bash
$ flutter pub run flutter_launcher_icons:main
```
to create the icons and then, to create a distributable application package run
```bash
$ flutter build [ios|apk|web]
```
The resulting build artifacts can be found in the `build` folder. To deploy the artifacts to a device consult the platform specific documentation.

View File

@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
openhaystack-mobile/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,68 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "de.seemoo.android.openhaystack"
minSdkVersion 21
targetSdkVersion 30
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 '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,63 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<application
android:label="OpenHaystack"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/json" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,6 @@
package de.seemoo.android.openhaystack
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,29 @@
buildscript {
ext.kotlin_version = '1.6.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

34
openhaystack-mobile/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.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

View File

@@ -0,0 +1,123 @@
PODS:
- DKImagePickerController/Core (4.3.2):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.2)
- DKImagePickerController/PhotoGallery (4.3.2):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.2)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_secure_storage (3.3.1):
- Flutter
- geocoding (1.0.5):
- Flutter
- location (0.0.1):
- Flutter
- maps_launcher (0.0.1):
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- receive_sharing_intent (0.0.1):
- Flutter
- SDWebImage (5.12.3):
- SDWebImage/Core (= 5.12.3)
- SDWebImage/Core (5.12.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- geocoding (from `.symlinks/plugins/geocoding/ios`)
- location (from `.symlinks/plugins/location/ios`)
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
geocoding:
:path: ".symlinks/plugins/geocoding/ios"
location:
:path: ".symlinks/plugins/location/ios"
maps_launcher:
:path: ".symlinks/plugins/maps_launcher/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
geocoding: 32cfcdb16d38d907caaba65e2e42ad10d38bee58
location: 3a2eed4dd2fab25e7b7baf2a9efefe82b512d740
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
SDWebImage: 53179a2dba77246efa8a9b85f5c5b21f8f43e38f
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.11.3

View File

@@ -0,0 +1,785 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
05B555C72796E0E100731D0C /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B555C62796E0E100731D0C /* ShareViewController.swift */; };
05B555CA2796E0E100731D0C /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 05B555C82796E0E100731D0C /* MainInterface.storyboard */; };
05B555CE2796E0E100731D0C /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 05B555C42796E0E100731D0C /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
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 */; };
FAFCFCF8207021C31CE2021E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30AF7E29CD9C08B4BA0A1C52 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
05B555CC2796E0E100731D0C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 05B555C32796E0E100731D0C;
remoteInfo = ShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
05B555CF2796E0E100731D0C /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
05B555CE2796E0E100731D0C /* ShareExtension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
05B555C42796E0E100731D0C /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
05B555C62796E0E100731D0C /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
05B555C92796E0E100731D0C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
05B555CB2796E0E100731D0C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
05B555D42796E21E00731D0C /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
05B555D52796E25F00731D0C /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
30AF7E29CD9C08B4BA0A1C52 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5147928FEB8FF70E5DCF0B91 /* 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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C142B296C6D81AB3420C4869 /* 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 = "<group>"; };
D67EF54705446F3A326E5778 /* 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 = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
05B555C12796E0E100731D0C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FAFCFCF8207021C31CE2021E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
05B555C52796E0E100731D0C /* ShareExtension */ = {
isa = PBXGroup;
children = (
05B555D52796E25F00731D0C /* ShareExtension.entitlements */,
05B555C62796E0E100731D0C /* ShareViewController.swift */,
05B555C82796E0E100731D0C /* MainInterface.storyboard */,
05B555CB2796E0E100731D0C /* Info.plist */,
);
path = ShareExtension;
sourceTree = "<group>";
};
67FFEEB1C00E19A4B34373A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
30AF7E29CD9C08B4BA0A1C52 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6BCC37388A6BAAA8424A31B1 /* Pods */ = {
isa = PBXGroup;
children = (
5147928FEB8FF70E5DCF0B91 /* Pods-Runner.debug.xcconfig */,
C142B296C6D81AB3420C4869 /* Pods-Runner.release.xcconfig */,
D67EF54705446F3A326E5778 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
05B555C52796E0E100731D0C /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */,
6BCC37388A6BAAA8424A31B1 /* Pods */,
67FFEEB1C00E19A4B34373A0 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
05B555C42796E0E100731D0C /* ShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
05B555D42796E21E00731D0C /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
05B555C32796E0E100731D0C /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 05B555D32796E0E100731D0C /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
05B555C02796E0E100731D0C /* Sources */,
05B555C12796E0E100731D0C /* Frameworks */,
05B555C22796E0E100731D0C /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ShareExtension;
productName = ShareExtension;
productReference = 05B555C42796E0E100731D0C /* ShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
F8ED8338B5331552C3B3682F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
090062C30368FBD0ED95CAB1 /* [CP] Embed Pods Frameworks */,
05B555CF2796E0E100731D0C /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
05B555CD2796E0E100731D0C /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
05B555C32796E0E100731D0C = {
CreatedOnToolsVersion = 13.2.1;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
05B555C32796E0E100731D0C /* ShareExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
05B555C22796E0E100731D0C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
05B555CA2796E0E100731D0C /* MainInterface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
090062C30368FBD0ED95CAB1 /* [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;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
F8ED8338B5331552C3B3682F /* [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;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
05B555C02796E0E100731D0C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
05B555C72796E0E100731D0C /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
05B555CD2796E0E100731D0C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 05B555C32796E0E100731D0C /* ShareExtension */;
targetProxy = 05B555CC2796E0E100731D0C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
05B555C82796E0E100731D0C /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
05B555C92796E0E100731D0C /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
05B555D02796E0E100731D0C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = H9XHQ4WHSF;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
05B555D12796E0E100731D0C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = H9XHQ4WHSF;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
05B555D22796E0E100731D0C /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = H9XHQ4WHSF;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = H9XHQ4WHSF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = H9XHQ4WHSF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = H9XHQ4WHSF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = de.seemoo.ios.openhaystack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
05B555D32796E0E100731D0C /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
05B555D02796E0E100731D0C /* Debug */,
05B555D12796E0E100731D0C /* Release */,
05B555D22796E0E100731D0C /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>OpenHaystack</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia</string>
</array>
</dict>
<dict/>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location is needed to show the users location (optional)</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.de.seemoo.ios.openhaystack</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.de.seemoo.ios.openhaystack</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,342 @@
//
// ShareViewController.swift
// ShareExtension
//
// Created by Max Granzow on 18.01.22.
//
import UIKit
import Social
import MobileCoreServices
import Photos
// Source: https://pub.dev/packages/receive_sharing_intent
class ShareViewController: SLComposeServiceViewController {
let hostAppBundleIdentifier = "de.seemoo.ios.openhaystack"
let sharedKey = "ShareKey"
var sharedMedia: [SharedMediaFile] = []
var sharedText: [String] = []
let imageContentType = kUTTypeImage as String
let videoContentType = kUTTypeMovie as String
let textContentType = kUTTypeText as String
let urlContentType = kUTTypeURL as String
let fileURLType = kUTTypeFileURL as String;
override func isContentValid() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad();
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
if let contents = content.attachments {
for (index, attachment) in (contents).enumerated() {
if attachment.hasItemConformingToTypeIdentifier(imageContentType) {
handleImages(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
handleFiles(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
handleText(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
handleUrl(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
handleVideos(content: content, attachment: attachment, index: index)
}
}
}
}
}
override func didSelectPost() {
print("didSelectPost");
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in
if error == nil, let item = data as? String, let this = self {
this.sharedText.append(item)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
}
} else {
self?.dismissWithError()
}
}
}
private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in
if error == nil, let item = data as? URL, let this = self {
this.sharedText.append(item.absoluteString)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
}
} else {
self?.dismissWithError()
}
}
}
private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .image)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
}
} else {
self?.dismissWithError()
}
}
}
private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .video)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
return
}
this.sharedMedia.append(sharedFile)
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
}
} else {
self?.dismissWithError()
}
}
}
private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from :url, type: .file)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if (copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
}
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .file)
}
} else {
self?.dismissWithError()
}
}
}
private func dismissWithError() {
print("[ERROR] Error loading data!")
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
let action = UIAlertAction(title: "Error", style: .cancel) { _ in
self.dismiss(animated: true, completion: nil)
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func redirectToHostApp(type: RedirectType) {
let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)")
var responder = self as UIResponder?
let selectorOpenURL = sel_registerName("openURL:")
while (responder != nil) {
if (responder?.responds(to: selectorOpenURL))! {
let _ = responder?.perform(selectorOpenURL, with: url)
}
responder = responder!.next
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
enum RedirectType {
case media
case text
case file
}
func getExtension(from url: URL, type: SharedMediaType) -> String {
let parts = url.lastPathComponent.components(separatedBy: ".")
var ex: String? = nil
if (parts.count > 1) {
ex = parts.last
}
if (ex == nil) {
switch type {
case .image:
ex = "PNG"
case .video:
ex = "MP4"
case .file:
ex = "TXT"
}
}
return ex ?? "Unknown"
}
func getFileName(from url: URL, type: SharedMediaType) -> String {
var name = url.lastPathComponent
if (name.isEmpty) {
name = UUID().uuidString + "." + getExtension(from: url, type: type)
}
return name
}
func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try FileManager.default.copyItem(at: srcURL, to: dstURL)
} catch (let error) {
print("Cannot copy item at \(srcURL) to \(dstURL): \(error)")
return false
}
return true
}
private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
let asset = AVAsset(url: forVideo)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: forVideo)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
}
var saved = false
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
// let scale = UIScreen.main.scale
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
do {
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
saved = true
} catch {
saved = false
}
return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
}
private func getThumbnailPath(for url: URL) -> URL {
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
let path = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.appendingPathComponent("\(fileName).jpg")
return path
}
class SharedMediaFile: Codable {
var path: String; // can be image, video or url path. It can also be text content
var thumbnail: String?; // video thumbnail
var duration: Double?; // video duration in milliseconds
var type: SharedMediaType;
init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
self.path = path
self.thumbnail = thumbnail
self.duration = duration
self.type = type
}
// Debug method to print out SharedMediaFile details in the console
func toString() {
print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)")
}
}
enum SharedMediaType: Int, Codable {
case image
case video
case file
}
func toData(data: [SharedMediaFile]) -> Data {
let encodedData = try? JSONEncoder().encode(data)
return encodedData!
}
}
extension Array {
subscript (safe index: UInt) -> Element? {
return Int(index) < count ? self[Int(index)] : nil
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
class AccessoryColorSelector extends StatelessWidget {
/// This shows a color selector.
///
/// The color can be selected via a color field or by inputing explicit
/// RGB values.
const AccessoryColorSelector({ Key? key }) : super(key: key);
/// Displays the color selector with the [initialColor] preselected.
///
/// The selected color is returned if the user selects the save option.
/// Otherwise the selection is discarded with a null return value.
static Future<Color?> showColorSelection(BuildContext context, Color initialColor) async {
Color currentColor = initialColor;
return await showDialog<Color>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Pick a color'),
content: SingleChildScrollView(
child: ColorPicker(
hexInputBar: true,
pickerColor: currentColor,
onColorChanged: (Color newColor) {
currentColor = newColor;
},
)
),
actions: <Widget>[
ElevatedButton(
child: const Text('Save'),
onPressed: () {
Navigator.pop(context, currentColor);
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:openhaystack_mobile/accessory/accessory_color_selector.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon_selector.dart';
import 'package:openhaystack_mobile/accessory/accessory_model.dart';
import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
import 'package:openhaystack_mobile/item_management/accessory_name_input.dart';
class AccessoryDetail extends StatefulWidget {
Accessory accessory;
/// A page displaying the editable information of a specific [accessory].
///
/// This shows the editable information of a specific [accessory] and
/// allows the user to edit them.
AccessoryDetail({
Key? key,
required this.accessory,
}) : super(key: key);
@override
_AccessoryDetailState createState() => _AccessoryDetailState();
}
class _AccessoryDetailState extends State<AccessoryDetail> {
// An accessory storing the changed values.
late Accessory newAccessory;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
// Initialize changed accessory with existing accessory properties.
newAccessory = widget.accessory.clone();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.accessory.name),
),
body: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
children: [
Center(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: AccessoryIcon(
size: 100,
icon: newAccessory.icon,
color: newAccessory.color,
),
),
Positioned(
bottom: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
decoration: const BoxDecoration(
color: Color.fromARGB(255, 200, 200, 200),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () async {
// Show icon selection
String? selectedIcon = await AccessoryIconSelector
.showIconSelection(context, newAccessory.rawIcon, newAccessory.color);
if (selectedIcon != null) {
setState(() {
newAccessory.setIcon(selectedIcon);
});
// Show color selection only when icon is selected
Color? selectedColor = await AccessoryColorSelector
.showColorSelection(context, newAccessory.color);
if (selectedColor != null) {
setState(() {
newAccessory.color = selectedColor;
});
}
}
},
icon: const Icon(Icons.edit),
),
),
),
),
],
),
),
AccessoryNameInput(
initialValue: newAccessory.name,
onChanged: (value) {
setState(() {
newAccessory.name = value;
});
},
),
SwitchListTile(
value: newAccessory.isActive,
title: const Text('Is Active'),
onChanged: (checked) {
setState(() {
newAccessory.isActive = checked;
});
},
),
SwitchListTile(
value: newAccessory.isDeployed,
title: const Text('Is Deployed'),
onChanged: (checked) {
setState(() {
newAccessory.isDeployed = checked;
});
},
),
ListTile(
title: OutlinedButton(
child: const Text('Save'),
onPressed: _formKey.currentState == null || !_formKey.currentState!.validate()
? null : () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
// Update accessory with changed values
var accessoryRegistry = Provider.of<AccessoryRegistry>(context, listen: false);
accessoryRegistry.editAccessory(widget.accessory, newAccessory);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Changes saved!'),
),
);
}
},
),
),
ListTile(
title: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return Theme.of(context).errorColor;
},
),
),
child: const Text('Delete Accessory', style: TextStyle(color: Colors.white),),
onPressed: () {
// Delete accessory
var accessoryRegistry = Provider.of<AccessoryRegistry>(context, listen: false);
accessoryRegistry.removeAccessory(widget.accessory);
Navigator.pop(context);
},
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,106 @@
/// This class is used for de-/serializing data to the JSON transfer format.
class AccessoryDTO {
int id;
List<double> colorComponents;
String name;
double? lastDerivationTimestamp;
String? symmetricKey;
int? updateInterval;
String privateKey;
String icon;
bool isDeployed;
String colorSpaceName;
bool usesDerivation;
String? oldestRelevantSymmetricKey;
bool isActive;
/// Creates a transfer object to serialize to the JSON export format.
///
/// This implements the [toJson] method used by the Dart JSON serializer.
/// ```dart
/// var accessoryDTO = AccessoryDTO(...);
/// jsonEncode(accessoryDTO);
/// ```
AccessoryDTO({
required this.id,
required this.colorComponents,
required this.name,
this.lastDerivationTimestamp,
this.symmetricKey,
this.updateInterval,
required this.privateKey,
required this.icon,
required this.isDeployed,
required this.colorSpaceName,
required this.usesDerivation,
this.oldestRelevantSymmetricKey,
required this.isActive,
});
/// Creates a transfer object from deserialized JSON data.
///
/// The data is only decoded and not processed further.
///
/// Typically used with JSON decoder.
/// ```dart
/// String json = '...';
/// var accessoryDTO = AccessoryDTO.fromJSON(jsonDecode(json));
/// ```
///
/// This implements the [toJson] method used by the Dart JSON serializer.
/// ```dart
/// var accessoryDTO = AccessoryDTO(...);
/// jsonEncode(accessoryDTO);
/// ```
AccessoryDTO.fromJson(Map<String, dynamic> json)
: id = json['id'],
colorComponents = List.from(json['colorComponents'])
.map((val) => double.parse(val.toString())).toList(),
name = json['name'],
lastDerivationTimestamp = json['lastDerivationTimestamp'] ?? 0,
symmetricKey = json['symmetricKey'] ?? '',
updateInterval = json['updateInterval'] ?? 0,
privateKey = json['privateKey'],
icon = json['icon'],
isDeployed = json['isDeployed'],
colorSpaceName = json['colorSpaceName'],
usesDerivation = json['usesDerivation'] ?? false,
oldestRelevantSymmetricKey = json['oldestRelevantSymmetricKey'] ?? '',
isActive = json['isActive'];
/// Creates a JSON map of the serialized transfer object.
///
/// Typically used by JSON encoder.
/// ```dart
/// var accessoryDTO = AccessoryDTO(...);
/// jsonEncode(accessoryDTO);
/// ```
Map<String, dynamic> toJson() => usesDerivation ? {
// With derivation
'id': id,
'colorComponents': colorComponents,
'name': name,
'lastDerivationTimestamp': lastDerivationTimestamp,
'symmetricKey': symmetricKey,
'updateInterval': updateInterval,
'privateKey': privateKey,
'icon': icon,
'isDeployed': isDeployed,
'colorSpaceName': colorSpaceName,
'usesDerivation': usesDerivation,
'oldestRelevantSymmetricKey': oldestRelevantSymmetricKey,
'isActive': isActive,
} : {
// Without derivation (skip rolling key params)
'id': id,
'colorComponents': colorComponents,
'name': name,
'privateKey': privateKey,
'icon': icon,
'isDeployed': isDeployed,
'colorSpaceName': colorSpaceName,
'usesDerivation': usesDerivation,
'isActive': isActive,
};
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class AccessoryIcon extends StatelessWidget {
/// The icon to display.
final IconData icon;
/// The color of the surrounding ring.
final Color color;
/// The size of the icon.
final double size;
/// Displays the icon in a colored ring.
///
/// The default size can be adjusted by setting the [size] parameter.
const AccessoryIcon({
Key? key,
this.icon = Icons.help,
this.color = Colors.grey,
this.size = 24,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.circle,
border: Border.all(width: size / 6, color: color),
),
child: Padding(
padding: EdgeInsets.all(size / 12),
child: Icon(
icon,
size: size,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class AccessoryIconModel {
/// A list of all available icons
static const List<String> icons = [
"creditcard.fill", "briefcase.fill", "case.fill", "latch.2.case.fill",
"key.fill", "mappin", "globe", "crown.fill",
"gift.fill", "car.fill", "bicycle", "figure.walk",
"heart.fill", "hare.fill", "tortoise.fill", "eye.fill",
];
/// A mapping from the cupertino icon names to the material icon names.
///
/// If the icons do not match, so a similar replacement is used.
static const iconMapping = {
'creditcard.fill': Icons.credit_card,
'briefcase.fill': Icons.business_center,
'case.fill': Icons.work,
'latch.2.case.fill': Icons.business_center,
'key.fill': Icons.vpn_key,
'mappin': Icons.place,
// 'pushpin': Icons.push_pin,
'globe': Icons.language,
'crown.fill': Icons.school,
'gift.fill': Icons.redeem,
'car.fill': Icons.directions_car,
'bicycle': Icons.pedal_bike,
'figure.walk': Icons.directions_walk,
'heart.fill': Icons.favorite,
'hare.fill': Icons.pets,
'tortoise.fill': Icons.bug_report,
'eye.fill': Icons.visibility,
};
/// Looks up the equivalent material icon for the cupertino icon [iconName].
static IconData? mapIcon(String iconName) {
return iconMapping[iconName];
}
}

View File

@@ -0,0 +1,76 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon_model.dart';
typedef IconChangeListener = void Function(String? newValue);
class AccessoryIconSelector extends StatelessWidget {
/// The existing icon used previously.
final String icon;
/// The existing color used previously.
final Color color;
/// A callback being called when the icon changes.
final IconChangeListener iconChanged;
/// This show an icon selector.
///
/// The icon can be selected from a list of available icons.
/// The icons are handled by the cupertino icon names.
const AccessoryIconSelector({
Key? key,
required this.icon,
required this.color,
required this.iconChanged,
}) : super(key: key);
/// Displays the icon selector with the [currentIcon] preselected in the [highlighColor].
///
/// The selected icon as a cupertino icon name is returned if the user selects an icon.
/// Otherwise the selection is discarded and a null value is returned.
static Future<String?> showIconSelection(BuildContext context, String currentIcon, Color highlighColor) async {
return await showDialog<String>(
context: context,
builder: (BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => Dialog(
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
shrinkWrap: true,
crossAxisCount: min((constraints.maxWidth / 80).floor(), 8),
semanticChildCount: AccessoryIconModel.icons.length,
children: AccessoryIconModel.icons
.map((value) => IconButton(
icon: Icon(AccessoryIconModel.mapIcon(value)),
color: value == currentIcon ? highlighColor : null,
onPressed: () { Navigator.pop(context, value); },
)).toList(),
),
),
);
}
);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(255, 200, 200, 200),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () async {
String? selectedIcon = await showIconSelection(context, icon, color);
if (selectedIcon != null) {
iconChanged(selectedIcon);
}
},
icon: Icon(AccessoryIconModel.mapIcon(icon)),
),
);
}
}

View File

@@ -0,0 +1,152 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:maps_launcher/maps_launcher.dart';
import 'package:provider/provider.dart';
import 'package:latlong2/latlong.dart';
import 'package:openhaystack_mobile/accessory/accessory_list_item.dart';
import 'package:openhaystack_mobile/accessory/accessory_list_item_placeholder.dart';
import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
import 'package:openhaystack_mobile/accessory/no_accessories.dart';
import 'package:openhaystack_mobile/history/accessory_history.dart';
import 'package:openhaystack_mobile/location/location_model.dart';
class AccessoryList extends StatefulWidget {
final AsyncCallback loadLocationUpdates;
final void Function(LatLng point)? centerOnPoint;
/// Display a location overview all accessories in a concise list form.
///
/// For each accessory the name and last known locaiton information is shown.
/// Uses the accessories in the [AccessoryRegistry].
const AccessoryList({
Key? key,
required this.loadLocationUpdates,
this.centerOnPoint,
}): super(key: key);
@override
_AccessoryListState createState() => _AccessoryListState();
}
class _AccessoryListState extends State<AccessoryList> {
@override
Widget build(BuildContext context) {
return Consumer2<AccessoryRegistry, LocationModel>(
builder: (context, accessoryRegistry, locationModel, child) {
var accessories = accessoryRegistry.accessories;
// Show placeholder while accessories are loading
if (accessoryRegistry.loading){
return LayoutBuilder(
builder: (context, constraints) {
// Show as many accessory placeholder fitting into the vertical space.
// Minimum one, maximum 6 placeholders
var nrOfEntries = min(max((constraints.maxHeight / 64).floor(), 1), 6);
List<Widget> placeholderList = [];
for (int i = 0; i < nrOfEntries; i++) {
placeholderList.add(const AccessoryListItemPlaceholder());
}
return Scrollbar(
child: ListView(
children: placeholderList,
),
);
}
);
}
if (accessories.isEmpty) {
return const NoAccessoriesPlaceholder();
}
// TODO: Refresh Indicator for desktop
// Use pull to refresh method
return SlidableAutoCloseBehavior(child:
RefreshIndicator(
onRefresh: widget.loadLocationUpdates,
child: Scrollbar(
child: ListView(
children: accessories.map((accessory) {
// Calculate distance from users devices location
Widget? trailing;
if (locationModel.here != null && accessory.lastLocation != null) {
const Distance distance = Distance();
final double km = distance.as(LengthUnit.Kilometer, locationModel.here!, accessory.lastLocation!);
trailing = Text(km.toString() + 'km');
}
// Get human readable location
return Slidable(
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
if (accessory.isDeployed) SlidableAction(
onPressed: (context) async {
if (accessory.lastLocation != null && accessory.isDeployed) {
var loc = accessory.lastLocation!;
await MapsLauncher.launchCoordinates(
loc.latitude, loc.longitude, accessory.name);
}
},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.directions,
label: 'Navigate',
),
if (accessory.isDeployed) SlidableAction(
onPressed: (context) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AccessoryHistory(
accessory: accessory,
)),
);
},
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.history,
label: 'History',
),
if (!accessory.isDeployed) SlidableAction(
onPressed: (context) {
var accessoryRegistry = Provider.of<AccessoryRegistry>(context, listen: false);
var newAccessory = accessory.clone();
newAccessory.isDeployed = true;
accessoryRegistry.editAccessory(accessory, newAccessory);
},
backgroundColor: Colors.green,
foregroundColor: Colors.white,
icon: Icons.upload_file,
label: 'Deploy',
),
],
),
child: Builder(
builder: (context) {
return AccessoryListItem(
accessory: accessory,
distance: trailing,
herePlace: locationModel.herePlace,
onTap: () {
var lastLocation = accessory.lastLocation;
if (lastLocation != null) {
widget.centerOnPoint?.call(lastLocation);
}
},
onLongPress: Slidable.of(context)?.openEndActionPane,
);
}
),
);
}).toList(),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon.dart';
import 'package:openhaystack_mobile/accessory/accessory_model.dart';
import 'package:intl/intl.dart';
class AccessoryListItem extends StatelessWidget {
/// The accessory to display the information for.
final Accessory accessory;
/// A trailing distance information widget.
final Widget? distance;
/// Address information about the accessories location.
final Placemark? herePlace;
final VoidCallback onTap;
final VoidCallback? onLongPress;
/// Displays the location of an accessory as a concise list item.
///
/// Shows the icon and name of the accessory, as well as the current
/// location and distance to the user's location (if known; `distance != null`)
const AccessoryListItem({
Key? key,
required this.accessory,
required this.onTap,
this.onLongPress,
this.distance,
this.herePlace,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder<Placemark?>(
future: accessory.place,
builder: (BuildContext context, AsyncSnapshot<Placemark?> snapshot) {
// Format the location of the accessory. Use in this order:
// * Address if known
// * Coordinates (latitude & longitude) if known
// * `Unknown` if unknown
String locationString = accessory.lastLocation != null
? '${accessory.lastLocation!.latitude}, ${accessory.lastLocation!.longitude}'
: 'Unknown';
if (snapshot.hasData && snapshot.data != null) {
Placemark place = snapshot.data!;
locationString = '${place.locality}, ${place.administrativeArea}';
if (herePlace != null && herePlace!.country != place.country) {
locationString = '${place.locality}, ${place.country}';
}
}
// Format published date in a human readable way
String? dateString = accessory.datePublished != null
? ' · ${DateFormat('dd.MM.yyyy kk:mm').format(accessory.datePublished!)}'
: '';
return ListTile(
onTap: onTap,
onLongPress: onLongPress,
title: Text(
accessory.name + (accessory.isDeployed ? '' : ' (not deployed)'),
style: TextStyle(
color: accessory.isDeployed
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).disabledColor,
),
),
subtitle: Text(locationString + dateString),
trailing: distance,
dense: true,
leading: AccessoryIcon(
icon: accessory.icon,
color: accessory.color,
),
);
},
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/accessory/accessory_list_item.dart';
import 'package:openhaystack_mobile/placeholder/avatar_placeholder.dart';
import 'package:openhaystack_mobile/placeholder/text_placeholder.dart';
class AccessoryListItemPlaceholder extends StatelessWidget {
/// A placeholder for an [AccessoryListItem] showing a loading animation.
const AccessoryListItemPlaceholder({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Uses a similar layout to the actual accessory list item
return const ListTile(
title: TextPlaceholder(),
subtitle: TextPlaceholder(),
dense: true,
leading: AvatarPlaceholder(),
trailing: TextPlaceholder(width: 60),
);
}
}

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:latlong2/latlong.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon_model.dart';
import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
import 'package:openhaystack_mobile/location/location_model.dart';
class Pair<T1, T2> {
final T1 a;
final T2 b;
Pair(this.a, this.b);
}
const defaultIcon = Icons.push_pin;
class Accessory {
/// The ID of the accessory key.
String id;
/// A hash of the public key.
/// An identifier for the private key stored separately in the key store.
String hashedPublicKey;
/// If the accessory uses rolling keys.
bool usesDerivation;
// Parameters for rolling keys (only relevant is usesDerivation == true)
String? symmetricKey;
double? lastDerivationTimestamp;
int? updateInterval;
String? oldestRelevantSymmetricKey;
/// The display name of the accessory.
String name;
/// The display icon of the accessory.
String _icon;
/// The display color of the accessory.
Color color;
/// If the accessory is active.
bool isActive;
/// If the accessory is already deployed
/// (and could therefore send locations).
bool isDeployed;
/// The timestamp of the last known location
/// (null if no location known).
DateTime? datePublished;
/// The last known locations coordinates
/// (null if no location known).
LatLng? _lastLocation;
/// A list of known locations over time.
List<Pair<LatLng, DateTime>> locationHistory = [];
/// Stores address information about the current location.
Future<Placemark?> place = Future.value(null);
/// Creates an accessory with the given properties.
Accessory({
required this.id,
required this.name,
required this.hashedPublicKey,
required this.datePublished,
this.isActive = false,
this.isDeployed = false,
LatLng? lastLocation,
String icon = 'mappin',
this.color = Colors.grey,
this.usesDerivation = false,
this.symmetricKey,
this.lastDerivationTimestamp,
this.updateInterval,
this.oldestRelevantSymmetricKey,
}): _icon = icon, _lastLocation = lastLocation, super() {
_init();
}
void _init() {
if (_lastLocation != null) {
place = LocationModel.getAddress(_lastLocation!);
}
}
/// Creates a new accessory with exactly the same properties of this accessory.
Accessory clone() {
return Accessory(
datePublished: datePublished,
id: id,
name: name,
hashedPublicKey: hashedPublicKey,
color: color,
icon: _icon,
isActive: isActive,
isDeployed: isDeployed,
lastLocation: lastLocation,
usesDerivation: usesDerivation,
symmetricKey: symmetricKey,
lastDerivationTimestamp: lastDerivationTimestamp,
updateInterval: updateInterval,
oldestRelevantSymmetricKey: oldestRelevantSymmetricKey,
);
}
/// Updates the properties of this accessor with the new values of the [newAccessory].
void update(Accessory newAccessory) {
datePublished = newAccessory.datePublished;
id = newAccessory.id;
name = newAccessory.name;
hashedPublicKey = newAccessory.hashedPublicKey;
color = newAccessory.color;
_icon = newAccessory._icon;
isActive = newAccessory.isActive;
isDeployed = newAccessory.isDeployed;
lastLocation = newAccessory.lastLocation;
}
/// The last known location of the accessory.
LatLng? get lastLocation {
return _lastLocation;
}
/// The last known location of the accessory.
set lastLocation(LatLng? newLocation) {
_lastLocation = newLocation;
if (_lastLocation != null) {
place = LocationModel.getAddress(_lastLocation!);
}
}
/// The display icon of the accessory.
IconData get icon {
IconData? icon = AccessoryIconModel.mapIcon(_icon);
return icon ?? defaultIcon;
}
/// The cupertino icon name.
String get rawIcon {
return _icon;
}
/// The display icon of the accessory.
setIcon (String icon) {
_icon = icon;
}
/// Creates an accessory from deserialized JSON data.
///
/// Uses the same format as in [toJson]
///
/// Typically used with JSON decoder.
/// ```dart
/// String json = '...';
/// var accessoryDTO = Accessory.fromJSON(jsonDecode(json));
/// ```
Accessory.fromJson(Map<String, dynamic> json)
: id = json['id'],
name = json['name'],
hashedPublicKey = json['hashedPublicKey'],
datePublished = json['datePublished'] != null
? DateTime.fromMillisecondsSinceEpoch(json['datePublished']) : null,
_lastLocation = json['latitude'] != null && json['longitude'] != null
? LatLng(json['latitude'].toDouble(), json['longitude'].toDouble()) : null,
isActive = json['isActive'],
isDeployed = json['isDeployed'],
_icon = json['icon'],
color = Color(int.parse(json['color'], radix: 16)),
usesDerivation = json['usesDerivation'] ?? false,
symmetricKey = json['symmetricKey'],
lastDerivationTimestamp = json['lastDerivationTimestamp'],
updateInterval = json['updateInterval'],
oldestRelevantSymmetricKey = json['oldestRelevantSymmetricKey'] {
_init();
}
/// Creates a JSON map of the serialized accessory.
///
/// Uses the same format as in [Accessory.fromJson].
///
/// Typically used by JSON encoder.
/// ```dart
/// var accessory = Accessory(...);
/// jsonEncode(accessory);
/// ```
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'hashedPublicKey': hashedPublicKey,
'datePublished': datePublished?.millisecondsSinceEpoch,
'latitude': _lastLocation?.latitude,
'longitude': _lastLocation?.longitude,
'isActive': isActive,
'isDeployed': isDeployed,
'icon': _icon,
'color': color.toString().split('(0x')[1].split(')')[0],
'usesDerivation': usesDerivation,
'symmetricKey': symmetricKey,
'lastDerivationTimestamp': lastDerivationTimestamp,
'updateInterval': updateInterval,
'oldestRelevantSymmetricKey': oldestRelevantSymmetricKey,
};
/// Returns the Base64 encoded hash of the advertisement key
/// (used to fetch location reports).
Future<String> getHashedAdvertisementKey() async {
var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
return keyPair.getHashedAdvertisementKey();
}
/// Returns the Base64 encoded advertisement key
/// (sent out by the accessory via BLE).
Future<String> getAdvertisementKey() async {
var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
return keyPair.getBase64AdvertisementKey();
}
/// Returns the Base64 encoded private key.
Future<String> getPrivateKey() async {
var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
return keyPair.getBase64PrivateKey();
}
}

View File

@@ -0,0 +1,155 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:openhaystack_mobile/accessory/accessory_model.dart';
import 'package:latlong2/latlong.dart';
import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
import 'package:openhaystack_mobile/findMy/models.dart';
const accessoryStorageKey = 'ACCESSORIES';
class AccessoryRegistry extends ChangeNotifier {
final _storage = const FlutterSecureStorage();
final _findMyController = FindMyController();
List<Accessory> _accessories = [];
bool loading = false;
bool initialLoadFinished = false;
/// Creates the accessory registry.
///
/// This is used to manage the accessories of the user.
AccessoryRegistry() : super();
/// A list of the user's accessories.
UnmodifiableListView<Accessory> get accessories => UnmodifiableListView(_accessories);
/// Loads the user's accessories from persistent storage.
Future<void> loadAccessories() async {
loading = true;
String? serialized = await _storage.read(key: accessoryStorageKey);
if (serialized != null) {
List accessoryJson = json.decode(serialized);
List<Accessory> loadedAccessories =
accessoryJson.map((val) => Accessory.fromJson(val)).toList();
_accessories = loadedAccessories;
} else {
_accessories = [];
}
// For Debugging:
// await overwriteEverythingWithDemoDataForDebugging();
loading = false;
notifyListeners();
}
/// __USE ONLY FOR DEBUGGING PURPOSES__
///
/// __ALL PERSISTENT DATA WILL BE LOST!__
///
/// Overwrites all accessories in this registry with demo data for testing.
Future<void> overwriteEverythingWithDemoDataForDebugging() async {
// Delete everything to start with a fresh set of demo accessories
await _storage.deleteAll();
// Load demo accessories
List<Accessory> demoAccessories = [
Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
id: '-5952179461995674635', name: 'Raspberry Pi', color: Colors.green,
datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
icon: 'gift.fill', lastLocation: LatLng(49.874739, 8.656280)),
Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
id: '-5952179461995674635', name: 'My Bag', color: Colors.blue,
datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
icon: 'case.fill', lastLocation: LatLng(49.874739, 8.656280)),
Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
id: '-5952179461995674635', name: 'Car', color: Colors.red,
datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
icon: 'car.fill', lastLocation: LatLng(49.874739, 8.656280)),
];
_accessories = demoAccessories;
// Store demo accessories for later use
await _storeAccessories();
// Import private key for demo accessories
// Public key hash is TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=
await FindMyController.importKeyPair('siykvOCIEQRVDwrbjyZUXuBwsMi0Htm7IBmBIg==');
}
/// Fetches new location reports and matches them to their accessory.
Future<void> loadLocationReports() async {
List<Future<List<FindMyLocationReport>>> runningLocationRequests = [];
// request location updates for all accessories simultaneously
List<Accessory> currentAccessories = accessories;
for (var i = 0; i < currentAccessories.length; i++) {
var accessory = currentAccessories.elementAt(i);
var keyPair = await FindMyController.getKeyPair(accessory.hashedPublicKey);
var locationRequest = FindMyController.computeResults(keyPair);
runningLocationRequests.add(locationRequest);
}
// wait for location updates to succeed and update state afterwards
var reportsForAccessories = await Future.wait(runningLocationRequests);
for (var i = 0; i < currentAccessories.length; i++) {
var accessory = currentAccessories.elementAt(i);
var reports = reportsForAccessories.elementAt(i);
print("Found ${reports.length} reports for accessory '${accessory.name}'");
accessory.locationHistory = reports
.where((report) => report.latitude.abs() <= 90 && report.longitude.abs() < 90 )
.map((report) => Pair<LatLng, DateTime>(
LatLng(report.latitude, report.longitude),
report.timestamp ?? report.published,
))
.toList();
if (reports.isNotEmpty) {
var lastReport = reports.first;
accessory.lastLocation = LatLng(lastReport.latitude, lastReport.longitude);
accessory.datePublished = lastReport.timestamp ?? lastReport.published;
}
}
// Store updated lastLocation and datePublished for accessories
_storeAccessories();
initialLoadFinished = true;
notifyListeners();
}
/// Stores the user's accessories in persistent storage.
Future<void> _storeAccessories() async {
List jsonList = _accessories.map(jsonEncode).toList();
await _storage.write(key: accessoryStorageKey, value: jsonList.toString());
}
/// Adds a new accessory to this registry.
void addAccessory(Accessory accessory) {
_accessories.add(accessory);
_storeAccessories();
notifyListeners();
}
/// Removes [accessory] from this registry.
void removeAccessory(Accessory accessory) {
_accessories.remove(accessory);
// TODO: remove private key from keychain
_storeAccessories();
notifyListeners();
}
/// Updates [oldAccessory] with the values from [newAccessory].
void editAccessory(Accessory oldAccessory, Accessory newAccessory) {
oldAccessory.update(newAccessory);
_storeAccessories();
notifyListeners();
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/item_management/new_item_action.dart';
class NoAccessoriesPlaceholder extends StatelessWidget {
/// Displays a message that no accessories are present.
///
/// Allows the user to quickly add a new accessory.
const NoAccessoriesPlaceholder({ Key? key }) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
'There\'s Nothing Here Yet\nAdd an accessory to get started.',
style: TextStyle(
fontSize: 20,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
NewKeyAction(mini: true),
],
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:provider/provider.dart';
import 'package:openhaystack_mobile/accessory/accessory_list.dart';
import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
import 'package:openhaystack_mobile/location/location_model.dart';
import 'package:openhaystack_mobile/map/map.dart';
import 'package:latlong2/latlong.dart';
class AccessoryMapListVertical extends StatefulWidget {
final AsyncCallback loadLocationUpdates;
/// Displays a map view and the accessory list in a vertical alignment.
const AccessoryMapListVertical({
Key? key,
required this.loadLocationUpdates,
}) : super(key: key);
@override
State<AccessoryMapListVertical> createState() => _AccessoryMapListVerticalState();
}
class _AccessoryMapListVerticalState extends State<AccessoryMapListVertical> {
final MapController _mapController = MapController();
void _centerPoint(LatLng point) {
_mapController.fitBounds(
LatLngBounds(point),
);
}
@override
Widget build(BuildContext context) {
return Consumer2<AccessoryRegistry, LocationModel>(
builder: (BuildContext context, AccessoryRegistry accessoryRegistry, LocationModel locationModel, Widget? child) {
return Column(
children: [
Flexible(
fit: FlexFit.tight,
child: AccessoryMap(
mapController: _mapController,
),
),
Flexible(
fit: FlexFit.tight,
child: AccessoryList(
loadLocationUpdates: widget.loadLocationUpdates,
centerOnPoint: _centerPoint,
),
),
],
);
},
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:openhaystack_mobile/accessory/accessory_list.dart';
import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
import 'package:openhaystack_mobile/location/location_model.dart';
import 'package:openhaystack_mobile/map/map.dart';
import 'package:openhaystack_mobile/preferences/preferences_page.dart';
import 'package:openhaystack_mobile/preferences/user_preferences_model.dart';
class DashboardDesktop extends StatefulWidget {
/// Displays the layout for the desktop view of the app.
///
/// The layout is optimized for horizontally aligned larger screens
/// on desktop devices.
const DashboardDesktop({ Key? key }) : super(key: key);
@override
_DashboardDesktopState createState() => _DashboardDesktopState();
}
class _DashboardDesktopState extends State<DashboardDesktop> {
@override
void initState() {
super.initState();
// Initialize models and preferences
var userPreferences = Provider.of<UserPreferences>(context, listen: false);
var locationModel = Provider.of<LocationModel>(context, listen: false);
var locationPreferenceKnown = userPreferences.locationPreferenceKnown ?? false;
var locationAccessWanted = userPreferences.locationAccessWanted ?? false;
if (!locationPreferenceKnown || locationAccessWanted) {
locationModel.requestLocationUpdates();
}
loadLocationUpdates();
}
/// Fetch locaiton updates for all accessories.
Future<void> loadLocationUpdates() async {
var accessoryRegistry = Provider.of<AccessoryRegistry>(context, listen: false);
await accessoryRegistry.loadLocationReports();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SizedBox(
width: 400,
child: Column(
children: [
AppBar(
title: const Text('OpenHaystack'),
leading: IconButton(
onPressed: () { /* reload */ },
icon: const Icon(Icons.menu),
),
actions: <Widget>[
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PreferencesPage()),
);
},
icon: const Icon(Icons.settings),
),
],
),
const Padding(
padding: EdgeInsets.all(5),
child: Text('My Accessories')
),
Expanded(
child: AccessoryList(
loadLocationUpdates: loadLocationUpdates,
),
),
],
),
),
const Expanded(
child: AccessoryMap(),
),
],
),
);
}
}

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
import 'package:openhaystack_mobile/dashboard/accessory_map_list_vert.dart';
import 'package:openhaystack_mobile/item_management/item_management.dart';
import 'package:openhaystack_mobile/item_management/new_item_action.dart';
import 'package:openhaystack_mobile/location/location_model.dart';
import 'package:openhaystack_mobile/preferences/preferences_page.dart';
import 'package:openhaystack_mobile/preferences/user_preferences_model.dart';
class DashboardMobile extends StatefulWidget {
/// Displays the layout for the mobile view of the app.
///
/// The layout is optimized for a vertically aligned small screens.
/// The functionality is structured in a bottom tab bar for easy access
/// on mobile devices.
const DashboardMobile({ Key? key }) : super(key: key);
@override
_DashboardMobileState createState() => _DashboardMobileState();
}
class _DashboardMobileState extends State<DashboardMobile> {
/// A list of the tabs displayed in the bottom tab bar.
late final List<Map<String, dynamic>> _tabs = [
{
'title': 'My Accessories',
'body': (ctx) => AccessoryMapListVertical(
loadLocationUpdates: loadLocationUpdates,
),
'icon': Icons.place,
'label': 'Map',
},
{
'title': 'My Accessories',
'body': (ctx) => const KeyManagement(),
'icon': Icons.style,
'label': 'Accessories',
'actionButton': (ctx) => const NewKeyAction(),
},
];
@override
void initState() {
super.initState();
// Initialize models and preferences
var userPreferences = Provider.of<UserPreferences>(context, listen: false);
var locationModel = Provider.of<LocationModel>(context, listen: false);
var locationPreferenceKnown = userPreferences.locationPreferenceKnown ?? false;
var locationAccessWanted = userPreferences.locationAccessWanted ?? false;
if (!locationPreferenceKnown || locationAccessWanted) {
locationModel.requestLocationUpdates();
}
// Load new location reports on app start
loadLocationUpdates();
}
/// Fetch locaiton updates for all accessories.
Future<void> loadLocationUpdates() async {
var accessoryRegistry = Provider.of<AccessoryRegistry>(context, listen: false);
try {
await accessoryRegistry.loadLocationReports();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).colorScheme.error,
content: Text(
'Could not find location reports. Try again later.',
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
),
),
),
);
}
}
/// The selected tab index.
int _selectedIndex = 0;
/// Updates the currently displayed tab to [index].
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Accessories'),
actions: <Widget>[
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PreferencesPage()),
);
},
icon: const Icon(Icons.settings),
),
],
),
body: _tabs[_selectedIndex]['body'](context),
bottomNavigationBar: BottomNavigationBar(
items: _tabs.map((tab) => BottomNavigationBarItem(
icon: Icon(tab['icon']),
label: tab['label'],
)).toList(),
currentIndex: _selectedIndex,
selectedItemColor: Theme.of(context).indicatorColor,
onTap: _onItemTapped,
),
floatingActionButton: _tabs[_selectedIndex]['actionButton']?.call(context),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CodeBlock extends StatelessWidget {
String text;
/// Displays a code block that can easily copied by the user.
CodeBlock({
Key? key,
required this.text,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Stack(
children: [
Container(
width: double.infinity,
constraints: const BoxConstraints(minHeight: 50),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10)),
color: Theme.of(context).colorScheme.background,
),
padding: const EdgeInsets.all(5),
child: SelectableText(text),
),
Positioned(
top: 0,
right: 5,
child: OutlinedButton(
child: const Text('Copy'),
onPressed: () {
Clipboard.setData(ClipboardData(text: text));
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
class DeploymentDetails extends StatefulWidget {
/// The steps required to deploy on this target.
List<Step> steps;
/// The name of the deployment target.
String title;
/// Describes a generic step-by-step deployment for a special hardware target.
///
/// The actual steps depend on the target platform and are provided in [steps].
DeploymentDetails({
Key? key,
required this.title,
required this.steps,
}) : super(key: key);
@override
_DeploymentDetailsState createState() => _DeploymentDetailsState();
}
class _DeploymentDetailsState extends State<DeploymentDetails> {
/// The index of the currently displayed step.
int _index = 0;
@override
Widget build(BuildContext context) {
var stepCount = widget.steps.length;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SafeArea(
child: Stepper(
currentStep: _index,
controlsBuilder: (BuildContext context, ControlsDetails details) {
String continueText = _index < stepCount - 1 ? 'CONTINUE' : 'FINISH';
return Row(
children: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(1))),
onPressed: details.onStepContinue,
child: Text(continueText),
),
if (_index > 0) TextButton(
onPressed: details.onStepCancel,
child: const Text('BACK'),
),
],
);
},
onStepCancel: () {
// Back button clicked
if (_index == 0) {
// Cancel deployment and return
Navigator.pop(context);
}
else if (_index > 0) {
setState(() {
_index -= 1;
});
}
},
onStepContinue: () {
// Continue button clicked
if (_index == stepCount - 1) {
// TODO: Mark accessory as deployed
// Deployment finished
Navigator.pop(context);
Navigator.pop(context);
} else {
setState(() {
_index += 1;
});
}
},
onStepTapped: (int index) {
setState(() {
_index = index;
});
},
steps: widget.steps,
),
),
);
}
}

View File

@@ -0,0 +1,95 @@
class DeploymentEmail {
static const _mailtoLink =
'mailto:?subject=Open%20Haystack%20Deplyoment%20Instructions&body=';
static const _welcomeMessage = 'OpenHaystack Deployment Guide\n\n'
'This is the deployment guide for your recently created OpenHaystack accessory. '
'The next step is to deploy the generated cryptographic key to a compatible '
'Bluetooth device.\n\n';
static const _finishedMessage =
'\n\nThe device now sends out Bluetooth advertisements. '
'It can take up to an hour for the location updates to appear in the app.\n';
static String getMicrobitDeploymentEmail(String advertisementKey) {
String mailContent = 'nRF51822 Deployment:\n\n'
'Requirements\n'
'To build the firmware the GNU Arm Embedded Toolchain is required.\n\n'
'Download\n'
'Download the firmware source code from GitHub and navigate to the '
'given folder.\n'
'https://github.com/seemoo-lab/openhaystack\n'
'git clone https://github.com/seemoo-lab/openhaystack.git && '
'cd openhaystack/Firmware/Microbit_v1\n\n'
'Build\n'
'Replace the public_key in main.c (initially '
'OFFLINEFINEINGPUBLICKEYHERE!) with the actual advertisement key. '
'Then execute make to create the firmware. You can export your '
'advertisement key directly from the OpenHaystack app.\n'
'static char public_key[28] = $advertisementKey;\n'
'make\n\n'
'Firmware Deployment\n'
'If the firmware is built successfully it can be deployed to the '
'microcontroller with the following command. (Please fill in the '
'volume of your microcontroller) \n'
'make install DEPLOY_PATH=/Volumes/MICROBIT';
return _mailtoLink +
Uri.encodeComponent(_welcomeMessage) +
Uri.encodeComponent(mailContent) +
Uri.encodeComponent(_finishedMessage);
}
static String getESP32DeploymentEmail(String advertisementKey) {
String mailContent = 'Espressif ESP32 Deployment: \n\n'
'Requirements\n'
'To build the firmware for the ESP32 Espressif\'s IoT Development '
'Framework (ESP-IDF) is required. Additionally Python 3 and the venv '
'module need to be installed.\n\n'
'Download\n'
'Download the firmware source code from GitHub and navigate to the '
'given folder.\n'
'https://github.com/seemoo-lab/openhaystack\n'
'git clone https://github.com/seemoo-lab/openhaystack.git '
'&& cd openhaystack/Firmware/ESP32\n\n'
'Build\n'
'Execute the ESP-IDF build command to create the ESP32 firmware.\n'
'idf.py build\n\n'
'Firmware Deployment\n'
'If the firmware is built successfully it can be flashed onto the '
'ESP32. This action is performed by the flash_esp32.sh script that '
'is provided with the advertisement key of the newly created accessory.\n'
'Please fill in the serial port of your microcontroller.\n'
'You can export your advertisement key directly from the '
'OpenHaystack app.\n'
'./flash_esp32.sh -p /dev/yourSerialPort $advertisementKey';
return _mailtoLink +
Uri.encodeComponent(_welcomeMessage) +
Uri.encodeComponent(mailContent) +
Uri.encodeComponent(_finishedMessage);
}
static String getLinuxHCIDeploymentEmail(String advertisementKey) {
String mailContent = 'Linux HCI Deployment:\n\n'
'Requirements\n'
'Install the hcitool software on a Bluetooth Low Energy Linux device, '
'for example a Raspberry Pi. Additionally Pyhton 3 needs to be '
'installed.\n\n'
'Download\n'
'Next download the python script that configures the HCI tool to '
'send out BLE advertisements.\n'
'https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py\n'
'curl -o HCI.py https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py\n\n'
'Usage\n'
'To start the BLE advertisements run the script.\n'
'You can export your advertisement key directly from the '
'OpenHaystack app.\n'
'sudo python3 HCI.py --key $advertisementKey';
return _mailtoLink +
Uri.encodeComponent(_welcomeMessage) +
Uri.encodeComponent(mailContent) +
Uri.encodeComponent(_finishedMessage);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/deployment/code_block.dart';
import 'package:openhaystack_mobile/deployment/deployment_details.dart';
import 'package:openhaystack_mobile/deployment/hyperlink.dart';
class DeploymentInstructionsESP32 extends StatelessWidget {
String advertisementKey;
/// Displays a deployment guide for the ESP32 platform.
DeploymentInstructionsESP32({
Key? key,
this.advertisementKey = '<ADVERTISEMENT_KEY>',
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DeploymentDetails(
title: 'ESP32 Deployment',
steps: [
const Step(
title: Text('Requirements'),
content: Text('To build the firmware for the ESP32 Espressif\'s '
'IoT Development Framework (ESP-IDF) is required. Additionally '
'Python 3 and the venv module need to be installed.'),
),
Step(
title: const Text('Download'),
content: Column(
children: [
const Text('Download the firmware source code from GitHub '
'and navigate to the given folder.'),
Hyperlink(target: 'https://github.com/seemoo-lab/openhaystack'),
CodeBlock(text: 'git clone https://github.com/seemoo-lab/openhaystack.git && cd openhaystack/Firmware/ESP32'),
],
),
),
Step(
title: const Text('Build'),
content: Column(
children: [
const Text('Execute the ESP-IDF build command to create the ESP32 firmware.'),
CodeBlock(text: 'idf.py build'),
],
),
),
Step(
title: const Text('Firmware Deployment'),
content: Column(
children: [
const Text('If the firmware is built successfully it can '
'be flashed onto the ESP32. This action is performed by '
'the flash_esp32.sh script that is provided with the '
'advertisement key of the newly created accessory.'),
const Text(
'Please fill in the serial port of your microcontroller.',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
CodeBlock(text: './flash_esp32.sh -p /dev/yourSerialPort "$advertisementKey"'),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/deployment/deployment_email.dart';
import 'package:openhaystack_mobile/deployment/deployment_esp32.dart';
import 'package:openhaystack_mobile/deployment/deployment_linux_hci.dart';
import 'package:openhaystack_mobile/deployment/deployment_nrf51.dart';
import 'package:openhaystack_mobile/deployment/hyperlink.dart';
import 'package:url_launcher/url_launcher.dart';
class DeploymentInstructions extends StatefulWidget {
String advertisementKey;
/// Displays deployment instructions for an already created accessory.
///
/// Provides general information about the created accessory and deployment.
/// Deployment guides for special hardware can be accessed separately.
///
/// The deployment instructions are customized with the [advertisementKey].
DeploymentInstructions({
Key? key,
this.advertisementKey = '<ADVERTISEMENT_KEY>',
}) : super(key: key);
@override
_DeploymentInstructionsState createState() => _DeploymentInstructionsState();
}
class _DeploymentInstructionsState extends State<DeploymentInstructions> {
final List<bool> _expanded = [false, false, false];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('How to Deploy'),
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
ListTile(
title: RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Congratulations, you successfully created '
'your accessory!\nThe next step is to deploy the generated '
'key to a Bluetooth device. OpenHaystack currently '
'supports three different deployment targets:\n'
'Nordic nRF51, Espressif ESP32 and the generic Linux HCI '
'platform.\nAdditional information about the deployment '
'can be found on ',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
),
),
TextSpan(
text: 'GitHub',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
fontSize: 18,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launch(
'https://github.com/seemoo-lab/openhaystack/');
},
),
const TextSpan(
text: '.',
style: TextStyle(color: Colors.black, fontSize: 18),
),
],
),
),
),
ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_expanded[index] = !isExpanded;
});
},
children: [
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const ListTile(
title: Text('Nordic vRF51'),
);
},
body: Column(
children: <Widget>[
const ListTile(
title: Text(
'For this firmware you need a nFR51822 platform '
'microcontroller. The provided firmware will send out '
'the created key so it can be found by Apple\'s Find My '
'network.'),
),
ListTile(
title: Hyperlink(
text: 'See deployment guide on GitHub',
target:
'https://github.com/seemoo-lab/openhaystack/tree/main/Firmware/Microbit_v1',
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
child: const Text('Send per mail'),
onPressed: () async {
await launch(
DeploymentEmail.getMicrobitDeploymentEmail(
widget.advertisementKey));
},
),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DeploymentInstructionsNRF51(
advertisementKey:
widget.advertisementKey,
)),
);
},
),
],
),
],
),
isExpanded: _expanded[0],
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const ListTile(
title: Text('Espressif ESP32'),
);
},
body: Column(
children: <Widget>[
const ListTile(
title: Text(
'For this firmware you need an ESP32 platform '
'microcontroller. The provided firmware will send out '
'the created key so it can be found by Apple\'s Find My '
'network.'),
),
ListTile(
title: Hyperlink(
text: 'See deployment guide on GitHub',
target:
'https://github.com/seemoo-lab/openhaystack/tree/main/Firmware/ESP32',
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
child: const Text('Send per mail'),
onPressed: () async {
await launch(
DeploymentEmail.getESP32DeploymentEmail(
widget.advertisementKey));
},
),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DeploymentInstructionsESP32(
advertisementKey:
widget.advertisementKey,
)),
);
},
),
],
),
],
),
isExpanded: _expanded[1],
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const ListTile(
title: Text('Linux HCI'),
);
},
body: Column(
children: <Widget>[
const ListTile(
title: Text(
'This method only requires a Bluetooth enabled '
'Linux device. Using the hcitool and a provided script '
'the devices advertises the created key so it can be '
'found by Apple\'s Find My network.'),
),
ListTile(
title: Hyperlink(
text: 'See deployment guide on GitHub',
target:
'https://github.com/seemoo-lab/openhaystack/tree/main/Firmware/Linux_HCI',
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
child: const Text('Send per mail'),
onPressed: () async {
await launch(
DeploymentEmail.getLinuxHCIDeploymentEmail(
widget.advertisementKey));
},
),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DeploymentInstructionsLinux(
advertisementKey:
widget.advertisementKey,
)),
);
},
),
],
),
],
),
isExpanded: _expanded[2],
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/deployment/code_block.dart';
import 'package:openhaystack_mobile/deployment/deployment_details.dart';
import 'package:openhaystack_mobile/deployment/hyperlink.dart';
class DeploymentInstructionsLinux extends StatelessWidget {
String advertisementKey;
/// Displays a deployment guide for the generic Linux HCI platform.
DeploymentInstructionsLinux({
Key? key,
this.advertisementKey = '<ADVERTISEMENT_KEY>',
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DeploymentDetails(
title: 'Linux HCI Deployment',
steps: [
const Step(
title: Text('Requirements'),
content: Text('Install the hcitool software on a Bluetooth '
'Low Energy Linux device, for example a Raspberry Pi. '
'Additionally Pyhton 3 needs to be installed.'),
),
Step(
title: const Text('Download'),
content: Column(
children: [
const Text('Next download the python script that '
'configures the HCI tool to send out BLE advertisements.'),
Hyperlink(target: 'https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py'),
CodeBlock(text: 'curl -o HCI.py https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py'),
],
),
),
Step(
title: const Text('Usage'),
content: Column(
children: [
const Text('To start the BLE advertisements run the script.'),
CodeBlock(text: 'sudo python3 HCI.py --key $advertisementKey'),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/deployment/code_block.dart';
import 'package:openhaystack_mobile/deployment/deployment_details.dart';
import 'package:openhaystack_mobile/deployment/hyperlink.dart';
class DeploymentInstructionsNRF51 extends StatelessWidget {
String advertisementKey;
/// Displays a deployment guide for the NRF51 platform.
DeploymentInstructionsNRF51({
Key? key,
this.advertisementKey = '<ADVERTISEMENT_KEY>',
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DeploymentDetails(
title: 'nRF51822 Deployment',
steps: [
const Step(
title: Text('Requirements'),
content: Text('To build the firmware the GNU Arm Embedded '
'Toolchain is required.'),
),
Step(
title: const Text('Download'),
content: Column(
children: [
const Text('Download the firmware source code from GitHub '
'and navigate to the given folder.'),
Hyperlink(target: 'https://github.com/seemoo-lab/openhaystack'),
CodeBlock(text: 'git clone https://github.com/seemoo-lab/openhaystack.git && cd openhaystack/Firmware/Microbit_v1'),
],
),
),
Step(
title: const Text('Build'),
content: Column(
children: [
const Text('Replace the public_key in main.c (initially '
'OFFLINEFINEINGPUBLICKEYHERE!) with the actual '
'advertisement key. Then execute make to create the '
'firmware.'),
CodeBlock(text: 'static char public_key[28] = "$advertisementKey";'),
CodeBlock(text: 'make'),
],
),
),
Step(
title: const Text('Firmware Deployment'),
content: Column(
children: [
const Text('If the firmware is built successfully it can '
'be deployed to the microcontroller with the following '
'command.'),
const Text(
'Please fill in the volume of your microcontroller.',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
CodeBlock(text: 'make install DEPLOY_PATH=/Volumes/MICROBIT'),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class Hyperlink extends StatelessWidget {
/// The target url to open.
String target;
/// The display text of the hyperlink. Default is [target].
String _text;
/// Displays a hyperlink that can be opened by a tap.
Hyperlink({
Key? key,
required this.target,
text,
}) : _text = text ?? target, super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
child: Text(_text,
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
onTap: () {
launch(target);
},
);
}
}

View File

@@ -0,0 +1,115 @@
import 'dart:convert';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';
import 'package:pointycastle/src/utils.dart' as pc_utils;
import 'package:openhaystack_mobile/findMy/models.dart';
class DecryptReports {
/// Decrypts a given [FindMyReport] with the given private key.
static Future<FindMyLocationReport> decryptReport(
FindMyReport report, Uint8List key) async {
final curveDomainParam = ECCurve_secp224r1();
final payloadData = report.payload;
final ephemeralKeyBytes = payloadData.sublist(5, 62);
final encData = payloadData.sublist(62, 72);
final tag = payloadData.sublist(72, payloadData.length);
_decodeTimeAndConfidence(payloadData, report);
final privateKey = ECPrivateKey(
pc_utils.decodeBigIntWithSign(1, key),
curveDomainParam);
final decodePoint = curveDomainParam.curve.decodePoint(ephemeralKeyBytes);
final ephemeralPublicKey = ECPublicKey(decodePoint, curveDomainParam);
final Uint8List sharedKeyBytes = _ecdh(ephemeralPublicKey, privateKey);
final Uint8List derivedKey = _kdf(sharedKeyBytes, ephemeralKeyBytes);
final decryptedPayload = _decryptPayload(encData, derivedKey, tag);
final locationReport = _decodePayload(decryptedPayload, report);
return locationReport;
}
/// Decodes the unencrypted timestamp and confidence
static void _decodeTimeAndConfidence(Uint8List payloadData, FindMyReport report) {
final seenTimeStamp = payloadData.sublist(0, 4).buffer.asByteData()
.getInt32(0, Endian.big);
final timestamp = DateTime(2001).add(Duration(seconds: seenTimeStamp));
final confidence = payloadData.elementAt(4);
report.timestamp = timestamp;
report.confidence = confidence;
}
/// Performs an Elliptic Curve Diffie-Hellman with the given keys.
/// Returns the derived raw key data.
static Uint8List _ecdh(ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) {
final sharedKey = ephemeralPublicKey.Q! * privateKey.d;
final sharedKeyBytes = pc_utils.encodeBigIntAsUnsigned(
sharedKey!.x!.toBigInteger()!);
print("Isolate:${Isolate.current.hashCode}: Shared Key (shared secret): ${base64Encode(sharedKeyBytes)}");
return sharedKeyBytes;
}
/// Decodes the raw decrypted payload and constructs and returns
/// the resulting [FindMyLocationReport].
static FindMyLocationReport _decodePayload(
Uint8List payload, FindMyReport report) {
final latitude = payload.buffer.asByteData(0, 4).getUint32(0, Endian.big);
final longitude = payload.buffer.asByteData(4, 4).getUint32(0, Endian.big);
final accuracy = payload.buffer.asByteData(8, 1).getUint8(0);
final latitudeDec = latitude / 10000000.0;
final longitudeDec = longitude / 10000000.0;
return FindMyLocationReport(latitudeDec, longitudeDec, accuracy,
report.datePublished, report.timestamp, report.confidence);
}
/// Decrypts the given cipher text with the key data using an AES-GCM block cipher.
/// Returns the decrypted raw data.
static Uint8List _decryptPayload(
Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) {
final decryptionKey = symmetricKey.sublist(0, 16);
final iv = symmetricKey.sublist(16, symmetricKey.length);
final aesGcm = GCMBlockCipher(AESEngine())
..init(false, AEADParameters(KeyParameter(decryptionKey),
tag.lengthInBytes * 8, iv, tag));
final plainText = Uint8List(cipherText.length);
var offset = 0;
while (offset < cipherText.length) {
offset += aesGcm.processBlock(cipherText, offset, plainText, offset);
}
assert(offset == cipherText.length);
return plainText;
}
/// ANSI X.963 key derivation to calculate the actual (symmetric) advertisement
/// key and returns the raw key data.
static Uint8List _kdf(Uint8List secret, Uint8List ephemeralKey) {
var shaDigest = SHA256Digest();
shaDigest.update(secret, 0, secret.length);
var counter = 1;
var counterData = ByteData(4)..setUint32(0, counter);
var counterDataBytes = counterData.buffer.asUint8List();
shaDigest.update(counterDataBytes, 0, counterDataBytes.lengthInBytes);
shaDigest.update(ephemeralKey, 0, ephemeralKey.lengthInBytes);
Uint8List out = Uint8List(shaDigest.digestSize);
shaDigest.doFinal(out, 0);
print("Isolate:${Isolate.current.hashCode}: Derived key: ${base64Encode(out)}");
return out;
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:pointycastle/export.dart';
import 'package:pointycastle/src/platform_check/platform_check.dart';
import 'package:pointycastle/src/utils.dart' as pc_utils;
import 'package:openhaystack_mobile/findMy/decrypt_reports.dart';
import 'package:openhaystack_mobile/findMy/models.dart';
import 'package:openhaystack_mobile/findMy/reports_fetcher.dart';
class FindMyController {
static const _storage = FlutterSecureStorage();
static final ECCurve_secp224r1 _curveParams = ECCurve_secp224r1();
static HashMap _keyCache = HashMap();
/// Starts a new [Isolate], fetches and decrypts all location reports
/// for the given [FindMyKeyPair].
/// Returns a list of [FindMyLocationReport]'s.
static Future<List<FindMyLocationReport>> computeResults(FindMyKeyPair keyPair) async{
await _loadPrivateKey(keyPair);
return compute(_getListedReportResults, keyPair);
}
/// Fetches and decrypts the location reports for the given
/// [FindMyKeyPair] from apples FindMy Network.
/// Returns a list of [FindMyLocationReport].
static Future<List<FindMyLocationReport>> _getListedReportResults(FindMyKeyPair keyPair) async{
List<FindMyLocationReport> results = <FindMyLocationReport>[];
final jsonResults = await ReportsFetcher.fetchLocationReports(keyPair.getHashedAdvertisementKey());
for (var result in jsonResults) {
results.add(await _decryptResult(result, keyPair, keyPair.privateKeyBase64!));
}
return results;
}
/// Loads the private key from the local cache or secure storage and adds it
/// to the given [FindMyKeyPair].
static Future<void> _loadPrivateKey(FindMyKeyPair keyPair) async {
String? privateKey;
if (!_keyCache.containsKey(keyPair.hashedPublicKey)) {
privateKey = await _storage.read(key: keyPair.hashedPublicKey);
final newKey = _keyCache.putIfAbsent(keyPair.hashedPublicKey, () => privateKey);
assert(newKey == privateKey);
} else {
privateKey = _keyCache[keyPair.hashedPublicKey];
}
keyPair.privateKeyBase64 = privateKey!;
}
/// Derives an [ECPublicKey] from a given [ECPrivateKey] on the given curve.
static ECPublicKey _derivePublicKey(ECPrivateKey privateKey) {
final pk = _curveParams.G * privateKey.d;
final publicKey = ECPublicKey(pk, _curveParams);
print("Isolate:${Isolate.current.hashCode}: Point Data: ${base64Encode(publicKey.Q!.getEncoded(false))}");
return publicKey;
}
/// Decrypts the encrypted reports with the given [FindMyKeyPair] and private key.
/// Returns the decrypted report as a [FindMyLocationReport].
static Future<FindMyLocationReport> _decryptResult(dynamic result, FindMyKeyPair keyPair, String privateKey) async {
assert (result["id"]! == keyPair.getHashedAdvertisementKey(),
"Returned FindMyReport hashed key != requested hashed key");
final unixTimestampInMillis = result["datePublished"];
final datePublished = DateTime.fromMillisecondsSinceEpoch(unixTimestampInMillis);
FindMyReport report = FindMyReport(
datePublished,
base64Decode(result["payload"]),
keyPair.getHashedAdvertisementKey(),
result["statusCode"]);
FindMyLocationReport decryptedReport = await DecryptReports
.decryptReport(report, base64Decode(privateKey));
return decryptedReport;
}
/// Returns the to the base64 encoded given hashed public key
/// corresponding [FindMyKeyPair] from the local [FlutterSecureStorage].
static Future<FindMyKeyPair> getKeyPair(String base64HashedPublicKey) async {
final privateKeyBase64 = await _storage.read(key: base64HashedPublicKey);
ECPrivateKey privateKey = ECPrivateKey(
pc_utils.decodeBigIntWithSign(1, base64Decode(privateKeyBase64!)), _curveParams);
ECPublicKey publicKey = _derivePublicKey(privateKey);
return FindMyKeyPair(publicKey, base64HashedPublicKey, privateKey, DateTime.now(), -1);
}
/// Imports a base64 encoded private key to the local [FlutterSecureStorage].
/// Returns a [FindMyKeyPair] containing the corresponding [ECPublicKey].
static Future<FindMyKeyPair> importKeyPair(String privateKeyBase64) async {
final privateKeyBytes = base64Decode(privateKeyBase64);
final ECPrivateKey privateKey = ECPrivateKey(
pc_utils.decodeBigIntWithSign(1, privateKeyBytes), _curveParams);
final ECPublicKey publicKey = _derivePublicKey(privateKey);
final hashedPublicKey = getHashedPublicKey(publicKey: publicKey);
final keyPair = FindMyKeyPair(
publicKey,
hashedPublicKey,
privateKey,
DateTime.now(),
-1);
await _storage.write(key: hashedPublicKey, value: keyPair.getBase64PrivateKey());
return keyPair;
}
/// Generates a [ECCurve_secp224r1] keypair.
/// Returns the newly generated keypair as a [FindMyKeyPair] object.
static Future<FindMyKeyPair> generateKeyPair() async {
final ecCurve = ECCurve_secp224r1();
final secureRandom = SecureRandom('Fortuna')
..seed(KeyParameter(
Platform.instance.platformEntropySource().getBytes(32)));
ECKeyGenerator keyGen = ECKeyGenerator()
..init(ParametersWithRandom(ECKeyGeneratorParameters(ecCurve), secureRandom));
final newKeyPair = keyGen.generateKeyPair();
final ECPublicKey publicKey = newKeyPair.publicKey as ECPublicKey;
final ECPrivateKey privateKey = newKeyPair.privateKey as ECPrivateKey;
final hashedKey = getHashedPublicKey(publicKey: publicKey);
final keyPair = FindMyKeyPair(publicKey, hashedKey, privateKey, DateTime.now(), -1);
await _storage.write(key: hashedKey, value: keyPair.getBase64PrivateKey());
return keyPair;
}
/// Returns hashed, base64 encoded public key for given [publicKeyBytes]
/// or for an [ECPublicKey] object [publicKey], if [publicKeyBytes] equals null.
/// Returns the base64 encoded hashed public key as a [String].
static String getHashedPublicKey({Uint8List? publicKeyBytes, ECPublicKey? publicKey}) {
var pkBytes = publicKeyBytes ?? publicKey!.Q!.getEncoded(false);
final shaDigest = SHA256Digest();
shaDigest.update(pkBytes, 0, pkBytes.lengthInBytes);
Uint8List out = Uint8List(shaDigest.digestSize);
shaDigest.doFinal(out, 0);
return base64Encode(out);
}
}

View File

@@ -0,0 +1,84 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/src/utils.dart' as pc_utils;
import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
/// Represents a decrypted FindMyReport.
class FindMyLocationReport {
double latitude;
double longitude;
int accuracy;
DateTime published;
DateTime? timestamp;
int? confidence;
FindMyLocationReport(this.latitude, this.longitude, this.accuracy,
this.published, this.timestamp, this.confidence);
Location get location => Location(latitude, longitude);
}
class Location {
double latitude;
double longitude;
Location(this.latitude, this.longitude);
}
/// FindMy report returned by the FindMy Network
class FindMyReport {
DateTime datePublished;
Uint8List payload;
String id;
int statusCode;
int? confidence;
DateTime? timestamp;
FindMyReport(this.datePublished, this.payload, this.id, this.statusCode);
FindMyReport.completeInit(this.datePublished, this.payload, this.id, this.statusCode,
this.confidence, this.timestamp);
}
class FindMyKeyPair {
final ECPublicKey _publicKey;
final ECPrivateKey _privateKey;
final String hashedPublicKey;
String? privateKeyBase64;
/// Time when this key was used to send BLE advertisements
DateTime startTime;
/// Duration from start time how long the key was used to send BLE advertisements
double duration;
FindMyKeyPair(this._publicKey, this.hashedPublicKey, this._privateKey, this.startTime,
this.duration);
String getBase64PublicKey() {
return base64Encode(_publicKey.Q!.getEncoded(false));
}
String getBase64PrivateKey() {
return base64Encode(pc_utils.encodeBigIntAsUnsigned(_privateKey.d!));
}
String getBase64AdvertisementKey() {
return base64Encode(_getAdvertisementKey());
}
Uint8List _getAdvertisementKey() {
var pkBytes = _publicKey.Q!.getEncoded(true);
//Drop first byte to get the 28byte version
var key = pkBytes.sublist(1, pkBytes.length);
return key;
}
String getHashedAdvertisementKey() {
var key = _getAdvertisementKey();
return FindMyController.getHashedPublicKey(publicKeyBytes: key);
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class ReportsFetcher {
static const _seemooEndpoint = "https://add-your-proxy-server-here/getLocationReports"
/// Fetches the location reports corresponding to the given hashed advertisement
/// key.
/// Throws [Exception] if no answer was received.
static Future<List> fetchLocationReports(String hashedAdvertisementKey) async {
final response = await http.post(Uri.parse(_seemooEndpoint),
headers: <String, String>{
"Content-Type": "application/json",
},
body: jsonEncode(<String, dynamic>{
"ids": [hashedAdvertisementKey],
}));
if (response.statusCode == 200) {
return await jsonDecode(response.body)["results"];
} else {
throw Exception("Failed to fetch location reports with statusCode:${response.statusCode}\n\n Response:\n${response}");
}
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:openhaystack_mobile/accessory/accessory_model.dart';
import 'package:latlong2/latlong.dart';
import 'package:openhaystack_mobile/history/days_selection_slider.dart';
import 'package:openhaystack_mobile/history/location_popup.dart';
class AccessoryHistory extends StatefulWidget {
Accessory accessory;
/// Shows previous locations of a specific [accessory] on a map.
/// The locations are connected by a chronological line.
/// The number of days to go back can be adjusted with a slider.
AccessoryHistory({
Key? key,
required this.accessory,
}) : super(key: key);
@override
_AccessoryHistoryState createState() => _AccessoryHistoryState();
}
class _AccessoryHistoryState extends State<AccessoryHistory> {
final MapController _mapController = MapController();
bool showPopup = false;
Pair<LatLng, DateTime>? popupEntry;
double numberOfDays = 7;
@override
void initState() {
super.initState();
_mapController.onReady
.then((_) {
var historicLocations = widget.accessory.locationHistory
.map((entry) => entry.a).toList();
var bounds = LatLngBounds.fromPoints(historicLocations);
_mapController.fitBounds(bounds);
});
}
@override
Widget build(BuildContext context) {
// Filter for the locations after the specified cutoff date (now - number of days)
var now = DateTime.now();
List<Pair<LatLng, DateTime>> locationHistory = widget.accessory.locationHistory
.where(
(element) => element.b.isAfter(
now.subtract(Duration(days: numberOfDays.round())),
),
).toList();
return Scaffold(
appBar: AppBar(
title: Text(widget.accessory.name),
),
body: SafeArea(
child: Column(
children: <Widget>[
Flexible(
flex: 3,
fit: FlexFit.tight,
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
center: LatLng(49.874739, 8.656280),
zoom: 13.0,
interactiveFlags:
InteractiveFlag.pinchZoom | InteractiveFlag.drag |
InteractiveFlag.doubleTapZoom | InteractiveFlag.flingAnimation |
InteractiveFlag.pinchMove,
onTap: (_, __) {
setState(() {
showPopup = false;
popupEntry = null;
});
},
),
layers: [
TileLayerOptions(
backgroundColor: Theme.of(context).colorScheme.surface,
tileBuilder: (context, child, tile) {
var isDark = (Theme.of(context).brightness == Brightness.dark);
return isDark ? ColorFiltered(
colorFilter: const ColorFilter.matrix([
-1, 0, 0, 0, 255,
0, -1, 0, 0, 255,
0, 0, -1, 0, 255,
0, 0, 0, 1, 0,
]),
child: child,
) : child;
},
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c'],
attributionBuilder: (_) {
return const Text("© OpenStreetMap contributors");
},
),
// The line connecting the locations chronologically
PolylineLayerOptions(
polylines: [
Polyline(
points: locationHistory.map((entry) => entry.a).toList(),
strokeWidth: 4,
color: Theme.of(context).colorScheme.primaryVariant,
),
],
),
// The markers for the historic locaitons
MarkerLayerOptions(
markers: locationHistory.map((entry) => Marker(
point: entry.a,
builder: (ctx) => GestureDetector(
onTap: () {
setState(() {
showPopup = true;
popupEntry = entry;
});
},
child: Icon(
Icons.circle,
size: 15,
color: entry == popupEntry
? Colors.red
: Theme.of(context).indicatorColor,
),
),
)).toList(),
),
// Displays the tooltip if active
MarkerLayerOptions(
markers: [
if (showPopup) LocationPopup(
location: popupEntry!.a,
time: popupEntry!.b,
),
],
),
],
),
),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: DaysSelectionSlider(
numberOfDays: numberOfDays,
onChanged: (double newValue) {
setState(() {
numberOfDays = newValue;
});
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class DaysSelectionSlider extends StatefulWidget {
/// The number of days currently selected.
double numberOfDays;
/// A callback listening for value changes.
ValueChanged<double> onChanged;
/// Display a slider that allows to define how many days to go back
/// (range 1 to 7).
DaysSelectionSlider({
Key? key,
required this.numberOfDays,
required this.onChanged,
}) : super(key: key);
@override
_DaysSelectionSliderState createState() => _DaysSelectionSliderState();
}
class _DaysSelectionSliderState extends State<DaysSelectionSlider> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Center(
child: Text(
'How many days back?',
style: TextStyle(fontSize: 20),
),
),
Row(
children: [
const Text('1', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(
child: Slider(
value: widget.numberOfDays,
min: 1,
max: 7,
label: '${widget.numberOfDays.round()}',
divisions: 6,
onChanged: widget.onChanged,
),
),
const Text('7', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong2/latlong.dart';
class LocationPopup extends Marker {
/// The location to display.
LatLng location;
/// The time stamp the location was recorded.
DateTime time;
/// Displays a small popup window with the coordinates at [location] and
/// the [time] in a human readable format.
LocationPopup({
Key? key,
required this.location,
required this.time,
}) : super(
key: key,
width: 200,
height: 150,
point: location,
builder: (ctx) => Padding(
padding: const EdgeInsets.only(bottom: 80),
child: InkWell(
onTap: () { /* NOOP */ },
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Text(
time.toLocal().toString().substring(0, 19),
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Lat: ${location.round(decimals: 2).latitude}, '
'Lng: ${location.round(decimals: 2).longitude}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
),
),
rotate: true,
);
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/accessory/accessory_color_selector.dart';
class AccessoryColorInput extends StatelessWidget {
/// The inititial color value
Color color;
/// Callback called when the color is changed. Parameter is null
/// if color did not change
ValueChanged<Color?> changeListener;
/// Displays a color selection input that previews the current selection.
AccessoryColorInput({
Key? key,
required this.color,
required this.changeListener,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Row(
children: [
const Text('Color: '),
Icon(
Icons.circle,
color: color,
),
const Spacer(),
OutlinedButton(
child: const Text('Change'),
onPressed: () async {
Color? selectedColor = await AccessoryColorSelector
.showColorSelection(context, color);
changeListener(selectedColor);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:openhaystack_mobile/accessory/accessory_icon_selector.dart';
class AccessoryIconInput extends StatelessWidget {
/// The initial icon
IconData initialIcon;
/// The original icon name
String iconString;
/// The color of the icon
Color color;
/// Callback called when the icon is changed. Parameter is null
/// if icon did not change
ValueChanged<String?> changeListener;
/// Displays an icon selection input that previews the current selection.
AccessoryIconInput({
Key? key,
required this.initialIcon,
required this.iconString,
required this.color,
required this.changeListener,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Row(
children: [
const Text('Icon: '),
Icon(initialIcon),
const Spacer(),
OutlinedButton(
child: const Text('Change'),
onPressed: () async {
String? selectedIcon = await AccessoryIconSelector
.showIconSelection(context, iconString, color);
changeListener(selectedIcon);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class AccessoryIdInput extends StatelessWidget {
ValueChanged<String?> changeListener;
/// Displays an input field with validation for an accessory ID.
AccessoryIdInput({
Key? key,
required this.changeListener,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
child: TextFormField(
decoration: const InputDecoration(
labelText: 'ID',
),
validator: (value) {
if (value == null) {
return 'ID must be provided.';
}
int? parsed = int.tryParse(value);
if (parsed == null) {
return 'ID must be an integer value.';
}
return null;
},
onSaved: changeListener,
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More