Browse Source

Fresh Respository Commit - Cwtch Beta

sarah-patch-1
Sarah Jamie Lewis 3 months ago
commit
4c370007d9
  1. 284
      .drone.yml
  2. 47
      .gitignore
  3. 10
      .metadata
  4. 12
      ARCH.md
  5. 1
      LIBCWTCH-GO.version
  6. 71
      README.md
  7. 144
      SPEC.md
  8. 11
      android/.gitignore
  9. 120
      android/app/build.gradle
  10. 7
      android/app/src/debug/AndroidManifest.xml
  11. 51
      android/app/src/main/AndroidManifest.xml
  12. 46
      android/app/src/main/kotlin/im/cwtch/flwtch/CwtchPlugin.kt
  13. 266
      android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt
  14. 203
      android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
  15. 15
      android/app/src/main/kotlin/im/cwtch/flwtch/SplashView.kt
  16. 12
      android/app/src/main/res/drawable-v21/launch_background.xml
  17. 12
      android/app/src/main/res/drawable/launch_background.xml
  18. 18
      android/app/src/main/res/layout/splash_view.xml
  19. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  20. BIN
      android/app/src/main/res/mipmap-hdpi/knott.png
  21. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  22. BIN
      android/app/src/main/res/mipmap-mdpi/knott.png
  23. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  24. BIN
      android/app/src/main/res/mipmap-xhdpi/knott.png
  25. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  26. BIN
      android/app/src/main/res/mipmap-xxhdpi/knott.png
  27. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  28. BIN
      android/app/src/main/res/mipmap-xxxhdpi/knott.png
  29. 1
      android/app/src/main/res/raw/cwtch_animated_logo_op.json
  30. 18
      android/app/src/main/res/values-night/styles.xml
  31. 7
      android/app/src/profile/AndroidManifest.xml
  32. 33
      android/build.gradle
  33. 2
      android/cwtch/build.gradle
  34. 5
      android/gradle.properties
  35. 6
      android/gradle/wrapper/gradle-wrapper.properties
  36. 4
      android/key.properties
  37. 11
      android/settings.gradle
  38. BIN
      assets/Open_Privacy_Logo_lightoutline.png
  39. 399
      assets/core/Cwtch_knott_white.svg
  40. BIN
      assets/core/Tor_Booting_up.png
  41. 63
      assets/core/Tor_Booting_up.svg
  42. BIN
      assets/core/Tor_OFF.png
  43. 63
      assets/core/Tor_OFF.svg
  44. BIN
      assets/core/Tor_icon.png
  45. 58
      assets/core/Tor_icon.svg
  46. BIN
      assets/core/Tor_icon_error.png
  47. 70
      assets/core/Tor_icon_error.svg
  48. 19
      assets/core/account_blocked.svg
  49. BIN
      assets/core/account_blocked.webp
  50. 1
      assets/core/account_circle-24px.svg
  51. BIN
      assets/core/account_circle-24px.webp
  52. 1
      assets/core/account_circle-24px_lines.svg
  53. BIN
      assets/core/account_circle-24px_lines.webp
  54. 22
      assets/core/account_circle-24px_lines_thin-blocked.svg
  55. BIN
      assets/core/account_circle-24px_lines_thin-blocked.webp
  56. 20
      assets/core/account_circle-24px_lines_thin.svg
  57. BIN
      assets/core/account_circle-24px_lines_thin.webp
  58. 17
      assets/core/account_circle-24px_negative_space.svg
  59. BIN
      assets/core/account_circle-24px_negative_space.webp
  60. 23
      assets/core/account_circle-24px_user.svg
  61. BIN
      assets/core/account_circle-24px_user.webp
  62. 1
      assets/core/add_circle-24px.svg
  63. BIN
      assets/core/add_circle-24px.webp
  64. 1
      assets/core/attach_file-24px.svg
  65. BIN
      assets/core/attach_file-24px.webp
  66. 1
      assets/core/block-24px.svg
  67. BIN
      assets/core/block-24px.webp
  68. BIN
      assets/core/broken_heart_24.png
  69. 71
      assets/core/broken_heart_24.svg
  70. 1
      assets/core/check-24px.svg
  71. BIN
      assets/core/check-24px.webp
  72. BIN
      assets/core/chevron_left-24px.png
  73. 1
      assets/core/chevron_left-24px.svg
  74. BIN
      assets/core/chevron_left-24px.webp
  75. 1
      assets/core/clear-24px.svg
  76. BIN
      assets/core/clear-24px.webp
  77. 1
      assets/core/delete-24px.svg
  78. BIN
      assets/core/delete-24px.webp
  79. 1
      assets/core/done-24px.svg
  80. BIN
      assets/core/done-24px.webp
  81. 1
      assets/core/drag_indicator-24px.svg
  82. BIN
      assets/core/drag_indicator-24px.webp
  83. 1
      assets/core/edit-24px.svg
  84. BIN
      assets/core/edit-24px.webp
  85. 1
      assets/core/favorite-24px.svg
  86. BIN
      assets/core/favorite-24px.webp
  87. 5
      assets/core/fontawesome/regular/check-circle.svg
  88. BIN
      assets/core/fontawesome/regular/check-circle.webp
  89. 5
      assets/core/fontawesome/regular/hourglass.svg
  90. BIN
      assets/core/fontawesome/regular/hourglass.webp
  91. 5
      assets/core/fontawesome/regular/user.svg
  92. BIN
      assets/core/fontawesome/regular/user.webp
  93. 5
      assets/core/fontawesome/regular/window-close.svg
  94. BIN
      assets/core/fontawesome/regular/window-close.webp
  95. 5
      assets/core/fontawesome/solid/plus-circle.svg
  96. BIN
      assets/core/fontawesome/solid/plus-circle.webp
  97. 5
      assets/core/fontawesome/solid/plus-square.svg
  98. BIN
      assets/core/fontawesome/solid/plus-square.webp
  99. 5
      assets/core/fontawesome/solid/plus.svg
  100. BIN
      assets/core/fontawesome/solid/plus.webp

