@ -0,0 +1,284 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: linux-android-test
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: cirrusci/flutter:dev
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
commands:
|
||||
- mkdir ~/.ssh
|
||||
- echo $buildbot_key_b64 > ~/.ssh/id_rsa.b64
|
||||
- base64 -d ~/.ssh/id_rsa.b64 > ~/.ssh/id_rsa
|
||||
- chmod 400 ~/.ssh/id_rsa
|
||||
# force by pass of ssh host key check, less secure
|
||||
- ssh-keyscan -H git.openprivacy.ca >> ~/.ssh/known_hosts
|
||||
- git clone gogs@git.openprivacy.ca:flutter/flutter_app.git .
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: cirrusci/flutter:dev
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
- ./fetch-tor.sh
|
||||
- echo `git describe --tags` > VERSION
|
||||
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
|
||||
- flutter pub get
|
||||
- mkdir deploy
|
||||
- ./fetch-libcwtch-go.sh
|
||||
|
||||
#- name: quality
|
||||
# image: golang
|
||||
# volumes:
|
||||
# - name: deps
|
||||
# path: /go
|
||||
# commands:
|
||||
# - go list ./... | xargs go vet
|
||||
# - go list ./... | xargs golint
|
||||
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
|
||||
|
||||
- name: build-linux
|
||||
image: openpriv/flutter-desktop:linux-dev
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
- flutter build linux --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
|
||||
- mkdir deploy/linux
|
||||
- cp -r build/linux/x64/release/bundle/* deploy/linux
|
||||
- cp linux/cwtch.*.desktop deploy/linux
|
||||
- cp linux/install-*.sh deploy/linux
|
||||
- cp linux/cwtch.png deploy/linux
|
||||
- cp linux/libCwtch.so deploy/linux/lib/
|
||||
# should not be needed, should be in data/flutter_assets and work from there
|
||||
#- cp /sdks/flutter/bin/cache/artifacts/engine/linux-x64/icudtl.dat deploy/linux
|
||||
- cp linux/tor deploy/linux
|
||||
- cd deploy
|
||||
- mv linux cwtch
|
||||
- tar -czf cwtch-`cat ../VERSION`.tar.gz cwtch
|
||||
- rm -r cwtch
|
||||
|
||||
- name: test-build-android
|
||||
image: cirrusci/flutter:dev
|
||||
when:
|
||||
event: pull_request
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
- flutter build apk --debug
|
||||
|
||||
- name: build-android
|
||||
image: cirrusci/flutter:dev
|
||||
when:
|
||||
event: push
|
||||
environment:
|
||||
upload_jks_file_b64:
|
||||
from_secret: upload_jks_file_b64
|
||||
upload_jks_pass:
|
||||
from_secret: upload_jks_pass
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
- echo $upload_jks_file_b64 > upload-keystore.jks.b64
|
||||
- base64 -i --decode upload-keystore.jks.b64 > android/app/upload-keystore.jks
|
||||
- sed -i "s/%jks-password%/$upload_jks_pass/g" android/key.properties
|
||||
- flutter build appbundle --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
|
||||
# cant do debug for final release, this is just a stop gap
|
||||
- flutter build apk --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
|
||||
# or build apk --split-per-abi ?
|
||||
- cp build/app/outputs/bundle/release/app-release.aab deploy/
|
||||
- cp build/app/outputs/apk/release/app-release.apk deploy/
|
||||
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
|
||||
|
||||
- name: widget-tests
|
||||
image: cirrusci/flutter:dev
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
commands:
|
||||
# - flutter config --enable-linux-desktop
|
||||
- flutter test --coverage
|
||||
- genhtml coverage/lcov.info -o coverage/html
|
||||
|
||||
# Todo: gonna need more work on container
|
||||
# https://flutter.dev/desktop
|
||||
# requirements: Visual Studio 2019 (not to be confused with Visual Studio Code) with the โDesktop development with C++โ workload installed, including all of its default components
|
||||
#- name: build-windows
|
||||
# image: cirrusci/flutter:dev
|
||||
#- volumes:
|
||||
# - name: deps
|
||||
# path: /root/.pub-cache
|
||||
# commands:
|
||||
# - flutter config --enable-windows-desktop
|
||||
# - flutter build windows
|
||||
|
||||
- name: deploy-buildfiles
|
||||
image: kroniak/ssh-client
|
||||
environment:
|
||||
BUILDFILES_KEY:
|
||||
from_secret: buildfiles_key
|
||||
secrets: [gogs_account_token]
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
commands:
|
||||
- echo $BUILDFILES_KEY > ~/id_rsab64
|
||||
- base64 -d ~/id_rsab64 > ~/id_rsa
|
||||
- chmod 400 ~/id_rsa
|
||||
- export DIR=flwtch-`cat VERSION`-`cat BUILDDATE`
|
||||
- mv deploy $DIR
|
||||
- cp -r coverage/html $DIR/coverage-tests
|
||||
- cp -r test/failures $DIR/test-failures || true
|
||||
- cd $DIR
|
||||
- find . -type f -exec sha256sum {} \; > ./../sha256s.txt
|
||||
- mv ./../sha256s.txt .
|
||||
- cd ..
|
||||
# TODO: do deployment once files actaully compile
|
||||
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
|
||||
- name: notify-email
|
||||
image: drillster/drone-email
|
||||
settings:
|
||||
host: build.openprivacy.ca
|
||||
port: 25
|
||||
skip_verify: true
|
||||
from: drone@openprivacy.ca
|
||||
when:
|
||||
status: [ failure ]
|
||||
|
||||
- name: notify-gogs
|
||||
image: openpriv/drone-gogs
|
||||
when:
|
||||
event: pull_request
|
||||
status: [ success, changed, failure ]
|
||||
environment:
|
||||
GOGS_ACCOUNT_TOKEN:
|
||||
from_secret: gogs_account_token
|
||||
settings:
|
||||
gogs_url: https://git.openprivacy.ca
|
||||
|
||||
volumes:
|
||||
- name: deps
|
||||
temp: {}
|
||||
|
||||
trigger:
|
||||
repo: flutter/flutter_app
|
||||
branch: trunk
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: windows
|
||||
|
||||
platform:
|
||||
os: windows
|
||||
#arch: amd64
|
||||
version: 1809
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
commands:
|
||||
#- # force by pass of ssh host key check, less secure
|
||||
#- ssh-keyscan -H git.openprivacy.ca >> ..\known_hosts
|
||||
- echo $Env:buildbot_key_b64 > ..\id_rsa.b64
|
||||
- certutil -decode ..\id_rsa.b64 ..\id_rsa
|
||||
- git init
|
||||
# -o UserKnownHostsFile=../known_hosts
|
||||
- git config core.sshCommand 'ssh -o StrictHostKeyChecking=no -i ../id_rsa'
|
||||
- git remote add origin gogs@git.openprivacy.ca:flutter/flutter_app.git
|
||||
- git pull origin trunk
|
||||
- git fetch --tags
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
commands:
|
||||
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip"
|
||||
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }"
|
||||
- git describe --tags > VERSION
|
||||
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
|
||||
- .\fetch-libcwtch-go.ps1
|
||||
|
||||
- name: build-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
environment:
|
||||
pfx:
|
||||
from_secret: pfx
|
||||
pfx_pass:
|
||||
from_secret: pfx_pass
|
||||
commands:
|
||||
- move pubspec.yaml pubspec.yaml.orig
|
||||
- (Get-Content -path pubspec.yaml.orig -Raw) -Replace 'pfx_pass',"$Env:pfx_pass" | Set-Content -path pubspec.yaml
|
||||
- flutter pub get
|
||||
- $Env:version += type .\VERSION
|
||||
- $Env:builddate += type .\BUILDDATE
|
||||
- $Env:buildname = 'flwtch-win-' + $Env:version + '-' + $Env:builddate
|
||||
- $Env:builddir = $Env:buildname
|
||||
- $Env:zip = 'deploy\\' + $Env:builddir +'\\cwtch-' + $Env:version + '.zip'
|
||||
- $Env:zipsha = $Env:zip + '.sha512'
|
||||
- $Env:msix = 'cwtch-install-' + $Env:version + '.msix'
|
||||
- $Env:msixsha = $Env:msix + '.sha512'
|
||||
- $Env:releasedir = "build\\windows\\runner\\Release\\"
|
||||
- echo $Env:releasedir
|
||||
- echo $Env:builddir
|
||||
- echo $Env:zip
|
||||
- flutter build windows --dart-define BUILD_VER=$Env:version --dart-define BUILD_DATE=$Env:builddate
|
||||
- copy windows\libCwtch.dll $Env:releasedir
|
||||
# flutter hasn't worked out it's packaging of required dll's so we have to resort to this manual nonsense
|
||||
# https://github.com/google/flutter-desktop-embedding/issues/587
|
||||
# https://github.com/flutter/flutter/issues/53167
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
|
||||
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:releasedir\Tor"
|
||||
- dir $Env:releasedir
|
||||
- echo $Env:pfx > codesign.pfx.b64
|
||||
- certutil -decode codesign.pfx.b64 codesign.pfx
|
||||
- flutter pub run msix:create
|
||||
- mkdir deploy
|
||||
- mkdir deploy\$Env:builddir
|
||||
- dir deploy
|
||||
- powershell -command "move $Env:releasedir\cwtch.msix deploy\$Env:builddir\$Env:msix"
|
||||
- move $Env:releasedir $Env:builddir
|
||||
- powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath $Env:zip"
|
||||
#- powershell -command "move $Env:zip deploy\$Env:builddir\$Env:zip"
|
||||
#- powershell -command "(Get-FileHash $Env:zip -Algorithm sha512).Hash" > ${Env:zipsha}
|
||||
|
||||
- name: deploy-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
environment:
|
||||
BUILDFILES_KEY:
|
||||
from_secret: buildfiles_key
|
||||
commands:
|
||||
- echo $Env:BUILDFILES_KEY > id_rsab64
|
||||
- certutil -decode id_rsab64 id_rsa
|
||||
- scp -r -o StrictHostKeyChecking=no -i id_rsa deploy\\* buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
|
||||
trigger:
|
||||
repo: flutter/flutter_app
|
||||
branch: trunk
|
||||
event:
|
||||
- push
|
@ -0,0 +1,47 @@
|
||||
# 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
|
||||
|
||||
libCwtch.so
|
||||
android/cwtch/cwtch.aar
|
||||
coverage
|
||||
test/failures
|
||||
.gradle
|
@ -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: 78910062997c3a836feee883712c241a5fd22983
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
@ -0,0 +1,12 @@
|
||||
# State Management
|
||||
|
||||
We use a MultiProvider to distribute state to the underlying widgets. Right now there are 2 top
|
||||
level Providers: FlwtchState (the app) and OpaqueTheme.
|
||||
|
||||
## Theme
|
||||
|
||||
OpaqueTheme extends ChangeProvider. SetLight and SetDark are functions that call notifyListeners()
|
||||
|
||||
ChangeNotiferProvider is used to package OpaqueTheme into a provider which is a top level
|
||||
provider (as every widget in the app needs to be re-rendered on a theme switch).
|
||||
|
@ -0,0 +1 @@
|
||||
v0.0.2-108-g3964348-2021-06-24-17-42
|
@ -0,0 +1,71 @@
|
||||
# flwtch
|
||||
|
||||
A Flutter based Cwtch UI
|
||||
|
||||
## Getting Started
|
||||
|
||||
click the play button in android studio
|
||||
|
||||
### Linux
|
||||
|
||||
- libCwtch-go: required to be on the link path (linux/cwtch.destktop demonstrates with `env LD_LIBRARY_PATH=./lib/` on the front of the comman)
|
||||
- fetch-libcwtch-go.sh will fetch a prebuilt version
|
||||
- or compile from source from libcwtch-go with `make linux`
|
||||
- `tor` should be in the PATH
|
||||
|
||||
### Windows
|
||||
|
||||
- run `fetch-libcwtch-go.ps1` to get `libCwtch.dll` which is required to run
|
||||
- run `fetch-tor-win.ps1` to fetch Tor for windows
|
||||
|
||||
#### Issues
|
||||
|
||||
- Flutter engine has a [known bug](https://github.com/flutter/flutter/issues/75675) around the Right Shift key being sticky. We have implemented the mostly work around, but until it is fixed, right shift occasionally acts permenent. If this happens, just tap left shift and it will reset
|
||||
|
||||
## l10n
|
||||
|
||||
### Adding a new string
|
||||
|
||||
Strings are managed directly from our Lokalise(url?) project.
|
||||
Keys should be valid Dart variable names in lowerCamelCase.
|
||||
After adding a new key and providing/obtaining translations for it, follow the next step to update your local copy.
|
||||
|
||||
### Updating translations
|
||||
|
||||
Only Open Privacy staff members can update translations.
|
||||
|
||||
In Lokalise, hit Download and make sure:
|
||||
|
||||
* Format is set to "Flutter (.arb)
|
||||
* Output filename is set to `l10n/intl_%LANG_ISO%.%FORMAT%`
|
||||
* Empty translations is set to "Replace with base language"
|
||||
|
||||
Build, download and unzip the output, overwriting `lib/l10n`. The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).
|
||||
|
||||
### Adding a language
|
||||
|
||||
If a new language has been added to the Lokalise project, two additional manual steps need to be done:
|
||||
|
||||
* Create a new key called `localeXX` for the name of the language
|
||||
* Add it to the settings pane by updating `getLanguageFull()` in `lib/views/globalsettingsview.dart`
|
||||
|
||||
Then rebuild as normal.
|
||||
|
||||
### Using a string
|
||||
|
||||
Any widget underneath the main MaterialApp should be able to:
|
||||
|
||||
```
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
```
|
||||
|
||||
and then use:
|
||||
|
||||
```
|
||||
Text(AppLocalizations.of(context)!.stringIdentifer),
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
With `generate: true` in `pubspec.yaml`, the Flutter build process checks `l10n.yaml` for input/output filenames.
|
||||
|
@ -0,0 +1,144 @@
|
||||
# Specification
|
||||
|
||||
This document outlines the minimal functionality necessary for us to consider Flwtch the canonical
|
||||
Cwtch UI implementation.
|
||||
|
||||
This functionality is implemented in libCwtch and so this work captures just the UI work
|
||||
required - any new Cwtch work is beyond the scope of this initial spec.
|
||||
|
||||
# Functional Requirements
|
||||
- [ ] Kill all processes / isolates on exit (Blocked - P1)
|
||||
- [X] Android Service? (P1)
|
||||
|
||||
# Splash Screen
|
||||
- [X] Android
|
||||
- [X] Investigate Lottie [example implementation blog](https://medium.com/swlh/native-splash-screen-in-flutter-using-lottie-121ce2b9b0a4)
|
||||
- [ ] Desktop (P2)
|
||||
|
||||
# Custom Styled Widgets
|
||||
- [X] Label Widget
|
||||
- [X] Initial
|
||||
- [X] With Accessibility / Zoom Integration (P1)
|
||||
- [X] Text Field Widget
|
||||
- [X] Password Widget
|
||||
- [X] Text Button Widget (for Copy)
|
||||
|
||||
## Home Pane (formally Profile Pane)
|
||||
|
||||
- [X] Unlock a profile with a password
|
||||
- [X] Create a new Profile
|
||||
- [X] With a password
|
||||
- [X] Without a password
|
||||
- [X] Display all unlocked profiles
|
||||
- [X] Profile Picture
|
||||
- [X] default images
|
||||
- [ ] custom images (P3)
|
||||
- [X] coloured ring border (P2)
|
||||
- [X] Profile Name
|
||||
- [X] Edit Button
|
||||
- [X Unread messages badge (P2)
|
||||
- [X] Navigate to a specific Profile Contacts Pane (when clicking on a Profile row)
|
||||
- [X] Navigate to a specific Profile Management Pane (edit Button)
|
||||
- [X] Navigate to the Settings Pane (Settings Button in Action bar)
|
||||
|
||||
## Settings Pane
|
||||
- [X] Save/Load
|
||||
- [X] Switch Dark / Light Theme
|
||||
- [X] Switch Language
|
||||
- [X] Enable/Disable Experiments
|
||||
- [ ] Accessibility Settings (Zoom etc. - needs a deep dive into flutter) (P1)
|
||||
- [X] Display Build & Version Info
|
||||
- [X] Acknowledgements & Credits
|
||||
|
||||
## Profile Management Pane
|
||||
|
||||
- [X] Update Profile Name
|
||||
- [X] Update Profile Password
|
||||
- [X] Error Message When Attempting to Update Password with Wrong Old Password (P2)
|
||||
- [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile (P3)
|
||||
- [X] Delete a Profile (P2)
|
||||
- [X] Dialog Acknowledgement (P2)
|
||||
- [X] Require Old Password Gate (P2)
|
||||
- [X] Async Checking of Password (P2)
|
||||
- [X] Copy Profile Onion Address
|
||||
|
||||
## Profile Pane (formally Contacts Pane)
|
||||
|
||||
- [X] Display Profile-specific status
|
||||
- [X] Profile Name
|
||||
- [X] Online Status
|
||||
- [X] Add Contact Button Navigates to Add Contact Pane
|
||||
- [ ] Search Bar (P2)
|
||||
- [ ] Search by name
|
||||
- [ ] Search by Onion
|
||||
- [ ] Display all Peer Contacts
|
||||
- [X] Profile Picture
|
||||
- [X] Name
|
||||
- [X] Onion
|
||||
- [X] Online Status
|
||||
- [X] Unread Messages Badge (P1)
|
||||
- [X] In Order of Most Recent Message / Activity (P1)
|
||||
- [X] With Accept / Reject Heart/Trash Bin Option (P1)
|
||||
- [X] Separate list area for Blocked Contacts (P1)
|
||||
- [X] Display all Group Contacts (if experiment is enabled)
|
||||
- [X] Navigate to a specific Contact or Group Message Pane (Contact Row)
|
||||
- [X] Pressing Back should go back to the home pane
|
||||
|
||||
## Add Contact Pane
|
||||
- [X] Allowing Copying the Profile Onion Address for Sharing
|
||||
- [X] Allowing Pasting a Peer Onion Address for adding to Contacts
|
||||
- [ ] (with optional name field)
|
||||
- [X] Allowing Pasting a Group Invite / Server Address
|
||||
- [X] (if group experiment is enabled)
|
||||
|
||||
## Message Overlay
|
||||
|
||||
- [X] Display Messages from Contacts
|
||||
- [X] Allowing copying the text of a specific message (on mobile) (P2)
|
||||
- [X] Send a message to the specific Contact / Group
|
||||
- [~] Display the Acknowledgement status of a message (P1)
|
||||
- [X] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar)
|
||||
- [ ] Emoji Support (P1)
|
||||
- [ ] Display in-message emoji text labels e.g. `:label:` as emoji. (P1)
|
||||
- [ ] Functional Emoji Drawer Widget for Selection (P2)
|
||||
- [ ] Mutant Standard? (P2)
|
||||
- [X] Display a warning if Contact / Server is offline (Broken Heart) (P1)
|
||||
- [X] Display a warning for configuring peer history (P2)
|
||||
- [X] Pressing Back should go back to the contacts pane
|
||||
|
||||
## List Overlay (P3)
|
||||
|
||||
- [ ] Add Item to List (P3)
|
||||
- [ ] mark Item as Complete (P3)
|
||||
- [ ] Delete Item from List (P3)
|
||||
- [ ] Search List (P3)
|
||||
|
||||
## Bulletin Overlay (P4)
|
||||
|
||||
## Contact Settings Pane
|
||||
- [X] Update local name of contact
|
||||
- [X] Copy contact onion address
|
||||
- [X] Block/Unblock a contact
|
||||
- [X] Configure Peer History Saving
|
||||
- [X] Pressing Back should go back to the message pane
|
||||
|
||||
## Group Settings Pane (experimental - P3)
|
||||
- [X] Gated behind group experiment
|
||||
- [X] Update local name of group
|
||||
- [X] Get Group Invite
|
||||
- [X] Leave Group
|
||||
- [X] Pressing Back should go back to the message pane for the group
|
||||
|
||||
|
||||
|
||||
## Android Requirements Notes
|
||||
|
||||
What are our expectations here?
|
||||
|
||||
- Can we periodically check groups in the background to power notifications?
|
||||
- Either way we need networking in the service not the main/UI thread.
|
||||
- We probably don't want to and very likely can't persist tor connections to peers indefinitely.
|
||||
- Neither google nor apple are very tolerant of apps that try to create their own push message infrastructure.
|
||||
|
||||
- "Aside": Retrieving a CallbackHandle for a method from PluginUtilities.getCallbackHandle has the side effect of populating a callback cache within the Flutter engine, as seen in the diagram above. This cache maps information required to retrieve callbacks to raw integer handles, which are simply hashes calculated based on the properties of the callback. This cache persists across launches, but be aware that callback lookups may fail if the callback is renamed or moved and PluginUtilities.getCallbackHandle is not called for the updated callback.
|
||||
- The above seems to imply that there is a persistent cache somewhere that can affect code between launches...the ramifications of this are ?!?!
|
@ -0,0 +1,11 @@
|
||||
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
|
@ -0,0 +1,120 @@
|
||||
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 FileNotFoundException("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"
|
||||
|
||||
// key.properties MUST have password placeholders filled in (via drone with secrets) and cwtch-upload.jks file must be added (from drone secret)
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
main.jniLibs.srcDirs += 'src/main/libs'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "im.cwtch.flwtch"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
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
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation project(':cwtch')
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
|
||||
implementation "com.airbnb.android:lottie:3.5.0"
|
||||
implementation "com.android.support.constraint:constraint-layout:2.0.4"
|
||||
|
||||
// WorkManager
|
||||
|
||||
// (Java only)
|
||||
//implementation("androidx.work:work-runtime:$work_version")
|
||||
|
||||
// Kotlin + coroutines
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
|
||||
// optional - RxJava2 support
|
||||
//implementation("androidx.work:work-rxjava2:$work_version")
|
||||
|
||||
// optional - GCMNetworkManager support
|
||||
//implementation("androidx.work:work-gcm:$work_version")
|
||||
|
||||
// optional - Test helpers
|
||||
//androidTestImplementation("androidx.work:work-testing:$work_version")
|
||||
|
||||
// optional - Multiprocess support
|
||||
implementation "androidx.work:work-multiprocess:2.5.0"
|
||||
|
||||
// end of workmanager deps
|
||||
|
||||
// needed to prevent a ListenableFuture dependency conflict/bug
|
||||
// see https://github.com/google/ExoPlayer/issues/7905#issuecomment-692637059
|
||||
implementation 'com.google.guava:guava:any'
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.cwtch.flwtch">
|
||||
<!-- 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"/>
|
||||
</manifest>
|
@ -0,0 +1,51 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.cwtch.flwtch">
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
In most cases you can leave this as-is, but you if you want to provide
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="Cwtch"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/knott">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/NormalTheme"
|
||||
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>
|
||||
|
||||
<!--Needed to access Tor socket-->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!--Needed to run in background (lol)-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
</manifest>
|
@ -0,0 +1,46 @@
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import androidx.annotation.NonNull
|
||||
import android.content.Context
|
||||
|
||||
//import libCwtch.LibCwtch
|
||||
|
||||
/* References:
|
||||
more detailed kotlin / flutter method channel example:
|
||||
https://stablekernel.com/article/flutter-platform-channels-quick-start/
|
||||
|
||||
kotlin / flutter plugin:
|
||||
https://github.com/flutter/samples -- experimental/federated_plugin/federated_plugin
|
||||
*/
|
||||
/*
|
||||
class FederatedPlugin : FlutterPlugin, MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
private var context: Context? = null
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cwtch")
|
||||
channel.setMethodCallHandler(this)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
}
|
||||
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
when (call.method) {
|
||||
"Start" -> {
|
||||
val appDir = (call.arguments as? String) ?: "";
|
||||
val tor = (call.arguments as? String) ?: "tor";
|
||||
result.success(LibCwtch.Start(appDir, tor))
|
||||
?: result.error("Failed to start cwtch", "", null);
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
context = null
|
||||
}
|
||||
|
||||
}*/
|
@ -0,0 +1,266 @@
|
||||
package im.cwtch.flwtch
|
||||
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
import cwtch.Cwtch
|
||||
import io.flutter.FlutterInjector
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||
CoroutineWorker(context, parameters) {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as
|
||||
NotificationManager
|
||||
|
||||
private var notificationID: MutableMap<String, Int> = mutableMapOf()
|
||||
private var notificationIDnext: Int = 1
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val method = inputData.getString(KEY_METHOD)
|
||||
?: return Result.failure()
|
||||
val args = inputData.getString(KEY_ARGS)
|
||||
?: return Result.failure()
|
||||
// Mark the Worker as important
|
||||
val progress = "Cwtch is keeping Tor running in the background"//todo:translate
|
||||
setForeground(createForegroundInfo(progress))
|
||||
return handleCwtch(method, args)
|
||||
}
|
||||
|
||||
private fun getNotificationID(profile: String, contact: String): Int {
|
||||
val k = "$profile $contact"
|
||||
if (!notificationID.containsKey(k)) {
|
||||
notificationID[k] = notificationIDnext++
|
||||
}
|
||||
return notificationID[k] ?: -1
|
||||
}
|
||||
|
||||
private fun handleCwtch(method: String, args: String): Result {
|
||||
val a = JSONObject(args)
|
||||
when (method) {
|
||||
"Start" -> {
|
||||
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
|
||||
val appDir = (a.get("appDir") as? String) ?: ""
|
||||
val torPath = (a.get("torPath") as? String) ?: "tor"
|
||||
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
|
||||
|
||||
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
||||
|
||||
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
while(true) {
|
||||
Log.i("FlwtchWorker.kt", "while(true)getAppbusEvent()")
|
||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
||||
val data = JSONObject(evt.Data)
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer"))
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
|
||||
val loader = FlutterInjector.instance().flutterLoader()
|
||||
val key = loader.getLookupKeyForAsset("assets/"+data.getString("Picture"))//"assets/profiles/001-centaur.png")
|
||||
val fh = applicationContext.assets.open(key)
|
||||
|
||||
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
|
||||
intent.action = Intent.ACTION_RUN
|
||||
intent.putExtra("EventType", "NotificationClicked")
|
||||
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
|
||||
intent.putExtra("RemotePeer", if (evt.EventType == "NewMessageFromPeer") data.getString("RemotePeer") else data.getString("GroupID"))
|
||||
}
|
||||
|
||||
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(data.getString("Nick"))
|
||||
.setContentText("New message")//todo: translate
|
||||
.setLargeIcon(BitmapFactory.decodeStream(fh))
|
||||
.setSmallIcon(R.mipmap.knott)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification)
|
||||
}
|
||||
|
||||
Intent().also { intent ->
|
||||
intent.action = "im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS"
|
||||
intent.putExtra("EventType", evt.EventType)
|
||||
intent.putExtra("Data", evt.Data)
|
||||
intent.putExtra("EventID", evt.EventID)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
"ReconnectCwtchForeground" -> {
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
"CreateProfile" -> {
|
||||
val nick = (a.get("nick") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.createProfile(nick, pass)
|
||||
}
|
||||
"LoadProfiles" -> {
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.loadProfiles(pass)
|
||||
}
|
||||
"GetMessage" -> {
|
||||
val profile = (a.get("profile") as? String) ?: ""
|
||||
val handle = (a.get("contact") as? String) ?: ""
|
||||
val indexI = a.getInt("index")
|
||||
Log.i("FlwtchWorker.kt", "indexI = $indexI")
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build())
|
||||
}
|
||||
"UpdateMessageFlags" -> {
|
||||
val profile = (a.get("profile") as? String) ?: ""
|
||||
val handle = (a.get("contact") as? String) ?: ""
|
||||
val midx = (a.get("midx") as? Long) ?: 0
|
||||
val flags = (a.get("flags") as? Long) ?: 0
|
||||
Cwtch.updateMessageFlags(profile, handle, midx, flags)
|
||||
}
|
||||
"AcceptContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
Cwtch.acceptContact(profile, handle)
|
||||
}
|
||||
"BlockContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
Cwtch.blockContact(profile, handle)
|
||||
}
|
||||
"SendMessage" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
val message = (a.get("message") as? String) ?: ""
|
||||
Cwtch.sendMessage(profile, handle, message)
|
||||
}
|
||||
"SendInvitation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
val target = (a.get("target") as? String) ?: ""
|
||||
Cwtch.sendInvitation(profile, handle, target)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion = (a.get("onion") as? String) ?: ""
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendProfileEvent(onion, jsonEvent)
|
||||
}
|
||||
"SendAppEvent" -> {
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendAppEvent(jsonEvent)
|
||||
}
|
||||
"ResetTor" -> {
|
||||
Cwtch.resetTor()
|
||||
}
|
||||
"ImportBundle" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val bundle = (a.get("bundle") as? String) ?: ""
|
||||
Cwtch.importBundle(profile, bundle)
|
||||
}
|
||||
"SetGroupAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val groupHandle = (a.get("groupHandle") as? String) ?: ""
|
||||
val key = (a.get("key") as? String) ?: ""
|
||||
val value = (a.get("value") as? String) ?: ""
|
||||
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
|
||||
}
|
||||
"CreateGroup" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val server = (a.get("server") as? String) ?: ""
|
||||
val groupName = (a.get("groupname") as? String) ?: ""
|
||||
Cwtch.createGroup(profile, server, groupName)
|
||||
}
|
||||
"DeleteProfile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"LeaveConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val contactHandle = (a.get("contactHandle") as? String) ?: ""
|
||||
Cwtch.leaveConversation(profile, contactHandle)
|
||||
}
|
||||
"LeaveGroup" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val groupHandle = (a.get("groupHandle") as? String) ?: ""
|
||||
Cwtch.leaveGroup(profile, groupHandle)
|
||||
}
|
||||
"RejectInvite" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val groupHandle = (a.get("groupHandle") as? String) ?: ""
|
||||
Cwtch.rejectInvite(profile, groupHandle)
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
return Result.success()
|
||||
}
|
||||
else -> return Result.failure()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
// Creates an instance of ForegroundInfo which can be used to update the
|
||||
// ongoing notification.
|
||||
private fun createForegroundInfo(progress: String): ForegroundInfo {
|
||||
val id = "flwtch"
|
||||
val title = "Flwtch"
|
||||
val cancel = "Shut down"//todo: translate
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createForegroundNotificationChannel(id, id)
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
|
||||
// This PendingIntent can be used to cancel the worker
|
||||
val intent = WorkManager.getInstance(applicationContext)
|
||||
.createCancelPendingIntent(getId())
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(title)
|
||||
.setTicker(title)
|
||||
.setContentText(progress)
|
||||
.setSmallIcon(R.mipmap.knott)
|
||||
.setOngoing(true)
|
||||
// Add the cancel action to the notification which can
|
||||
// be used to cancel the worker
|
||||
.addAction(android.R.drawable.ic_delete, cancel, intent)
|
||||
.build()
|
||||
|
||||
return ForegroundInfo(101, notification)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createForegroundNotificationChannel(channelId: String, channelName: String): String{
|
||||
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
|
||||
chan.lightColor = Color.MAGENTA
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
notificationManager.createNotificationChannel(chan)
|
||||
return channelId
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createMessageNotificationChannel(channelId: String, channelName: String): String{
|
||||
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
|
||||
chan.lightColor = Color.MAGENTA
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
notificationManager.createNotificationChannel(chan)
|
||||
return channelId
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_METHOD = "KEY_METHOD"
|
||||
const val KEY_ARGS = "KEY_ARGS"
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
package im.cwtch.flwtch
|
||||
|
||||
import SplashView
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.annotation.NonNull
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.view.Window
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
|
||||
import io.flutter.embedding.android.SplashScreen
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
||||
|
||||
// Channel to get app info
|
||||
private val CHANNEL_APP_INFO = "test.flutter.dev/applicationInfo"
|
||||
private val CALL_APP_INFO = "getNativeLibDir"
|
||||
|
||||
// Channel to get cwtch api calls on
|
||||
private val CHANNEL_CWTCH = "cwtch"
|
||||
|
||||
// Channel to send eventbus events on
|
||||
private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus"
|
||||
|
||||
// Channel to trigger contactview when an external notification is clicked
|
||||
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
|
||||
|
||||
// WorkManager tag applied to all Start() infinite coroutines
|
||||
val WORKER_TAG = "cwtchEventBusWorker"
|
||||
|
||||
private var myReceiver: MyBroadcastReceiver? = null
|
||||
private var methodChan: MethodChannel? = null
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if (methodChan == null || intent.extras == null) return
|
||||
if (!intent.extras!!.containsKey("ProfileOnion") || !intent.extras!!.containsKey("RemotePeer")) {
|
||||
Log.i("onNewIntent", "got intent with no onions")
|
||||
return
|
||||
}
|
||||
val profile = intent.extras!!.getString("ProfileOnion")
|
||||
val handle = intent.extras!!.getString("RemotePeer")
|
||||
val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle)
|
||||
val j = JSONObject(mappo)
|
||||
methodChan!!.invokeMethod("NotificationClicked", j.toString())
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
// Note: this methods are invoked on the main thread.
|
||||
//note to self: ask someone if this does anything ^ea
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) }
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
|
||||
methodChan = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
|
||||
}
|
||||
|
||||
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
when (call.method) {
|
||||
CALL_APP_INFO -> result.success(getNativeLibDir())
|
||||
?: result.error("Unavailable", "nativeLibDir not available", null);
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNativeLibDir(): String {
|
||||
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
|
||||
"im.cwtch.flwtch", // Must be app name
|
||||
PackageManager.GET_SHARED_LIBRARY_FILES)
|
||||
return ainfo.nativeLibraryDir
|
||||
}
|
||||
|
||||
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
|
||||
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
var method = call.method
|
||||
val argmap: Map<String, String> = call.arguments as Map<String, String>
|
||||
|
||||
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
|
||||
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
|
||||
// the eventbus listener.
|
||||
if (call.method == "Start") {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
|
||||
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
|
||||
// accidentally duplicated. however, we still need to manually check if it's running or not, so
|
||||
// that we can divert this method call to ReconnectCwtchForeground instead if so.
|
||||
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
|
||||
for (workInfo in works) {
|
||||
Log.i("handleCwtch:WorkManager", "$workInfo")
|
||||
if (!workInfo.tags.contains(uniqueTag)) {
|
||||
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
|
||||
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
|
||||
}
|
||||
}
|
||||
WorkManager.getInstance(this).pruneWork()
|
||||
|
||||
Log.i("MainActivity.kt", "Start() launching foregroundservice")
|
||||
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
// 15 minutes is the shortest interval you can request
|
||||
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(this).enqueue(workRequest)
|
||||
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
|
||||
this, Observer { workInfo ->
|
||||
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||
val res = workInfo.outputData.keyValueMap.toString()
|
||||
result.success(workInfo.outputData.getString("result"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.i("MainActivity.kt", "onResume")
|
||||
if (myReceiver == null) {
|
||||
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
|
||||
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
|
||||
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
|
||||
myReceiver = MyBroadcastReceiver(mc)
|
||||
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter)
|
||||
}
|
||||
|
||||
// ReconnectCwtchForeground which will resync counters and settings...
|
||||
// We need to do this here because after a "pause" flutter is still running
|
||||
// but we might have lost sync with the background process...
|
||||
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(applicationContext).enqueue(workRequest)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
Log.i("MainActivity.kt", "onStop")
|
||||
if (myReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(myReceiver!!);
|
||||
myReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.i("MainActivity.kt", "onDestroy - cancelling all WORKER_TAG and pruning old work")
|
||||
WorkManager.getInstance(this).cancelAllWorkByTag(WORKER_TAG)
|
||||
WorkManager.getInstance(this).pruneWork()
|
||||
}
|
||||
|
||||
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
|
||||
// for reference:
|
||||
//
|
||||
// class Response(json: String) : JSONObject(json) {
|
||||
// val type: String? = this.optString("type")
|
||||
// val data = this.optJSONArray("data")
|
||||
// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
|
||||
// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
|
||||
// }
|
||||
//
|
||||
// class Foo(json: String) : JSONObject(json) {
|
||||
// val id = this.optInt("id")
|
||||
// val title: String? = this.optString("title")
|
||||
// }
|
||||
class AppbusEvent(json: String) : JSONObject(json) {
|
||||
val EventType = this.optString("EventType")
|
||||
val EventID = this.optString("EventID")
|
||||
val Data = this.optString("Data")
|
||||
}
|
||||
|
||||
// MainActivity.MyBroadcastReceiver receives events from the Cwtch service via im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS Android local broadcast intents
|
||||
// then it forwards them to the flutter ui engine using the CWTCH_EVENTBUS methodchannel
|
||||
class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() {
|
||||
val eventBus: MethodChannel = mc
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val evtType = intent.getStringExtra("EventType") ?: ""
|
||||
val evtData = intent.getStringExtra("Data") ?: ""
|
||||
//val evtID = intent.getStringExtra("EventID") ?: ""//todo?
|
||||
eventBus.invokeMethod(evtType, evtData)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import im.cwtch.flwtch.R
|
||||
import io.flutter.embedding.android.SplashScreen
|
||||
|
||||
class SplashView : SplashScreen {
|
||||
override fun createSplashView(context: Context, savedInstanceState: Bundle?): View? =
|
||||
LayoutInflater.from(context).inflate(R.layout.splash_view, null, false)
|
||||
|
||||
override fun transitionToFlutter(onTransitionComplete: Runnable) {
|
||||
onTransitionComplete.run()
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed="1.00" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
After Width: | Height: | Size: 544 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 442 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 721 B |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 44 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,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.cwtch.flwtch">
|
||||
<!-- 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"/>
|
||||
</manifest>
|
@ -0,0 +1,33 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
//removed due to gradle namespace conflicts that are beyond erinn's mere mortal understanding
|
||||
//task clean(type: Delete) {
|
||||
// delete rootProject.buildDir
|
||||
//}
|
@ -0,0 +1,2 @@
|
||||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file('cwtch.aar'))
|
@ -0,0 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||