build: initial android app build

This commit is contained in:
2026-07-16 17:36:28 +02:00
parent ddae86ea6f
commit 34e9d3f9ef
68 changed files with 4555 additions and 13 deletions
+4 -1
View File
@@ -19,6 +19,8 @@ AGENTS.md
# Zephyr # Zephyr
# ---------------------------- # ----------------------------
firmware/app/build/ firmware/app/build/
firmware/app/build_*/
firmware/diagnostic
external/ external/
west-manifest/ west-manifest/
.venv/ .venv/
@@ -66,4 +68,5 @@ _autosave-*
# ---------------------------- # ----------------------------
# Divers # Divers
# ---------------------------- # ----------------------------
tools/ tools/
*.xcf
+81
View File
@@ -10,6 +10,28 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
### Added ### Added
- One-second white RGB indication when firmware initialization completes and
the keypad loop is ready.
- Branded Android launcher icon and native splash screen using the project
assets.
- French and English mobile UI with automatic phone-language selection and a
manual override.
- Dark mobile dashboard, shortcut bar, styled modals, code limits, opening
history, phone-side NFC UID scanning, and administrator/guest role controls.
- Secure saved-box records, automatic BLE reconnection, invitation QR
generation/scanning, and guest revocation.
- AES-256-GCM password-protected mobile backup and restore using a
PBKDF2-SHA256 derived key.
- Persistent box names and administrator factory reset with BLE bond removal.
- Persistent seven-day opening history for permanent codes, temporary codes,
named NFC tags, and named mobile identities.
- Fallback time `2026-06-01T00:00:00Z` and reboot continuity from the newest
persisted opening event.
- Eight permanent and twenty temporary access-code slots.
- BLE LE Secure Connections, AES-CCM encrypted GATT access, persistent bonding,
and 128-bit administrator/guest application identities.
- First-phone administrator provisioning without exposing the stored key.
- App-triggered NFC enrollment with persistent tag names.
- Initial project structure. - Initial project structure.
- Zephyr RTOS firmware baseline. - Zephyr RTOS firmware baseline.
- Standardized project architecture. - Standardized project architecture.
@@ -27,6 +49,13 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
- Persistent six-digit access code storage. - Persistent six-digit access code storage.
- Persistent NFC tag UID storage with default development UID `60:4F:E2:B5`. - Persistent NFC tag UID storage with default development UID `60:4F:E2:B5`.
- NFC scan-mode application flow triggered by keypad activity. - NFC scan-mode application flow triggered by keypad activity.
- Flutter mobile application shell in `mobile-app/app`.
- Mobile application use of the root `images/` logo and `background.jpg` assets.
- Mobile BLE scan/connect flow targeting the OpenParcelBox XIAO service.
- Mobile commands for XIAO clock synchronization, direct lock opening, access-code management, and NFC tag management.
- Firmware BLE administration service with JSON command writes and stored-state readback.
- Volatile phone-synchronized firmware clock for lock-opening log timestamps.
- One-time access codes that are removed after first successful keypad use.
- Lock control pulse on XIAO pin `D9`. - Lock control pulse on XIAO pin `D9`.
- Lock state feedback detection using KR-S79 `COM/NC` on XIAO `D7` / `D8`. - Lock state feedback detection using KR-S79 `COM/NC` on XIAO `D7` / `D8`.
- Long buzzer success beep for accepted unlock codes. - Long buzzer success beep for accepted unlock codes.
@@ -48,10 +77,62 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
- Updated keypad unlock user feedback with off-at-rest LEDs, green-open indication, red invalid-code feedback, and `B` entry cancellation. - Updated keypad unlock user feedback with off-at-rest LEDs, green-open indication, red invalid-code feedback, and `B` entry cancellation.
- Updated runtime LED feedback with blue NFC scan-mode indication. - Updated runtime LED feedback with blue NFC scan-mode indication.
- Updated open-lock reminder beep interval to 2 seconds. - Updated open-lock reminder beep interval to 2 seconds.
- Updated the mobile application to reload access codes and NFC tags from the firmware BLE state characteristic.
- Corrected NFC hardware documentation: the nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware, while passive badge UID reading requires a dedicated NFC reader circuit. - Corrected NFC hardware documentation: the nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware, while passive badge UID reading requires a dedicated NFC reader circuit.
### Fixed ### Fixed
- Removed oversized Settings callback allocations that exceeded the 1024-byte
main stack once opening-history or application-identity records existed.
- Moved Settings/NVS loading to a dedicated 4096-byte services thread and
increased the main, system-workqueue, and Bluetooth RX stack margins.
- Kept the keypad fallback code and default clock available from RAM before any
flash or Bluetooth operation can run.
- Added bounded retries when Bluetooth advertising temporarily returns
`-EAGAIN`, and no longer abort advertising solely because stored Bluetooth
settings report a load error.
- Prevented invalid persisted box settings from overwriting the active default
configuration before validation.
- Kept opening-history timestamps monotonic after an older phone clock sync so
one descending event cannot invalidate the complete table on reboot.
- Provided Flutter's Material localization delegates so input fields and
registration dialogs work when the phone language is French.
- Prevented incompatible persistent development-table layouts from blocking
firmware startup before keypad initialization; affected tables now recover
through their safe defaults.
- Made Bluetooth initialization asynchronous so keypad operation and USB
availability no longer wait for BLE controller startup.
- Removed the silent early exit when RGB LED initialization fails.
- Kept keypad access available with in-memory default credentials when
persistent settings fail, and added background keypad initialization retries.
- Fixed secure BLE startup ordering: the persisted `bt/*` settings and identity
are now loaded after `bt_enable()` and before advertising, preventing
`BLE advertising failed: -11`.
- Moved secure BLE initialization to a dedicated thread started after the local
keypad-ready indication, so Bluetooth or bond restoration can never block
keypad access, lock control, or the main application loop.
- Removed the interactive Zephyr/I2C shell and its competing serial backend;
USB CDC is now owned exclusively by the firmware console, matching the
validated standalone diagnostic configuration.
- Restricted guest BLE sessions to authentication, clock synchronization, and
lock opening, and replaced oversized full-state notifications with a compact
change marker.
- Added strict validation of persisted identity and opening-history records
before they can be used or serialized.
- Removed automatic NVS writes and migrations from the boot path. Default
tables are now available immediately in RAM and are persisted only after a
real credential, identity, tag, box-name, or history change.
- Replaced the coupled application loop with a resilient local-first entry
point: keypad input, code validation, lock control, buzzer, LEDs, and door
feedback run independently from BLE and NFC processing.
- Temporarily removed NFC polling from the production main loop while retaining
NFC storage and BLE management initialization; it will be restored after the
validated keypad/BLE baseline is confirmed on hardware.
- Fixed Flutter inherited-widget assertions when closing input modals by keeping
text controllers alive until their dialog widgets are actually unmounted.
- Changed encrypted-backup restoration to select the backup file before asking
for its password.
- Matrix keypad scanning. - Matrix keypad scanning.
- GPIO expander abstraction. - GPIO expander abstraction.
- Firmware build configuration. - Firmware build configuration.
+32 -3
View File
@@ -32,12 +32,25 @@ Completed or validated:
- Persistent six-digit access codes - Persistent six-digit access codes
- Keypad unlock flow with default development code `784512` - Keypad unlock flow with default development code `784512`
- Persistent NFC tag UID table and scan-mode firmware API - Persistent NFC tag UID table and scan-mode firmware API
- Flutter mobile application with branded Android icon/splash, bilingual dark
interface, secure registration, encrypted backups, and role-aware modals
- Mobile BLE automatic reconnection and command UI for clock sync, lock
opening, history, access codes, NFC tags, and guest invitations
- Firmware BLE service for those mobile commands plus stored-state readback
- One-time access codes with automatic removal after first successful keypad use
- Phone-synchronized runtime timestamps for lock-opening logs
- Persistent seven-day opening history with power-loss time continuity
- Eight permanent and twenty temporary access-code slots
- BLE LE Secure Connections, encrypted GATT access, and 128-bit phone identities
- First-phone administrator provisioning and named guest identities
- App-triggered NFC enrollment with stored tag names
- Lock command pulse on XIAO `D9` - Lock command pulse on XIAO `D9`
- KR-S79 `COM/NC` lock state feedback on XIAO `D7` / `D8` - KR-S79 `COM/NC` lock state feedback on XIAO `D7` / `D8`
- Green-open, off-closed, red-invalid LED feedback - Green-open, off-closed, red-invalid LED feedback
- Short key beeps, long valid-code beep, invalid-code beep sequence, and 2s open reminder beep - Short key beeps, long valid-code beep, invalid-code beep sequence, and 2s open reminder beep
- Door opened / closed serial debug messages - Door opened / closed serial debug messages
- Firmware debug output over serial console - Firmware debug output over serial console
- One-second white RGB confirmation when the local keypad path is ready
In progress: In progress:
@@ -45,16 +58,16 @@ In progress:
- Production keypad mapping - Production keypad mapping
- NFC credential reader hardware selection - NFC credential reader hardware selection
- Hardware validation - Hardware validation
- BLE protocol documentation and hardening
Planned: Planned:
- Administrator access management - Firmware update from the mobile application
- NFC badge unlock with a dedicated reader circuit - NFC badge unlock with a dedicated reader circuit
- Battery management - Battery management
- Zigbee - Zigbee
- OTA updates - OTA updates
- Home Assistant integration - Home Assistant integration
- Mobile configuration application
- Custom PCB - Custom PCB
## Repository Structure ## Repository Structure
@@ -69,7 +82,8 @@ OpenParcelBox/
| +-- app/ Main firmware application | +-- app/ Main firmware application
+-- hardware/ Hardware documentation and reverse engineering +-- hardware/ Hardware documentation and reverse engineering
+-- homeassistant/ Home Assistant integration notes +-- homeassistant/ Home Assistant integration notes
+-- mobile-app/ Mobile application notes +-- mobile-app/ Flutter mobile application and notes
+-- images/ Project logo and application background assets
+-- CHANGELOG.md +-- CHANGELOG.md
+-- LICENSE +-- LICENSE
+-- README.md +-- README.md
@@ -93,6 +107,21 @@ Current firmware modules:
- Lock state feedback - Lock state feedback
- Persistent access codes - Persistent access codes
- Persistent NFC tag UID storage - Persistent NFC tag UID storage
- BLE administration service
## Mobile App
The mobile application is developed with Flutter in:
```text
mobile-app/app/
```
The application provides local-first administrator and guest surfaces for
secure Bluetooth registration, automatic reconnection, XIAO date/time
synchronization, access-code management, NFC enrollment, guest invitation QR
codes, opening history, encrypted backup/restore, direct opening, and
authoritative firmware state readback.
Firmware documentation is available in: Firmware documentation is available in:
+37 -3
View File
@@ -46,10 +46,11 @@ The firmware is based on Zephyr RTOS and follows a modular architecture to simpl
| NFC credential storage and scan API | Done | | NFC credential storage and scan API | Done |
| Dedicated NFC reader driver | Pending | | Dedicated NFC reader driver | Pending |
| Power management | Pending | | Power management | Pending |
| Bluetooth configuration | Pending | | Bluetooth configuration | In progress |
| Zigbee integration | Pending | | Zigbee integration | Pending |
| Home Assistant integration | Pending | | Home Assistant integration | Pending |
| PCB V1 | Pending | | PCB V1 | Pending |
| Flutter mobile application | In progress |
--- ---
@@ -192,7 +193,20 @@ Features:
- Device information. - Device information.
- Firmware information. - Firmware information.
Status: planned. Implemented so far:
- Flutter-side BLE scan and connection flow.
- Draft JSON command transport for clock sync, lock opening, access-code management, and NFC tag management.
- Firmware-side BLE command characteristic and state readback characteristic.
- Phone-synchronized volatile firmware clock for timestamped lock-opening logs.
- BLE LE Secure Connections, encrypted GATT permissions, and bonded peers.
- First-phone administrator plus named guest application identities.
- App-triggered named NFC enrollment.
- Persistent seven-day opening history and reboot time continuity.
- Persistent box identifier, guest invitation/revocation commands, and
administrator factory reset with BLE bond removal.
Status: in progress.
--- ---
@@ -240,7 +254,27 @@ Features:
- Device status. - Device status.
- Firmware update. - Firmware update.
Status: planned. Implemented so far:
- Application shell using the project logo and background image.
- Bluetooth scan/connect UI targeting the XIAO BLE service.
- Phone-to-XIAO date/time sync command.
- Direct lock-open command.
- Random six-digit numeric code generation.
- Permanent code management screen.
- One-time temporary code management screen.
- NFC tag add/remove screen.
- Stored code and NFC tag readback from the firmware BLE state characteristic.
- Branded Android launcher icon and native splash screen.
- Automatic French/English selection with a manual override.
- Dark modal dashboard with separate administrator and guest capabilities.
- Secure box registration, invitation QR scanning/generation, and automatic
BLE reconnection.
- AES-256-GCM password-protected application backup and restore.
- Opening history, code limits, phone-side NFC scans, and destructive-action
warnings.
Status: in progress.
--- ---
+22 -6
View File
@@ -23,7 +23,7 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] Create firmware documentation. - [x] Create firmware documentation.
- [x] Create hardware documentation baseline. - [x] Create hardware documentation baseline.
- [ ] Create development documentation. - [ ] Create development documentation.
- [ ] Create protocol documentation. - [x] Create BLE protocol documentation.
### Firmware Baseline ### Firmware Baseline
@@ -146,7 +146,7 @@ Tasks are grouped by development phase and updated throughout the project.
### Configuration ### Configuration
- [x] Persistent storage. - [x] Persistent storage.
- [ ] Factory reset. - [x] Factory reset.
- [ ] Version information. - [ ] Version information.
--- ---
@@ -181,10 +181,26 @@ Tasks are grouped by development phase and updated throughout the project.
## Later Phases ## Later Phases
- [ ] Bluetooth communication. - [x] Firmware BLE command service.
- [ ] Flutter mobile application. - [x] Initial Flutter mobile application shell.
- [ ] User management. - [x] Mobile Bluetooth scan and connect UI.
- [ ] Local event history. - [x] Mobile clock-sync command.
- [x] Mobile random permanent and one-time code management UI.
- [x] Mobile NFC tag add/remove UI.
- [x] Mobile direct lock-open command UI.
- [x] Mobile stored-state readback from firmware.
- [x] Administrator and named guest identity storage.
- [x] Local seven-day event history.
- [x] Secure BLE pairing and encrypted application authorization.
- [x] App-triggered named NFC enrollment flow.
- [x] Branded Android launcher icon and native splash screen.
- [x] French/English application localization and language override.
- [x] Secure saved-box registration and automatic BLE reconnection.
- [x] Administrator/guest modal dashboard and role restrictions.
- [x] Guest invitation QR generation, scanning, and revocation.
- [x] Password-protected AES-256-GCM application backup and restore.
- [x] Mobile opening-history, NFC, and factory-reset administration.
- [ ] Validate pairing recovery and guest invitation on Android and iOS hardware.
- [ ] Zigbee connectivity. - [ ] Zigbee connectivity.
- [ ] Home Assistant integration. - [ ] Home Assistant integration.
- [ ] Temporary access. - [ ] Temporary access.
Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# 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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# 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
+30
View File
@@ -0,0 +1,30 @@
# 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: "ee80f08bbf97172ec030b8751ceab557177a34a6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: android
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+104
View File
@@ -0,0 +1,104 @@
# OpenParcelBox Mobile App
Flutter application for local OpenParcelBox setup and administration.
## User Interface
- Dark solid background `#333333`, button surfaces `#292929`, modal surfaces
`#303030`, accent `#c19d60`, and text `#c6c6c6`.
- Project logo used for the Android launcher icon and centered native splash.
- Native splash uses the project `background.jpg`.
- Automatic French or English selection from the phone, with a manual override.
- Empty state restricted to box registration and encrypted-backup restoration.
- Shortcut bar for opening and history, plus modal cards for codes, NFC tags,
guests, and settings.
- Guest mode hides NFC, guest administration, factory reset, and future firmware
update controls.
## Registration and Secure Storage
The first phone connecting to an unprovisioned box:
1. chooses the box identifier;
2. generates a random 128-bit administrator identity;
3. sends both values over the encrypted BLE link;
4. stores the remote BLE identifier, box name, role, and identity in the phone
secure keystore.
An administrator creates named guest identities and displays an invitation QR
code. A guest scans that QR code to register the same box and its unique guest
identity. Saved boxes reconnect automatically when the application starts.
Application backups contain the saved connection records. They are protected
with AES-256-GCM and a PBKDF2-SHA256 key derived from the user password before a
file is written. During restoration, the application first asks for the backup
file and only then requests the password needed to decrypt it.
## Features
- Direct lock opening and seven-day opening history.
- Eight permanent and twenty one-time access codes.
- NFC tag listing, renaming, deletion, manual UID entry, phone NFC scanning,
and enrollment through the box reader.
- Named guest creation, invitation QR display, and revocation.
- Firmware clock synchronization after connection.
- Language override, encrypted backup/restore, factory reset, and local removal
of a saved box.
- Reserved disabled entry for a later firmware-update flow.
## BLE Security and Protocol
The firmware requires BLE LE Secure Connections and encrypted GATT access.
Bluetooth uses AES-CCM link encryption. Every application command is also
authorized with the random administrator or guest identity stored in the phone
keystore.
Service UUID:
```text
f2a00000-8e7a-4f8d-9b1d-7d8e4b7a0001
```
Command characteristic:
```text
f2a00001-8e7a-4f8d-9b1d-7d8e4b7a0001
```
State characteristic:
```text
f2a00002-8e7a-4f8d-9b1d-7d8e4b7a0001
```
See `docs/firmware/bluetooth.md` from the repository root for the command and
state formats.
## Dependencies
The app uses:
- `flutter_blue_plus` for BLE;
- `flutter_secure_storage` for connection identities;
- `mobile_scanner` and `qr_flutter` for guest invitations;
- `nfc_manager` for optional phone-side NFC UID scans;
- `file_picker` and `cryptography` for encrypted backups.
`flutter_blue_plus` 2.3.10 requires a license mode when connecting. The app uses
`License.nonprofit` for this open-source project.
## Development
```powershell
flutter pub get
flutter analyze
flutter test
flutter build apk --debug
```
Native assets can be regenerated with:
```powershell
dart run flutter_launcher_icons
dart run flutter_native_splash:create
```
+28
View File
@@ -0,0 +1,28 @@
# 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.dev/lints.
#
# 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
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,46 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "fr.zaynet.openparcelbox.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "fr.zaynet.openparcelbox.app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,57 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<application
android:label="OpenParcelBox"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
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"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</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>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package fr.zaynet.openparcelbox.app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -0,0 +1,21 @@
<?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">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#333333</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</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,22 @@
<?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
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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,21 @@
<?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">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#333333</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#333333</color>
</resources>
@@ -0,0 +1,22 @@
<?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
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+36
View File
@@ -0,0 +1,36 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
// mobile_scanner 7.2.1 detects AGP 9, while Flutter currently opts this
// project out of AGP's built-in Kotlin support. Apply the legacy Kotlin
// plugin explicitly so the package's Kotlin DSL remains available.
if (name in setOf("file_picker", "mobile_scanner", "nfc_manager")) {
pluginManager.apply("org.jetbrains.kotlin.android")
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(
org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
)
}
}
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
kotlin.incremental=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+157
View File
@@ -0,0 +1,157 @@
import 'package:flutter/widgets.dart';
class AppStrings {
AppStrings(this.locale);
final Locale locale;
bool get isFrench => locale.languageCode == 'fr';
String text(String key) => (_values[key] ?? const <String, String>{})[
isFrench ? 'fr' : 'en'] ??
key;
static const Map<String, Map<String, String>> _values =
<String, Map<String, String>>{
'add_box': {
'fr': 'Ajouter une nouvelle OpenParcelBox',
'en': 'Add a new OpenParcelBox',
},
'restore_backup': {
'fr': 'Restaurer une sauvegarde',
'en': 'Restore a backup',
},
'open': {'fr': 'Ouvrir', 'en': 'Open'},
'history': {'fr': 'Historique', 'en': 'History'},
'permanent_codes': {
'fr': 'Codes permanents',
'en': 'Permanent codes',
},
'temporary_codes': {
'fr': 'Codes temporaires',
'en': 'Temporary codes',
},
'nfc_tags': {'fr': 'Tags NFC', 'en': 'NFC tags'},
'guests': {'fr': 'Invités', 'en': 'Guests'},
'settings': {'fr': 'Paramètres', 'en': 'Settings'},
'close': {'fr': 'Fermer', 'en': 'Close'},
'add': {'fr': 'Ajouter', 'en': 'Add'},
'delete': {'fr': 'Supprimer', 'en': 'Delete'},
'edit': {'fr': 'Modifier', 'en': 'Edit'},
'cancel': {'fr': 'Annuler', 'en': 'Cancel'},
'save': {'fr': 'Enregistrer', 'en': 'Save'},
'scan': {'fr': 'Scanner', 'en': 'Scan'},
'scan_qr': {
'fr': 'Scanner un QR code invité',
'en': 'Scan a guest QR code',
},
'box_name': {
'fr': 'Identifiant de la box',
'en': 'Box identifier',
},
'nearby_boxes': {
'fr': 'OpenParcelBox détectées',
'en': 'Detected OpenParcelBox devices',
},
'search': {'fr': 'Rechercher', 'en': 'Search'},
'no_results': {
'fr': 'Aucune OpenParcelBox détectée.',
'en': 'No OpenParcelBox detected.',
},
'connected': {'fr': 'Connectée', 'en': 'Connected'},
'disconnected': {'fr': 'Déconnectée', 'en': 'Disconnected'},
'connecting': {'fr': 'Connexion…', 'en': 'Connecting…'},
'no_code': {'fr': 'Aucun code.', 'en': 'No code.'},
'new_code': {'fr': 'Générer un code', 'en': 'Generate a code'},
'code_limit': {
'fr': 'La limite de codes est atteinte.',
'en': 'The code limit has been reached.',
},
'no_history': {
'fr': 'Aucune ouverture enregistrée.',
'en': 'No opening has been recorded.',
},
'opened_by_permanent': {
'fr': 'Code permanent',
'en': 'Permanent code',
},
'opened_by_temporary': {
'fr': 'Code temporaire',
'en': 'Temporary code',
},
'opened_by_nfc': {'fr': 'Tag NFC', 'en': 'NFC tag'},
'opened_by_app': {'fr': 'Application', 'en': 'Application'},
'tag_name': {'fr': 'Nom du tag', 'en': 'Tag name'},
'tag_uid': {'fr': 'UID du tag', 'en': 'Tag UID'},
'scan_phone_nfc': {
'fr': 'Scanner avec le téléphone',
'en': 'Scan with phone',
},
'pair_on_box': {
'fr': 'Appairer sur la box',
'en': 'Pair on the box',
},
'manual_uid': {'fr': 'Ajouter un UID', 'en': 'Add a UID'},
'nfc_unavailable': {
'fr': 'Le NFC est indisponible sur ce téléphone.',
'en': 'NFC is unavailable on this phone.',
},
'hold_tag': {
'fr': 'Approchez un tag NFC du téléphone…',
'en': 'Hold an NFC tag near the phone…',
},
'guest_name': {'fr': "Nom de l'invité", 'en': 'Guest name'},
'show_qr': {'fr': 'Voir le QR code', 'en': 'Show QR code'},
'no_guest': {'fr': 'Aucun invité.', 'en': 'No guest.'},
'language': {'fr': 'Langue', 'en': 'Language'},
'automatic': {'fr': 'Automatique', 'en': 'Automatic'},
'french': {'fr': 'Français', 'en': 'French'},
'english': {'fr': 'Anglais', 'en': 'English'},
'backup': {
'fr': "Sauvegarder les paramètres de l'app",
'en': 'Back up app settings',
},
'backup_location': {
'fr': "Choisir l'emplacement de sauvegarde",
'en': 'Choose backup location',
},
'restore': {
'fr': 'Restaurer une sauvegarde',
'en': 'Restore a backup',
},
'password': {'fr': 'Mot de passe', 'en': 'Password'},
'firmware_update': {
'fr': 'Mise à jour du firmware (à venir)',
'en': 'Firmware update (coming later)',
},
'factory_reset': {
'fr': "Réinitialiser la box",
'en': 'Factory-reset the box',
},
'forget_box': {'fr': 'Supprimer la box', 'en': 'Forget the box'},
'danger': {'fr': 'Danger', 'en': 'Danger'},
'factory_warning': {
'fr':
"Cette action supprime tous les paramètres de la box et autorise l'appairage d'un nouvel administrateur.",
'en':
'This deletes every setting on the box and allows a new administrator to pair.',
},
'forget_warning': {
'fr':
"Seules les informations de l'application seront supprimées. Les paramètres de la box restent inchangés et un administrateur peut perdre définitivement l'accès.",
'en':
'Only app information will be deleted. Box settings remain unchanged and an administrator may permanently lose access.',
},
'confirm': {'fr': 'Confirmer', 'en': 'Confirm'},
'administrator': {'fr': 'Administrateur', 'en': 'Administrator'},
'guest': {'fr': 'Invité', 'en': 'Guest'},
'backup_done': {
'fr': 'Sauvegarde chiffrée créée.',
'en': 'Encrypted backup created.',
},
'restore_done': {
'fr': 'Sauvegarde restaurée.',
'en': 'Backup restored.',
},
};
}
+108
View File
@@ -0,0 +1,108 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
import 'package:file_picker/file_picker.dart';
import 'models.dart';
class BackupService {
static const _format = 'openparcelbox-backup-v1';
final Random _random = Random.secure();
Future<bool> exportBoxes(
List<SavedBox> boxes,
String password, {
String? initialDirectory,
}) async {
if (password.isEmpty) {
throw ArgumentError('A backup password is required.');
}
final salt = _randomBytes(16);
final nonce = _randomBytes(12);
final key = await _deriveKey(password, salt);
final clearText = utf8.encode(
jsonEncode(<String, Object?>{
'format': _format,
'boxes': boxes.map((box) => box.toJson()).toList(),
}),
);
final secretBox = await AesGcm.with256bits().encrypt(
clearText,
secretKey: key,
nonce: nonce,
);
final envelope = utf8.encode(
jsonEncode(<String, Object?>{
'format': _format,
'salt': base64Encode(salt),
'nonce': base64Encode(secretBox.nonce),
'ciphertext': base64Encode(secretBox.cipherText),
'mac': base64Encode(secretBox.mac.bytes),
}),
);
final path = await FilePicker.saveFile(
dialogTitle: 'OpenParcelBox backup',
fileName: 'openparcelbox-backup.opb',
initialDirectory: initialDirectory,
type: FileType.custom,
allowedExtensions: const <String>['opb'],
bytes: Uint8List.fromList(envelope),
);
return path != null;
}
Future<Uint8List?> pickBackupFile() async {
final result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: const <String>['opb'],
withData: true,
);
return result?.files.single.bytes;
}
Future<List<SavedBox>> importBoxes(
Uint8List bytes,
String password,
) async {
if (password.isEmpty) {
throw ArgumentError('A backup password is required.');
}
final envelope = jsonDecode(utf8.decode(bytes));
if (envelope is! Map<String, Object?> || envelope['format'] != _format) {
throw const FormatException('Unsupported OpenParcelBox backup.');
}
final salt = base64Decode(envelope['salt']! as String);
final nonce = base64Decode(envelope['nonce']! as String);
final cipherText = base64Decode(envelope['ciphertext']! as String);
final mac = Mac(base64Decode(envelope['mac']! as String));
final clearText = await AesGcm.with256bits().decrypt(
SecretBox(cipherText, nonce: nonce, mac: mac),
secretKey: await _deriveKey(password, salt),
);
final decoded = jsonDecode(utf8.decode(clearText));
if (decoded is! Map<String, Object?> || decoded['format'] != _format) {
throw const FormatException('Invalid OpenParcelBox backup.');
}
final boxes = decoded['boxes'];
if (boxes is! List<Object?>) {
throw const FormatException('The backup does not contain boxes.');
}
return boxes
.whereType<Map<String, dynamic>>()
.map((item) => SavedBox.fromJson(item.cast<String, Object?>()))
.toList();
}
Future<SecretKey> _deriveKey(String password, List<int> salt) {
return Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: 210000,
bits: 256,
).deriveKey(secretKey: SecretKey(utf8.encode(password)), nonce: salt);
}
Uint8List _randomBytes(int length) =>
Uint8List.fromList(List<int>.generate(length, (_) => _random.nextInt(256)));
}
+491
View File
@@ -0,0 +1,491 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'models.dart';
import 'secure_box_store.dart';
const _serviceUuid = 'f2a00000-8e7a-4f8d-9b1d-7d8e4b7a0001';
const _commandUuid = 'f2a00001-8e7a-4f8d-9b1d-7d8e4b7a0001';
const _stateUuid = 'f2a00002-8e7a-4f8d-9b1d-7d8e4b7a0001';
class OpenParcelBoxCommand {
const OpenParcelBoxCommand(this.name, [this.payload = const {}]);
final String name;
final Map<String, Object?> payload;
List<int> encode([String? identityKey]) => utf8.encode(
jsonEncode(<String, Object?>{
'command': name,
...payload,
'identity_key': ?identityKey,
}),
);
}
class OpenParcelBoxBleController extends ChangeNotifier {
OpenParcelBoxBleController({SecureBoxStore? store})
: store = store ?? SecureBoxStore();
final SecureBoxStore store;
final List<SavedBox> savedBoxes = <SavedBox>[];
final List<ScanResult> scanResults = <ScanResult>[];
final List<AccessCode> permanentCodes = <AccessCode>[];
final List<AccessCode> oneTimeCodes = <AccessCode>[];
final List<NfcTag> nfcTags = <NfcTag>[];
final List<OpeningEvent> history = <OpeningEvent>[];
final List<GuestIdentity> guests = <GuestIdentity>[];
SavedBox? currentBox;
BluetoothDevice? connectedDevice;
BluetoothCharacteristic? _commandCharacteristic;
BluetoothCharacteristic? _stateCharacteristic;
StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
bool initialized = false;
bool isScanning = false;
bool isBusy = false;
bool isAuthenticated = false;
String status = 'disconnected';
String? errorMessage;
bool get hasBox => savedBoxes.isNotEmpty;
bool get isConnected =>
connectedDevice != null && _commandCharacteristic != null;
bool get isAdministrator =>
currentBox?.role == BoxRole.administrator && isAuthenticated;
Future<void> initialize() async {
try {
savedBoxes
..clear()
..addAll(await store.loadBoxes());
if (savedBoxes.isNotEmpty) {
currentBox = savedBoxes.first;
await connectSavedBox();
}
} catch (error) {
errorMessage = '$error';
} finally {
initialized = true;
notifyListeners();
}
}
Future<void> startScan() async {
_setBusy(true);
try {
scanResults.clear();
errorMessage = null;
status = 'scanning';
if (!await FlutterBluePlus.isSupported) {
throw StateError('Bluetooth is not supported on this phone.');
}
_scanSubscription ??= FlutterBluePlus.scanResults.listen((results) {
scanResults
..clear()
..addAll(results.where(_looksLikeOpenParcelBox));
notifyListeners();
});
await FlutterBluePlus.adapterState
.where((state) => state == BluetoothAdapterState.on)
.first
.timeout(const Duration(seconds: 8));
isScanning = true;
notifyListeners();
await FlutterBluePlus.startScan(
withServices: <Guid>[Guid(_serviceUuid)],
timeout: const Duration(seconds: 8),
);
} catch (error) {
errorMessage = '$error';
} finally {
isScanning = false;
status = isConnected ? 'connected' : 'disconnected';
_setBusy(false);
}
}
Future<void> registerAdministrator(ScanResult result, String boxName) async {
final cleanedName = boxName.trim();
if (cleanedName.isEmpty) {
throw ArgumentError('A box identifier is required.');
}
_setBusy(true);
try {
await _connectDevice(result.device);
final publicState = await _readState(updateCollections: false);
if (publicState?['admin_exists'] == true) {
throw StateError('This box already has an administrator.');
}
final key = _generateIdentityKey();
await _write(
OpenParcelBoxCommand('provision_admin', <String, Object?>{
'identity_key': key,
'name': 'Administrator',
'box_name': cleanedName,
}),
authenticate: false,
);
final saved = SavedBox(
remoteId: result.device.remoteId.str,
name: cleanedName,
identityKey: key,
role: BoxRole.administrator,
);
await _upsertSavedBox(saved);
currentBox = saved;
isAuthenticated = true;
await syncClock();
} finally {
_setBusy(false);
}
}
Future<void> importGuestInvitation(String rawPayload) async {
final decoded = jsonDecode(rawPayload);
if (decoded is! Map<String, dynamic> ||
decoded['format'] != 'openparcelbox-invite-v1' ||
decoded['remote_id'] is! String ||
decoded['box_name'] is! String ||
decoded['guest_key'] is! String) {
throw const FormatException('Invalid OpenParcelBox invitation.');
}
final saved = SavedBox(
remoteId: decoded['remote_id'] as String,
name: decoded['box_name'] as String,
identityKey: decoded['guest_key'] as String,
role: BoxRole.guest,
);
await _upsertSavedBox(saved);
currentBox = saved;
await connectSavedBox();
}
Future<void> connectSavedBox() async {
final box = currentBox;
if (box == null) {
return;
}
_setBusy(true);
try {
await _connectDevice(BluetoothDevice.fromId(box.remoteId));
await _write(const OpenParcelBoxCommand('authenticate'));
isAuthenticated = true;
await syncClock();
} catch (error) {
isAuthenticated = false;
errorMessage = '$error';
status = 'disconnected';
} finally {
_setBusy(false);
}
}
Future<void> _connectDevice(BluetoothDevice device) async {
status = 'connecting';
errorMessage = null;
notifyListeners();
await device.connect(
license: License.nonprofit,
timeout: const Duration(seconds: 12),
);
_connectionSubscription?.cancel();
_connectionSubscription = device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
connectedDevice = null;
_commandCharacteristic = null;
_stateCharacteristic = null;
isAuthenticated = false;
status = 'disconnected';
notifyListeners();
}
});
final services = await device.discoverServices();
_commandCharacteristic = _findCharacteristic(services, _commandUuid);
_stateCharacteristic = _findCharacteristic(services, _stateUuid);
if (_commandCharacteristic == null || _stateCharacteristic == null) {
throw StateError('OpenParcelBox BLE characteristics were not found.');
}
connectedDevice = device;
status = 'connected';
}
BluetoothCharacteristic? _findCharacteristic(
List<BluetoothService> services,
String uuid,
) {
for (final service in services) {
if (service.uuid.str.toLowerCase() != _serviceUuid) {
continue;
}
for (final characteristic in service.characteristics) {
if (characteristic.uuid.str.toLowerCase() == uuid) {
return characteristic;
}
}
}
return null;
}
bool _looksLikeOpenParcelBox(ScanResult result) {
final name = result.advertisementData.advName.isNotEmpty
? result.advertisementData.advName
: result.device.platformName;
return name.toUpperCase().startsWith('OPB-') ||
name.toLowerCase().contains('openparcelbox') ||
result.advertisementData.serviceUuids.any(
(uuid) => uuid.str.toLowerCase() == _serviceUuid,
);
}
Future<void> syncClock() async {
final now = DateTime.now();
await send(
OpenParcelBoxCommand('sync_clock', <String, Object?>{
'unix_ms': now.toUtc().millisecondsSinceEpoch,
'timezone_offset_minutes': now.timeZoneOffset.inMinutes,
}),
);
}
Future<void> openLock() => send(const OpenParcelBoxCommand('open_lock'));
Future<void> addGeneratedCode(CredentialKind kind) => send(
OpenParcelBoxCommand('add_code', <String, Object?>{
'code': _generateCode(),
'kind': kind == CredentialKind.oneTime ? 'one_time' : 'permanent',
}),
);
Future<void> removeCode(AccessCode code) => send(
OpenParcelBoxCommand('remove_code', <String, Object?>{'code': code.code}),
);
Future<void> addNfcTag(String uid, String name) => send(
OpenParcelBoxCommand('add_nfc_tag', <String, Object?>{
'uid': uid.trim(),
'name': name.trim(),
}),
);
Future<void> startNfcEnrollment(String name) => send(
OpenParcelBoxCommand('start_nfc_enrollment', <String, Object?>{
'name': name.trim(),
}),
);
Future<void> removeNfcTag(NfcTag tag) => send(
OpenParcelBoxCommand('remove_nfc_tag', <String, Object?>{'uid': tag.uid}),
);
Future<String> addGuest(String name) async {
final key = _generateIdentityKey();
await send(
OpenParcelBoxCommand('add_guest', <String, Object?>{
'name': name.trim(),
'guest_key': key,
}),
);
return key;
}
Future<void> removeGuest(GuestIdentity guest) => send(
OpenParcelBoxCommand('remove_guest', <String, Object?>{
'guest_key': guest.key,
}),
);
String invitationPayload(GuestIdentity guest) => jsonEncode(<String, Object?>{
'format': 'openparcelbox-invite-v1',
'remote_id': currentBox!.remoteId,
'box_name': currentBox!.name,
'guest_name': guest.name,
'guest_key': guest.key,
});
Future<void> factoryReset() async {
await send(const OpenParcelBoxCommand('factory_reset'), refresh: false);
await forgetCurrentBox();
}
Future<void> forgetCurrentBox() async {
final box = currentBox;
if (box == null) {
return;
}
savedBoxes.removeWhere((item) => item.remoteId == box.remoteId);
await store.saveBoxes(savedBoxes);
await connectedDevice?.disconnect();
currentBox = savedBoxes.firstOrNull;
_clearState();
notifyListeners();
}
Future<void> replaceSavedBoxes(List<SavedBox> boxes) async {
savedBoxes
..clear()
..addAll(boxes);
await store.saveBoxes(savedBoxes);
currentBox = savedBoxes.firstOrNull;
notifyListeners();
if (currentBox != null) {
await connectSavedBox();
}
}
Future<void> send(OpenParcelBoxCommand command, {bool refresh = true}) async {
_setBusy(true);
try {
await _write(command);
if (refresh) {
await _readState();
}
} catch (error) {
errorMessage = '$error';
rethrow;
} finally {
_setBusy(false);
}
}
Future<void> _write(
OpenParcelBoxCommand command, {
bool authenticate = true,
}) async {
final characteristic = _commandCharacteristic;
if (characteristic == null) {
throw StateError('The box is not connected.');
}
final key = authenticate ? currentBox?.identityKey : null;
if (authenticate && key == null) {
throw StateError('No secure identity is available.');
}
await characteristic.write(command.encode(key), withoutResponse: false);
}
Future<Map<String, dynamic>?> _readState({
bool updateCollections = true,
}) async {
final raw = await _stateCharacteristic?.read();
if (raw == null) {
return null;
}
final decoded = jsonDecode(utf8.decode(raw));
if (decoded is! Map<String, dynamic>) {
return null;
}
if (updateCollections) {
_applyState(decoded);
}
return decoded;
}
void _applyState(Map<String, dynamic> state) {
permanentCodes.clear();
oneTimeCodes.clear();
nfcTags.clear();
history.clear();
guests.clear();
for (final item in (state['codes'] as List<dynamic>? ?? const [])) {
if (item is! Map<String, dynamic> ||
item['code'] is! String ||
item['kind'] is! String) {
continue;
}
final kind = item['kind'] == 'one_time'
? CredentialKind.oneTime
: CredentialKind.permanent;
final code = AccessCode(
id: 'slot-${item['slot']}',
code: item['code'] as String,
kind: kind,
);
(kind == CredentialKind.oneTime ? oneTimeCodes : permanentCodes).add(
code,
);
}
for (final item in (state['nfc_tags'] as List<dynamic>? ?? const [])) {
if (item is Map<String, dynamic> && item['uid'] is String) {
nfcTags.add(
NfcTag(
id: 'slot-${item['slot']}',
uid: item['uid'] as String,
name: item['name'] as String? ?? '',
),
);
}
}
for (final item in (state['history'] as List<dynamic>? ?? const [])) {
if (item is Map<String, dynamic> && item['unix_ms'] is int) {
history.add(
OpeningEvent(
date: DateTime.fromMillisecondsSinceEpoch(
item['unix_ms'] as int,
isUtc: true,
).toLocal(),
kind: item['kind'] as int? ?? 0,
actor: item['actor'] as String? ?? '',
),
);
}
}
for (final item in (state['guests'] as List<dynamic>? ?? const [])) {
if (item is Map<String, dynamic> &&
item['name'] is String &&
item['key'] is String) {
guests.add(
GuestIdentity(
name: item['name'] as String,
key: item['key'] as String,
),
);
}
}
notifyListeners();
}
Future<void> _upsertSavedBox(SavedBox box) async {
savedBoxes.removeWhere((item) => item.remoteId == box.remoteId);
savedBoxes.insert(0, box);
await store.saveBoxes(savedBoxes);
}
void _clearState() {
connectedDevice = null;
_commandCharacteristic = null;
_stateCharacteristic = null;
isAuthenticated = false;
permanentCodes.clear();
oneTimeCodes.clear();
nfcTags.clear();
history.clear();
guests.clear();
status = 'disconnected';
}
String _generateCode() =>
List<int>.generate(6, (_) => Random.secure().nextInt(10)).join();
String _generateIdentityKey() =>
List<int>.generate(16, (_) => Random.secure().nextInt(256))
.map((value) => value.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
void _setBusy(bool value) {
isBusy = value;
notifyListeners();
}
@override
void dispose() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
super.dispose();
}
}
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
enum BoxRole { administrator, guest }
enum CredentialKind { permanent, oneTime }
class SavedBox {
const SavedBox({
required this.remoteId,
required this.name,
required this.identityKey,
required this.role,
});
final String remoteId;
final String name;
final String identityKey;
final BoxRole role;
Map<String, Object?> toJson() => <String, Object?>{
'remote_id': remoteId,
'name': name,
'identity_key': identityKey,
'role': role.name,
};
factory SavedBox.fromJson(Map<String, Object?> json) => SavedBox(
remoteId: json['remote_id']! as String,
name: json['name']! as String,
identityKey: json['identity_key']! as String,
role: json['role'] == BoxRole.guest.name
? BoxRole.guest
: BoxRole.administrator,
);
}
class AccessCode {
const AccessCode({
required this.id,
required this.code,
required this.kind,
});
final String id;
final String code;
final CredentialKind kind;
}
class NfcTag {
const NfcTag({required this.id, required this.uid, required this.name});
final String id;
final String uid;
final String name;
}
class OpeningEvent {
const OpeningEvent({
required this.date,
required this.kind,
required this.actor,
});
final DateTime date;
final int kind;
final String actor;
}
class GuestIdentity {
const GuestIdentity({required this.name, required this.key});
final String name;
final String key;
}
+51
View File
@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'models.dart';
class SecureBoxStore {
SecureBoxStore({FlutterSecureStorage? storage})
: _storage = storage ?? const FlutterSecureStorage();
static const _boxesKey = 'opb.saved-boxes.v2';
static const _languageKey = 'opb.language';
static const _backupDirectoryKey = 'opb.backup-directory';
final FlutterSecureStorage _storage;
Future<List<SavedBox>> loadBoxes() async {
final raw = await _storage.read(key: _boxesKey);
if (raw == null || raw.isEmpty) {
return <SavedBox>[];
}
final decoded = jsonDecode(raw);
if (decoded is! List<Object?>) {
return <SavedBox>[];
}
return decoded
.whereType<Map<String, dynamic>>()
.map((item) => SavedBox.fromJson(item.cast<String, Object?>()))
.toList();
}
Future<void> saveBoxes(List<SavedBox> boxes) => _storage.write(
key: _boxesKey,
value: jsonEncode(boxes.map((box) => box.toJson()).toList()),
);
Future<String?> loadLanguage() => _storage.read(key: _languageKey);
Future<void> saveLanguage(String? language) async {
if (language == null) {
await _storage.delete(key: _languageKey);
} else {
await _storage.write(key: _languageKey, value: language);
}
}
Future<String?> loadBackupDirectory() =>
_storage.read(key: _backupDirectoryKey);
Future<void> saveBackupDirectory(String path) =>
_storage.write(key: _backupDirectoryKey, value: path);
}
+743
View File
@@ -0,0 +1,743 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
bluez:
dependency: transitive
description:
name: bluez
sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545"
url: "https://pub.dev"
source: hosted
version: "0.8.3"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cryptography:
dependency: "direct main"
description:
name: cryptography
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dbus:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
version: "0.7.14"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387
url: "https://pub.dev"
source: hosted
version: "11.0.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_blue_plus:
dependency: "direct main"
description:
name: flutter_blue_plus
sha256: "8b2e2f5870539e2d2a8a6473557455b990b87bc0cd86e7224bb3e20ee2eb00df"
url: "https://pub.dev"
source: hosted
version: "2.3.10"
flutter_blue_plus_android:
dependency: transitive
description:
name: flutter_blue_plus_android
sha256: "5f1db477d442974c516196718e2ab66e3601e9a2de271bddde9368c1a85e6e64"
url: "https://pub.dev"
source: hosted
version: "9.0.3"
flutter_blue_plus_darwin:
dependency: transitive
description:
name: flutter_blue_plus_darwin
sha256: bf41a4a07978b4c86a344c0c6e7388ff69f2b442a8d943b8da6d7f01a5c435bc
url: "https://pub.dev"
source: hosted
version: "9.0.3"
flutter_blue_plus_linux:
dependency: transitive
description:
name: flutter_blue_plus_linux
sha256: "79387947c27d04fce505916d168a1f8b7a89846d22d11a659970aba316459622"
url: "https://pub.dev"
source: hosted
version: "9.0.3"
flutter_blue_plus_platform_interface:
dependency: transitive
description:
name: flutter_blue_plus_platform_interface
sha256: "9378ed463673ab51e7ab72cf4bad3633b134182ca184ddcc598d6f7474ada993"
url: "https://pub.dev"
source: hosted
version: "9.0.3"
flutter_blue_plus_web:
dependency: transitive
description:
name: flutter_blue_plus_web
sha256: "62670fd0072e9424661170c3439eb2784679e0cb8420907ae2fe979aab8eed71"
url: "https://pub.dev"
source: hosted
version: "9.0.3"
flutter_blue_plus_winrt:
dependency: transitive
description:
name: flutter_blue_plus_winrt
sha256: "0000b2d818e6f79ad07764206fdd8afb69426fd44453c6254c35828fd16aa09f"
url: "https://pub.dev"
source: hosted
version: "0.0.20"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
image:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
intl:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "30f7dc342094eb257ead933572f7e442dfd9d8aa6f3fa5506c3206f7837d1615"
url: "https://pub.dev"
source: hosted
version: "7.2.1"
ndef_record:
dependency: transitive
description:
name: ndef_record
sha256: "210ffb12284961cab9e44b99462143316d9a20cd992581170706069ef77d74a6"
url: "https://pub.dev"
source: hosted
version: "1.4.2"
nfc_manager:
dependency: "direct main"
description:
name: nfc_manager
sha256: "1b8d3788ed71bb34f59878a445e433891eb2268353289515c1542b2780151fe6"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.38.4"
+114
View File
@@ -0,0 +1,114 @@
name: app
description: "OpenParcelBox mobile application."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.12.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
cryptography: ^2.9.0
file_picker: ^11.0.2
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8
flutter_blue_plus: 2.3.10
flutter_secure_storage: ^9.2.4
mobile_scanner: ^7.2.1
nfc_manager: ^4.2.1
qr_flutter: ^4.1.0
dev_dependencies:
flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.8
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- images/background.jpg
- images/openparcelbox-logo-256.png
flutter_launcher_icons:
android: true
ios: false
image_path: images/openparcelbox-logo-256.png
adaptive_icon_background: "#333333"
adaptive_icon_foreground: images/openparcelbox-logo-256.png
flutter_native_splash:
color: "#333333"
background_image: images/background.jpg
image: images/openparcelbox-logo-256.png
android: true
ios: false
android_12:
color: "#333333"
image: images/openparcelbox-logo-256.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+121
View File
@@ -0,0 +1,121 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app/backup_service.dart';
import 'package:app/main.dart';
import 'package:app/models.dart';
import 'package:app/secure_box_store.dart';
class _MemoryStore extends SecureBoxStore {
_MemoryStore({this.language = 'en'});
final String? language;
@override
Future<List<SavedBox>> loadBoxes() async => <SavedBox>[];
@override
Future<String?> loadLanguage() async => language;
@override
Future<void> saveBoxes(List<SavedBox> boxes) async {}
}
class _FakeBackupService extends BackupService {
bool fileWasPicked = false;
@override
Future<Uint8List?> pickBackupFile() async {
fileWasPicked = true;
return Uint8List(0);
}
@override
Future<List<SavedBox>> importBoxes(Uint8List bytes, String password) async =>
<SavedBox>[];
}
void main() {
testWidgets('empty home only exposes registration and restore actions', (
WidgetTester tester,
) async {
await tester.pumpWidget(MyApp(store: _MemoryStore()));
await tester.pumpAndSettle();
expect(find.text('Add a new OpenParcelBox'), findsOneWidget);
expect(find.text('Restore a backup'), findsOneWidget);
expect(find.byIcon(Icons.lock_open), findsNothing);
expect(find.text('Permanent codes'), findsNothing);
});
testWidgets('registration opens the styled setup modal', (
WidgetTester tester,
) async {
await tester.pumpWidget(MyApp(store: _MemoryStore()));
await tester.pumpAndSettle();
await tester.tap(find.text('Add a new OpenParcelBox'));
await tester.pumpAndSettle();
expect(find.text('Box identifier'), findsOneWidget);
expect(find.text('Scan a guest QR code'), findsOneWidget);
expect(find.text('Detected OpenParcelBox devices'), findsOneWidget);
expect(find.byIcon(Icons.close), findsOneWidget);
});
testWidgets('registration text field works with the French locale', (
WidgetTester tester,
) async {
await tester.pumpWidget(MyApp(store: _MemoryStore(language: 'fr')));
await tester.pumpAndSettle();
await tester.tap(find.text('Ajouter une nouvelle OpenParcelBox'));
await tester.pumpAndSettle();
expect(find.byType(TextFormField), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets(
'closing registration after editing does not dispose input early',
(WidgetTester tester) async {
await tester.pumpWidget(MyApp(store: _MemoryStore()));
await tester.pumpAndSettle();
await tester.tap(find.text('Add a new OpenParcelBox'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), 'Front gate');
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
expect(find.text('Add a new OpenParcelBox'), findsOneWidget);
expect(tester.takeException(), isNull);
},
);
testWidgets('restore picks a file before requesting its password', (
WidgetTester tester,
) async {
final backupService = _FakeBackupService();
await tester.pumpWidget(
MyApp(store: _MemoryStore(), backupService: backupService),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Restore a backup'));
await tester.pumpAndSettle();
expect(backupService.fileWasPicked, isTrue);
expect(find.text('Password'), findsNWidgets(2));
await tester.enterText(find.byType(TextField), 'secret');
await tester.tap(find.text('Confirm'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('Backup restored.'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}