284
.drone.yml

@ -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

47
.gitignore

@ -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

10
.metadata

@ -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

12
ARCH.md

@ -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).

1
LIBCWTCH-GO.version

@ -0,0 +1 @@
v0.0.2-108-g3964348-2021-06-24-17-42

71
README.md

@ -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.

144
SPEC.md

@ -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 ?!?!

11
android/.gitignore

@ -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

120
android/app/build.gradle

@ -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'
}

7
android/app/src/debug/AndroidManifest.xml

@ -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>

51
android/app/src/main/AndroidManifest.xml

@ -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>

46
android/app/src/main/kotlin/im/cwtch/flwtch/CwtchPlugin.kt

@ -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
}
}*/

266
android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt

@ -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"
}
}

203
android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt

@ -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)
}
}
}

15
android/app/src/main/kotlin/im/cwtch/flwtch/SplashView.kt

@ -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()
}
}

12
android/app/src/main/res/drawable-v21/launch_background.xml

@ -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>

12
android/app/src/main/res/drawable/launch_background.xml

@ -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>

18
android/app/src/main/res/layout/splash_view.xml

@ -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>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png

After

Width: 72  |  Height: 72  |  Size: 544 B

BIN
android/app/src/main/res/mipmap-hdpi/knott.png

After

Width: 64  |  Height: 64  |  Size: 3.7 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png

After

Width: 48  |  Height: 48  |  Size: 442 B

BIN
android/app/src/main/res/mipmap-mdpi/knott.png

After

Width: 48  |  Height: 48  |  Size: 2.8 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

After

Width: 96  |  Height: 96  |  Size: 721 B

BIN
android/app/src/main/res/mipmap-xhdpi/knott.png

After

Width: 128  |  Height: 128  |  Size: 8.6 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

After

Width: 144  |  Height: 144  |  Size: 1.0 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/knott.png

After

Width: 128  |  Height: 128  |  Size: 8.6 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

After

Width: 192  |  Height: 192  |  Size: 1.4 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/knott.png

After

Width: 500  |  Height: 500  |  Size: 44 KiB

1
android/app/src/main/res/raw/cwtch_animated_logo_op.json
File diff suppressed because it is too large
View File

18
android/app/src/main/res/values-night/styles.xml

@ -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>

7
android/app/src/profile/AndroidManifest.xml

@ -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>

33
android/build.gradle

@ -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
//}

2
android/cwtch/build.gradle

@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('cwtch.aar'))

5
android/gradle.properties

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
android.bundle.enableUncompressedNativeLibs=false

6
android/gradle/wrapper/gradle-wrapper.properties

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

4
android/key.properties

@ -0,0 +1,4 @@
storePassword=%jks-password%
keyPassword=%jks-password%
keyAlias=cwtch-upload
storeFile=upload-keystore.jks

11
android/settings.gradle

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

BIN
assets/Open_Privacy_Logo_lightoutline.png

After

Width: 250  |  Height: 120  |  Size: 20 KiB

399
assets/core/Cwtch_knott_white.svg

@ -0,0 +1,399 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;"
xml:space="preserve"
sodipodi:docname="Cwtch_knott_white.svg"
inkscape:export-filename="/home/sarah/PARA/projects/cwtch/assets/core/knott-white.png"
inkscape:export-xdpi="98.300003"
inkscape:export-ydpi="98.300003"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata35"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs33" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview31"
showgrid="false"
inkscape:zoom="1.3350176"
inkscape:cx="-56.414859"
inkscape:cy="254.41396"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
.st1{fill:#010101;}
.st2{fill:#AF9CBA;}
.st3{clip-path:url(#SVGID_2_);}
.st4{clip-path:url(#SVGID_3_);}
.st5{clip-path:url(#SVGID_4_);}
.st6{clip-path:url(#SVGID_6_);}
.st7{clip-path:url(#SVGID_7_);}
.st8{clip-path:url(#SVGID_8_);}
.st9{clip-path:url(#SVGID_10_);}
.st10{clip-path:url(#SVGID_11_);}
.st11{clip-path:url(#SVGID_12_);}
.st12{clip-path:url(#SVGID_14_);}
.st13{clip-path:url(#SVGID_15_);}
.st14{clip-path:url(#SVGID_16_);}
.st15{clip-path:url(#SVGID_18_);}
.st16{clip-path:url(#SVGID_19_);}
.st17{clip-path:url(#SVGID_20_);}
.st18{clip-path:url(#SVGID_22_);}
.st19{clip-path:url(#SVGID_23_);}
.st20{clip-path:url(#SVGID_24_);}
.st21{clip-path:url(#SVGID_26_);}
.st22{clip-path:url(#SVGID_27_);}
.st23{clip-path:url(#SVGID_28_);}
.st24{clip-path:url(#SVGID_30_);}
.st25{clip-path:url(#SVGID_31_);}
.st26{clip-path:url(#SVGID_32_);}
.st27{clip-path:url(#SVGID_34_);}
.st28{clip-path:url(#SVGID_35_);}
.st29{clip-path:url(#SVGID_36_);}
.st30{clip-path:url(#SVGID_38_);}
.st31{clip-path:url(#SVGID_39_);}
.st32{clip-path:url(#SVGID_40_);}
.st33{clip-path:url(#SVGID_42_);}