Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0f37d129 | ||
|
|
6b3196e798 | ||
|
|
9829d6ceb4 | ||
|
|
6c4895d68f | ||
|
|
33716d7f9d | ||
|
|
e27051e71e | ||
|
|
ed2c80b8c7 | ||
|
|
62bbee528e | ||
|
|
00e3b5ad14 | ||
|
|
3d593a006c | ||
|
|
b65a6e6be0 |
6
.github/workflows/build-app.yml
vendored
@@ -18,7 +18,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
format-swift:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: swift-format lint --recursive .
|
||||
|
||||
format-objc:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: clang-format -n **/*.{h,m}
|
||||
|
||||
build-app:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-12
|
||||
needs:
|
||||
- format-swift
|
||||
- format-objc
|
||||
|
||||
@@ -48,7 +48,7 @@ class AccessoryNearbyMonitor: BluetoothAccessoryDelegate {
|
||||
accessory.lastAdvertisement = Date()
|
||||
}
|
||||
|
||||
func removeNearbyAccessories(now: Date = Date(), timeout: TimeInterval = 10.0) {
|
||||
func removeNearbyAccessories(now: Date = Date(), timeout: TimeInterval = 120.0) {
|
||||
let nearbyAccessories = self.accessoryController.accessories.filter({ $0.isNearby })
|
||||
for accessory in nearbyAccessories {
|
||||
guard let lastAdvertisement = accessory.lastAdvertisement else {
|
||||
|
||||
@@ -98,6 +98,8 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
Divider()
|
||||
Button("Mark as \(accessory.isDeployed ? "deployable" : "deployed")", action: { accessory.isDeployed.toggle() })
|
||||
|
||||
Button("Copy private Key B64", action: { copyPrivateKey(accessory: accessory) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +190,15 @@ struct AccessoryListEntry: View {
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
func copyPrivateKey(accessory: Accessory) {
|
||||
let privateKey = accessory.privateKey
|
||||
let keyB64 = privateKey.base64EncodedString()
|
||||
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.prepareForNewContents(with: .currentHostOnly)
|
||||
pasteboard.setString(keyB64, forType: .string)
|
||||
}
|
||||
|
||||
struct AccessoryListEntry_Previews: PreviewProvider {
|
||||
@StateObject static var accessory = PreviewData.accessories.first!
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string># For Mail.app version 16.0 (3696.80.82.1.1) on macOS version 12.3.1 (build 21E258)</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.4PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
@@ -135,6 +137,7 @@
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.5PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
@@ -142,6 +145,7 @@
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.6PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
@@ -149,6 +153,7 @@
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.7PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
@@ -156,6 +161,7 @@
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.8PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
@@ -163,12 +169,14 @@
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.9PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
10
README.md
@@ -19,6 +19,7 @@ OpenHaystack is a framework for tracking personal Bluetooth devices via Apple's
|
||||
- [Finding](#finding-3)
|
||||
- [Searching](#searching-4)
|
||||
- [How to track other Bluetooth devices?](#how-to-track-other-bluetooth-devices)
|
||||
- [OpenHaystack Mobile](#openhaystack-mobile)
|
||||
- [Authors](#authors)
|
||||
- [References](#references)
|
||||
- [License](#license)
|
||||
@@ -116,6 +117,15 @@ Feel free to port OpenHaystack to other devices that support Bluetooth Low Energ
|
||||
|
||||

|
||||
|
||||
## OpenHaystack Mobile
|
||||
OpenHaystack Mobile is a complete reimplementation of the OpenHaystack macOS application for smartphones. The app provides the same functionality to create and track accessories and aims to increase the usability, especially for new users. In contrast to the macOS application, the location reports cannot be fetched directly on the smartphone, so the app requires a proxy server hosted on Mac hardware to access the Find My network. The proxy server can be accessed over a network by multiple users simultaneously.
|
||||
|
||||
To connect to your proxy server set the correct URL in: openhaystack-mobile/lib/findMy/reports_fetcher.dart
|
||||
|
||||
<img width="300" src="./Resources/mobile-map-view.png"> <img width="300" src="./Resources/mobile-accessory-history.png">
|
||||
|
||||
OpenHaystack Mobile is built with the cross-platform [Flutter framework](https://flutter.dev/) and currently runs on Android and iOS. More information about the app and usage instructions can be found in the [openhaystack-mobile](openhaystack-mobile) folder of this repository.
|
||||
|
||||
## Authors
|
||||
|
||||
- **Alexander Heinrich** ([@Sn0wfreezeDev](https://github.com/Sn0wfreezeDev), [email](mailto:aheinrich@seemoo.tu-darmstadt.de))
|
||||
|
||||
BIN
Resources/mobile-accessory-history.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Resources/mobile-map-view.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
46
openhaystack-mobile/.gitignore
vendored
Normal 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
|
||||
10
openhaystack-mobile/.metadata
Normal 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
|
||||
52
openhaystack-mobile/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# OpenHaystack Mobile
|
||||
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.
|
||||
|
||||
# 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.
|
||||
29
openhaystack-mobile/analysis_options.yaml
Normal 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
@@ -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
|
||||
68
openhaystack-mobile/android/app/build.gradle
Normal 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"
|
||||
}
|
||||
@@ -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>
|
||||
63
openhaystack-mobile/android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.seemoo.android.openhaystack
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
29
openhaystack-mobile/android/build.gradle
Normal 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
|
||||
}
|
||||
3
openhaystack-mobile/android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
6
openhaystack-mobile/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
11
openhaystack-mobile/android/settings.gradle
Normal 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"
|
||||
BIN
openhaystack-mobile/assets/OpenHaystackIcon.png
Normal file
|
After Width: | Height: | Size: 671 KiB |
34
openhaystack-mobile/ios/.gitignore
vendored
Normal 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
|
||||
26
openhaystack-mobile/ios/Flutter/AppFrameworkInfo.plist
Normal 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>
|
||||
2
openhaystack-mobile/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
2
openhaystack-mobile/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
41
openhaystack-mobile/ios/Podfile
Normal 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
|
||||
123
openhaystack-mobile/ios/Podfile.lock
Normal 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
|
||||
785
openhaystack-mobile/ios/Runner.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
7
openhaystack-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
10
openhaystack-mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal 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>
|
||||
@@ -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>
|
||||
13
openhaystack-mobile/ios/Runner/AppDelegate.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
23
openhaystack-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
openhaystack-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
openhaystack-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
openhaystack-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
openhaystack-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||
@@ -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>
|
||||
26
openhaystack-mobile/ios/Runner/Base.lproj/Main.storyboard
Normal 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>
|
||||
63
openhaystack-mobile/ios/Runner/Info.plist
Normal 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>
|
||||
1
openhaystack-mobile/ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
10
openhaystack-mobile/ios/Runner/Runner.entitlements
Normal 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>
|
||||
@@ -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>
|
||||
21
openhaystack-mobile/ios/ShareExtension/Info.plist
Normal 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>
|
||||
@@ -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>
|
||||
342
openhaystack-mobile/ios/ShareExtension/ShareViewController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
166
openhaystack-mobile/lib/accessory/accessory_detail.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
openhaystack-mobile/lib/accessory/accessory_dto.dart
Normal 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,
|
||||
};
|
||||
|
||||
}
|
||||
40
openhaystack-mobile/lib/accessory/accessory_icon.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
openhaystack-mobile/lib/accessory/accessory_icon_model.dart
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
openhaystack-mobile/lib/accessory/accessory_list.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
75
openhaystack-mobile/lib/accessory/accessory_list_item.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
225
openhaystack-mobile/lib/accessory/accessory_model.dart
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
155
openhaystack-mobile/lib/accessory/accessory_registry.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
30
openhaystack-mobile/lib/accessory/no_accessories.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
93
openhaystack-mobile/lib/dashboard/dashboard_desktop.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
121
openhaystack-mobile/lib/dashboard/dashboard_mobile.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
openhaystack-mobile/lib/deployment/code_block.dart
Normal 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));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
openhaystack-mobile/lib/deployment/deployment_details.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
openhaystack-mobile/lib/deployment/deployment_email.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
67
openhaystack-mobile/lib/deployment/deployment_esp32.dart
Normal 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"'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
253
openhaystack-mobile/lib/deployment/deployment_instructions.dart
Normal 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],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
openhaystack-mobile/lib/deployment/deployment_linux_hci.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
70
openhaystack-mobile/lib/deployment/deployment_nrf51.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
31
openhaystack-mobile/lib/deployment/hyperlink.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
115
openhaystack-mobile/lib/findMy/decrypt_reports.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
146
openhaystack-mobile/lib/findMy/find_my_controller.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
84
openhaystack-mobile/lib/findMy/models.dart
Normal 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);
|
||||
}
|
||||
}
|
||||