285 Commits

Author SHA1 Message Date
chris 6d7b77fd4b Update .drone.yml
continuous-integration/drone/push Build is failing
2021-12-08 14:55:02 +01:00
chris 6b8066d473 Add .drone.yml
continuous-integration/drone/push Build is failing
2021-12-08 14:41:36 +01:00
chris 0405d9e04f 🤯 Update dependencies, switch to AndroidX 2018-10-15 20:00:52 +02:00
chris e67a4ea71b 🐛 Fix connectivity issue 2018-08-24 17:34:58 +02:00
chris a9602368fb 🎉 Separate messages by identity
Also, allow deleting all messages/conversations in a list
2018-08-24 17:32:45 +02:00
chris 9f2508c1a5 Merge branch 'feature/fix-connectivity-issues' into develop 2018-08-21 07:39:57 +02:00
chris 6128fd32f9 👻 Clean up services 2018-08-21 07:39:16 +02:00
chris ccfdff7fd8 😎 Make code more Kotliney 2018-07-11 16:59:50 +02:00
chris 3767d976c8 🔥 Massively simplified how Abit connects to the network
* Removed features "Synchronization" and "Server POW"
* Service isn't foreground anymore (not yet sure this is a good decision)
* "Full node" renamed to "online"
2018-07-05 19:44:04 +02:00
chris 87bc01701c ⬆️ Bump dependencies 2018-07-05 18:40:48 +02:00
chris 6878f80a54 🐛 Fix connectivity issues 2018-06-14 07:07:48 +02:00
chris a01f116065 🐛 Fix connectivity issues 2018-06-14 06:56:07 +02:00
chris c6e29c056b 🔥 Drop support for API 19 (KitKat)
I'm fed up with all the extra effort for supporting an outdated
Android version. Apologies for the few users that are now excluded,
but I feel like it's just not worth the hassle.
2018-06-13 22:03:06 +02:00
chris 8b89d81970 😴 Minor improvements 2018-06-13 19:48:00 +02:00
chris 76317a2488 🔀 Merge branch 'feature/conversations' into develop 2018-06-12 16:58:19 +02:00
chris 85f114a33d 🔌 Add preference to connect on charging only 2018-06-12 16:56:15 +02:00
chris 9e7f247763 ⬆️ Bump android build tools version 2018-06-12 16:54:00 +02:00
chris ec4615b639 🐘 Tweak memory usage 2018-06-12 16:52:42 +02:00
chris 2ddd78dfe2 📡 Improve and simplify connectivity handling 2018-06-04 12:55:00 +02:00
chris 90bb538692 🚀 Improve performance 2018-05-25 20:48:42 +02:00
chris 9cc07f73ae 🐛 Prevent ANR 2018-05-24 21:08:06 +02:00
chris 0b432b6a67 🔃 Switch showcase library
Which, unfortunately, pulls along a lot of other
changes (mostly for the better)
2018-05-23 19:04:27 +02:00
chris 725ec60fd4 📄 Add some project documentation
🤝 Code of Conduct
👩‍💻 Contribution
📄 License
2018-05-17 11:06:20 +00:00
chris 60c4a4d8a0 ⬆️ Kotlin version bump 2018-04-22 12:06:10 +02:00
chris 6585876b25 🚸 Add number of messages to conversation list item 2018-04-22 11:29:39 +02:00
chris b1fd9d9ef9 😴 Minor code style improvements 2018-04-20 17:51:14 +02:00
chris e05d27bfbc 🎨 Conversation rendering improvements 2018-04-20 07:04:31 +02:00
chris be7a7f1af6 🎨 Identicon rendering improvements 2018-04-20 07:00:55 +02:00
chris 61e579c0d4 🚸 Improve settings structure 2018-04-17 19:55:56 +02:00
chris eee1be873a 🎨 Add new icon vor Oreo and later 2018-04-17 09:42:25 +02:00
chris 4c213d3e9c Merge branch 'develop' into feature/conversations 2018-04-14 20:44:19 +02:00
chris 76cb5df998 🚸 Improved network notification 2018-04-14 20:42:53 +02:00
chris 3026ae8505 🐛 Fixed bug 2018-04-14 20:42:10 +02:00
chris 412180f443 Add batch service for migrating existing messages 2018-04-14 20:27:29 +02:00
chris 78f9621afa 🚧 Add code to migrate existing conversations
Work in progress: does work, but usually doesn't finish. This needs to be
moved into some proper batch processing.
2018-04-13 12:39:59 +02:00
chris 85562efc0d ⬆️ Update Android build tools 2018-04-13 12:39:20 +02:00
chris a89f80f400 🎨 minor multi-identicon improvements/fixes 2018-04-13 07:45:31 +02:00
chris 1426b786e8 Add message grouping by subject 2018-04-03 22:14:46 +02:00
chris 6a311a0346 Fix minor lint warning 2018-04-03 22:14:00 +02:00
chris 9b75a8c2ef Fixed tests 2018-04-03 22:13:23 +02:00
chris 4e5ba4401a Improved multi identicon background colour 2018-04-03 22:12:13 +02:00
chris f374748f71 Fixed tests and updated dependencies 2018-04-03 22:11:37 +02:00
chris 49e77199b0 Improve utilities 2018-03-23 17:50:43 +01:00
chris 46e5bb7ece Improve conversation view 2018-03-18 07:00:21 +01:00
chris 8004865e01 Nicer labels 2018-03-12 21:18:10 +01:00
chris 101913a531 Show messages in inbox and archive as conversations
Work in progress - detail view not yet adapted, and needs extended encoding
for sensible results.
2018-03-05 09:48:49 +01:00
chris 40f8bc87a2 Bump dependencies 2018-03-05 09:43:40 +01:00
chris 0d1cfff883 Fix lint issues 2018-02-27 12:54:30 +01:00
chris d7b7b11cdf Fix deleting messages from archive 2018-02-27 06:43:45 +01:00
chris d8d5f70b37 Merge tag '1.0-rc1.1' into develop
1.0-rc1.1
2018-02-25 23:38:45 +01:00
chris 06e99ea0cd Merge branch 'hotfix/1.0-rc1.1' 2018-02-25 23:38:18 +01:00
chris ad3af929d7 Hotfix: properly select recipient 2018-02-25 23:35:03 +01:00
chris 16f1dfa6f6 Merge tag '1.0-rc1' into develop
1.0-rc1
2018-02-24 08:50:31 +01:00
chris 2bddd0f256 Merge branch 'release/1.0-rc1' 2018-02-24 08:50:13 +01:00
chris ab70e6df12 Version 1.0-rc1 bump 2018-02-24 08:49:05 +01:00
chris fb72356467 Retain content of compose view on orientation change 2018-02-24 07:45:33 +01:00
chris e3c7c4d557 Merge branch 'develop' into 'develop'
Develop

See merge request chris/Abit!3
2018-02-24 07:44:30 +01:00
NourEddine 938bfc206e Translated using Weblate (Arabic)
Currently translated at 100.0% (132 of 132 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/ar/
2018-02-23 06:25:46 +00:00
NourEddine b4b1d25f99 Translated using Weblate (Arabic)
Currently translated at 100.0% (132 of 132 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/ar/
2018-02-22 15:40:20 +00:00
chris 3509082d30 Remove redundant semicolon; 2018-02-21 16:55:22 +01:00
chris 9e59187ae0 Handle invalid WIF files 2018-02-21 15:05:38 +01:00
chris c1af65732a Improve translations
Moved labels.xml into strings.xml as they are handled by Weblate and it was a mess anyway.
2018-02-21 15:05:08 +01:00
chris 61e0a12a50 Don't show addresses without alias as contacts
Those normally aren't of interest, and can now be retrieved via message.
2018-02-20 22:48:39 +01:00
chris c254c1bacd Merge branch 'develop' into 'develop'
Develop

See merge request chris/Abit!2
2018-02-20 18:24:19 +01:00
chris 4f1ef4407c Translated using Weblate (French)
Currently translated at 100.0% (125 of 125 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/fr/
2018-02-20 17:21:46 +00:00
chris 46bbd59712 Translated using Weblate (French)
Currently translated at 100.0% (125 of 125 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/fr/
2018-02-20 17:10:33 +00:00
chris 8d876719c4 Merge branch 'develop' of git.dissem.ch:chris/Abit into develop 2018-02-20 18:02:25 +01:00
chris ec645d70ce Merge branch 'feature/drafts' into develop 2018-02-20 18:01:54 +01:00
chris 7f0d8828d1 Merge branch 'develop' into 'develop'
Develop

See merge request chris/Abit!1
2018-02-20 18:00:18 +01:00
chris d98b800249 Translated using Weblate (French)
Currently translated at 99.1% (123 of 124 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/fr/
2018-02-20 16:35:50 +00:00
NourEddine c750e2004a Translated using Weblate (Arabic)
Currently translated at 31.4% (39 of 124 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/ar/
2018-02-20 16:35:45 +00:00
chris 9eefbad7d6 Fix lint issues 2018-02-20 16:40:03 +01:00
chris 21abdbd720 Add link to detail view of the sender 2018-02-19 23:55:48 +01:00
chris 39ad5e8baf Improve scrolling behaviour for the compose view 2018-02-19 23:38:40 +01:00
chris 1da0674857 Add option to select identity when composing a message 2018-02-14 17:49:39 +01:00
chris f481914a65 Implemented basic draft functionality 2018-02-13 07:24:24 +01:00
chris 99b2d1903e Added translation using Weblate (Arabic) 2018-02-11 10:03:03 +00:00
chris d611bd13bc Translated using Weblate (French)
Currently translated at 8.8% (11 of 124 strings)

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/fr/
2018-02-11 10:01:13 +00:00
chris 66c8536a84 Added translation using Weblate (French) 2018-02-09 21:48:09 +00:00
chris 72213a53a5 Merge tag '1.0-beta20' into develop
1.0-beta20
2018-01-27 21:43:43 +01:00
chris 8a668f4af2 Merge branch 'release/1.0-beta20' 2018-01-27 21:43:22 +01:00
chris 6829614da0 Version 1.0-beta20 bump 2018-01-27 21:28:18 +01:00
chris 1906a2e13c Fixed crash on Oreo and issue on mobile network 2018-01-27 21:10:43 +01:00
chris 708529fc0a Merge tag '1.0-beta19' into develop
1.0-beta19
2018-01-18 22:50:21 +01:00
chris b5dbbeb46a Merge branch 'release/1.0-beta19' 2018-01-18 22:50:08 +01:00
chris b368c8251d 1.0-beta19 2018-01-18 22:49:55 +01:00
chris 6986d9a2df Bumped Kotlin version 2018-01-18 22:38:11 +01:00
chris 35249a0145 Improved some repository code in search for an unclosed cursor
(that I didn't find)
2018-01-18 22:37:40 +01:00
chris 2a1aa736cc Fixed newly introduced bugs 2018-01-18 22:33:07 +01:00
chris 9f26ade617 Merge tag '1.0-beta18' into develop
Version 1.0-beta18
2018-01-18 17:26:11 +01:00
chris 396f1a23a6 Merge branch 'release/1.0-beta18' 2018-01-18 17:25:33 +01:00
chris db939dab9c Version 1.0-beta18 bump 2018-01-18 17:25:24 +01:00
chris 30d9d72133 Fixed bug where deleted items didn't disappear from list 2018-01-16 17:34:42 +01:00
chris df581f4c51 Use observer pattern for label change 2018-01-13 21:59:20 +01:00
chris 4c89bfe1cf Bug fixes & code improvements
- simplified access to MainActivity
- fixed bug where the 'unread' tag wasn't updated
- aboutlibraries version bump
2018-01-11 17:25:26 +01:00
chris d88d3c900e Bug fixes
- fixed check if connection is allowed (metered/unmetered network)
- label is actually selected when opening the overview
- minor code improvements
- dependency version bumps
2017-12-30 19:21:25 +01:00
chris f6ebd62c8d Code style improvements (thanks to codebeat.co) 2017-11-27 22:26:30 +01:00
chris 760e423b9b Split LabelRepository off the MessageRepository 2017-11-27 22:06:25 +01:00
chris f45f6c3919 Code style improvements (thanks to codebeat.co)
+ bumped Jabit to the feature/refactoring branch
2017-11-23 20:33:00 +01:00
chris dd22caaa50 Code style improvements (thanks to codebeat.co) 2017-11-22 21:07:09 +01:00
chris 49d87c3c75 Fixed bugg where message was still marked as unread after returning from detail screen
+ bumped kotlin to version 1.1.60
2017-11-21 20:12:31 +01:00
chris c7dbe660b9 Bumped dependencies 2017-11-21 18:17:10 +01:00
chris b825b33250 Fixed swipe gesture 2017-11-14 07:48:21 +01:00
chris 33e932e630 Improved node registry so it should provide better nodes 2017-11-14 07:47:34 +01:00
chris 072f732924 Switch to API level 27 and updated libraries 2017-10-31 07:50:57 +01:00
chris f58a22dadb Merge branch 'feature/performance-improvements' into develop 2017-09-22 21:01:04 +02:00
chris 1329aecde4 Version 1.0-beta17 bump 2017-09-22 20:55:09 +02:00
chris f27f438998 Minor fixes and improvements 2017-09-22 20:29:23 +02:00
chris c1d74e4781 Minor fixes
- don't show dialog on resume in mobile networks
- load more items when deleting without scrolling
2017-09-19 17:34:11 +02:00
chris 34e2e0673b Fixed the timeout issue with the JobScheduler 2017-09-16 06:57:59 +02:00
chris f4b6bcb8d9 Some fixes around deleting/trashing messages 2017-09-15 15:25:32 +02:00
chris 1e8b71e43b Some performance improvements 2017-09-12 21:28:56 +02:00
chris 9dd1b457e3 Fixed crash 2017-09-08 07:36:54 +02:00
chris 287de9deb5 Added repository tests and fixed some bugs 2017-09-04 20:35:41 +02:00
chris 696cd6c0a6 Removed most '!!' 2017-09-01 07:29:44 +02:00
chris a23ae14f1d Merge branch 'feature/kotlin' into develop 2017-08-29 21:15:51 +02:00
chris 415107a6c8 Minor code improvements and bug fixes 2017-08-29 21:14:50 +02:00
chris cc18f34161 Fully migrated to Kotlin 2017-08-25 20:34:38 +02:00
chris 852e38b97d Use JobScheduler to make the "WiFi-only" feature work properly newer Android versions.
I'm considering dropping support for KitKat, as we have now double the code for the same feature.
2017-08-19 08:17:52 +02:00
chris 858651e808 Imports basically work now, although there may be some issues if the user switches language between export and import. 2017-08-17 17:31:06 +02:00
chris 625848bd9d Improved FAB and added translation for showcase button 2017-08-14 19:51:16 +02:00
chris 4622ad68f0 Version 1.0-beta16 bump 2017-08-12 19:44:39 +02:00
chris d05f1f98e5 Renamed "store" to "promo" and added mastodon header graphics 2017-08-11 17:34:07 +02:00
chris 973e4a0dca Fake POW notification progress tweaks 2017-08-11 17:25:35 +02:00
chris 3a98cc115a Option to turn off acknowledgements 2017-08-11 17:24:36 +02:00
chris e2aa0e8b1d Fixed FAB 2017-08-10 00:40:44 +02:00
chris ec3009a257 Some notification improvements
POW progress probably needs some tweaking
2017-08-10 00:39:36 +02:00
chris e79bfdb244 Exports, some Kotlin stuff, and version 1.0-beta15 2017-08-03 00:00:23 +02:00
chris 898c49802b Some Bugfixes and some Kotlin that helped fixing the bugs 2017-07-28 07:39:57 +02:00
chris e064012551 Improved fab speed dial 2017-07-26 21:01:07 +02:00
chris faa6752b10 Replaced fab speed dial with a different library - there is still some tweaking needed, but at least it works again 2017-07-24 18:01:40 +02:00
chris 593a390b40 Version 1.0-beta14 bump 2017-07-24 17:25:15 +02:00
chris ccfeb5b479 (Hopefully) fixed crash when user moved on after loading the labels took too long 2017-07-22 08:16:06 +02:00
chris 8057980f6c Minor bugfixes 2017-07-22 06:37:06 +02:00
chris 8af8419b7c Version 1.0-beta13 bump 2017-07-11 20:48:56 +02:00
chris 433c757107 UI improvements and fixes for older Android versions 2017-07-10 06:21:29 +02:00
chris 1c284eba26 Asynchronously load messages to improve responsiveness 2017-06-30 00:07:54 +02:00
chris bf52d2f3de Asynchronously load contacts to improve responsiveness 2017-06-29 23:29:56 +02:00
chris a67560c28b Fixed and improved the UI for starting a full node 2017-05-18 07:29:16 +02:00
chris bf070da20a Fixed NullPointerException when accessing outbox items 2017-05-17 12:24:13 +02:00
chris 73944b5883 Fixed issues with new full node behaviour 2017-05-08 20:28:43 +02:00
chris 263fc8893f Some improvements suggested by Codacy 2017-05-07 16:17:43 +02:00
chris 6540df4fc9 Some improvements suggested by Codacy 2017-05-07 16:13:20 +02:00
chris 422c7ac803 Remember connection state (so you don't have to start a full node whenever you lost your WiFi connection) 2017-05-07 13:39:30 +02:00
chris 5bc1bc2a47 Some improvements suggested by Codacy 2017-05-05 07:27:24 +02:00
chris c7200d06bc Some improvements suggested by Codacy 2017-04-25 23:00:31 +02:00
chris 3bdf1bd6bf Merge tag '1.0-beta12' into develop
A new beta version was overdue
2017-04-25 21:29:47 +02:00
chris 4f36f36ab3 Merge branch 'release/1.0-beta12' 2017-04-25 21:29:27 +02:00
chris 66108c8618 Version 1.0-beta12 bump 2017-04-25 21:29:11 +02:00
chris 3ae572bcf2 Merge branch 'feature/conversations' into develop 2017-04-25 21:27:41 +02:00
chris d36ffe1939 Fixed layout - apparently ConstraintLayout doesn't work properly for dialogs yet, at least in this case 2017-04-25 08:07:33 +02:00
chris d704a40b66 Regularly call cleanup() 2017-04-22 07:42:20 +02:00
chris d3f1e6abd2 Minor layout improvements 2017-04-21 15:20:50 +02:00
chris 91cc90ec04 Fix unread badge for archive 2017-04-21 07:23:39 +02:00
chris 30c5bf6b90 Open related messages on click 2017-04-20 23:24:28 +02:00
chris 572ecf1577 Some minor fixes for working with extended encoding 2017-04-20 07:39:08 +02:00
chris a8dada6c89 Alternative key exchange by providing the public key in the URI (bugfixes) 2017-04-19 00:20:51 +02:00
chris 911dfa7a27 Show QR code when clicking on profile image 2017-04-18 16:38:45 +02:00
chris 26fca259bc Alternative key exchange by providing the public key in the URI 2017-04-16 22:38:11 +02:00
chris 55746743c4 Updated dependencies, enabled multidex 2017-04-16 22:36:23 +02:00
chris f77bbe1a43 Show related messages (parents, replies) 2017-04-16 22:35:26 +02:00
chris 8770575c95 Fixed NullPointerException 2017-04-07 14:36:16 +02:00
chris 61c9cde2f5 Gradle wrapper and dependency versions bumped 2017-04-04 06:29:44 +02:00
chris e93dc43f89 Tiny code improvement 2017-03-24 07:35:57 +01:00
chris a203af654b Add labels to message detail view 2017-03-23 16:59:36 +01:00
chris 6be31c9f4e Fixed "add identity" dialog, updated dependencies 2017-03-20 07:28:41 +01:00
chris 42cf18445c Bumped Jabit version to prepare for conversations 2017-03-18 07:09:03 +01:00
chris 22ac1920a2 Merge branch 'develop' into feature/conversations 2017-02-24 17:35:01 +01:00
chris cd8192e99c Merge branch 'feature/load-lists-asynchronously' into develop 2017-02-24 17:34:14 +01:00
chris 300da1730d Archive is now handled somewhat differently, so we can distinguish between 'archive' and 'not initialized'. 2017-02-24 17:33:59 +01:00
chris 65c03bd638 Load lists asynchronously 2017-02-23 17:38:00 +01:00
chris 18d72d727c Fixed notification title for when there are more than 5 messages 2017-02-23 17:37:15 +01:00
chris 37371a0e94 Moved dependency so it isn't groupt with the test dependencies 2017-01-27 08:25:40 +01:00
chris 2c41aff3af Merge remote-tracking branch 'origin/develop' into develop 2017-01-01 15:01:01 +01:00
chris e74b18ed3f Fixed bug where network notification couldn't be dismissed
and updated build tools
2017-01-01 15:00:46 +01:00
chris d5407b7b01 Minor improvement 2016-12-24 00:15:56 +01:00
chris 249c97d4ba Added function to cleanup inventory (mine grew to 1.5GB) but no automatism yet 2016-12-16 07:44:01 +01:00
chris 6f26e84f71 Some code quality improvements 2016-12-06 06:40:58 +01:00
chris 0a8459750a Fixed/added translation 2016-12-05 12:59:37 +01:00
chris 4be1f1e505 Added code to send messages with extended encoding 2016-12-05 12:37:32 +01:00
chris 21fde7c22e Updated README.md
It was about time
2016-11-05 07:31:18 +01:00
chris 96af8e0750 Some chan improvements
- When replying on a chan, the receiving address isn't used as the sending identity
- Chans are listed as contacts as well
2016-11-04 22:03:04 +01:00
chris b34e678c68 Disabled Jack
I'd love to use Java 8 features, but Jack just isn't ready yet.
2016-11-03 23:04:41 +01:00
chris 9af80f008d "Up" navigation now brings you back to the selected label 2016-11-02 20:55:56 +01:00
chris edd1124c32 Updated Jabit and fixed the update of the "unread messages" badge 2016-11-02 07:32:13 +01:00
chris e249c86b79 Minor UI improvements
- added outbox label
- notification is now always removed when on main activity
- send status is now shown in message list and detail view
2016-10-27 17:37:34 +02:00
chris 7332886786 Added UI for sending broadcasts
- UI shouldn't block until the first identity is ready anymore
2016-10-25 07:30:16 +02:00
chris 6a8648ca28 Added actions to notifications (this required a slight detour) 2016-10-20 12:53:35 +02:00
chris b3dd53a5df Lint fixes 2016-10-20 05:49:07 +02:00
chris 2b1fb436a9 Fixed the 'full node' switch, activated the jack tools to support some Java 8 features, and fixed some lint issues 2016-10-16 23:16:38 +02:00
chris a5b3c33394 Improved identicon 2016-10-16 22:15:41 +02:00
chris dc3bfce9a2 Fixed identicon rendering in identity view 2016-10-10 21:57:46 +02:00
chris 4ab64c0ed1 Version 1.0-beta9 bump 2016-10-10 21:56:31 +02:00
chris b94e48e544 Updated Jabit version to fix exception on import 2016-10-07 23:23:01 +02:00
chris 9e8067cda5 Merge branch 'feature/support-info' into develop 2016-10-07 23:13:32 +02:00
chris 30bb407bbf Merge remote-tracking branch 'origin/develop' into feature/support-info
# Conflicts:
#	app/src/main/res/values-de/strings.xml
#	app/src/main/res/values/strings.xml
2016-10-07 23:13:06 +02:00
chris a1fb11357b Fixed NPEs in identity creation dialogs 2016-10-07 23:06:47 +02:00
chris 0efd17a356 Merge branch 'feature/identity-import' into develop 2016-10-06 22:02:01 +02:00
chris 91451b0ce7 Fixed some layouts and updated dependencies 2016-10-06 22:01:33 +02:00
chris da6cd43a46 Added missing libraries for AboutLibraries dialog 2016-10-05 15:59:24 +02:00
chris bfd5a72b52 Identity creation dialog improvements 2016-10-02 16:27:01 +02:00
chris dea9231fcf Fixed bug where two items were removed from view with one swipe 2016-10-02 14:19:50 +02:00
chris 34000e7b79 Some layout improvements and fixes 2016-10-01 17:40:38 +02:00
chris a18ef1ac29 Refactored adding new identities
- add deterministic identities
- import existing identities
2016-10-01 10:33:11 +02:00
chris 246649d028 Added info on supporting development to preferences screen 2016-09-23 16:33:19 +02:00
chris c1d5af0034 Updated support library versions 2016-09-23 16:30:17 +02:00
chris 8d58af423c Merge branch 'feature/swipe-actions' into develop 2016-09-23 08:02:28 +02:00
chris 5ea317c295 Fixed garbage shown on older devices when the ellipsized value had line breaks in them 2016-09-23 07:51:04 +02:00
chris cc4c16e970 Fixed layout issue 2016-09-22 00:09:59 +02:00
chris 1c226a6a5b Added swipe actions for messages.
- there is a minor layout problem on pre-Lollipop devices
2016-09-21 23:46:57 +02:00
chris d416db1307 Merge branch 'feature/nio' into develop 2016-09-21 20:40:20 +02:00
chris 141c17a28c Bumped Jabit version 2016-09-21 20:39:53 +02:00
chris c8a0301402 GUI, layout and style improvements, updated dependencies 2016-09-16 17:35:24 +02:00
chris af2bfc796b Use the nio network listener. 2016-09-12 11:00:00 +02:00
chris dd9539aa3f Added UI for chans 2016-09-12 09:55:48 +02:00
chris da73388d8c Added .editorconfig, mainly to enforce Unix LF line breaks 2016-09-02 17:29:53 +02:00
chris c3eef5d711 Fixed icon color for pre-lollipop phones 2016-09-02 17:18:04 +02:00
chris 044776acbb Added store graphics 2016-09-02 17:16:34 +02:00
chris e102908acf Fixed sending messages 2016-08-13 23:24:42 +02:00
chris f0e03f15a3 Fixed layout for composing messages 2016-08-13 12:06:41 +02:00
chris 709e333e78 Updated libraries, most notably Jabit to develop-SNAPSHOT
Also, finally added proper icon
Known issue: the client seems to sever all connections after some time, I'll need to look into this. This might happen when 8 connections are reached for the first time.
2016-08-12 23:54:01 +02:00
chris f705e13d0b Fixed notification and made code slightly simpler 2016-04-27 17:30:05 +02:00
chris 0ecfbd3fb8 Implemented methods needed for chan support (no GUI yet, though) 2016-04-25 09:27:36 +02:00
chris 59f0bc7b74 Added export option for identities (import will follow) 2016-03-03 16:43:05 +01:00
chris 5db5442064 Improvements for Contacts
- create contacts by manually entering the address (or pasting it)
- share address
2016-02-24 19:50:25 +01:00
chris 563085ed79 Added badge for unread messages 2016-02-23 07:06:34 +01:00
chris df121b25c6 Version 1.0-beta7 bump 2016-02-23 07:05:38 +01:00
chris 17a99b6562 Added showcase explaining full node 2016-02-12 00:19:15 +01:00
chris a4f6642f6a Added option to scan QR codes directly from the app 2016-02-06 13:25:04 +01:00
chris a3c3fc082d Fixed error when responding to messages with a very short subject 2016-02-06 13:24:25 +01:00
chris d682df4cdb Updated Jabit, which should fix a bug where you can't send messages 2016-02-03 07:42:21 +01:00
chris 3d7c1a504e Never delete the private key 2016-01-31 23:13:35 +01:00
chris 6bcb7fc50e Updated Jabit version (there were some bugs) 2016-01-31 18:20:12 +01:00
chris 9275f5ca9c Address related improvements
- QR code is now shown in contact details and 'manage identity' view
- Contacts and identities can now be deleted
2016-01-29 18:05:43 +01:00
chris adfb3a920a Fixed problem with proof of work 2016-01-23 23:30:51 +01:00
chris 491a8a0ccb Version 1.0-beta v2
fixed sync
2016-01-22 09:22:43 +01:00
chris 4a854045e0 Version 1.0-beta bump 2016-01-21 20:42:20 +01:00
chris c4d76fc8ce Code cleanup - most notably BitmessageService was cleaned up and doesn't use messaging anymore 2016-01-19 20:50:58 +01:00
chris 6f6a2e4e6c Minor bug fixes and improvements, added caching to inventory 2016-01-19 07:43:48 +01:00
chris 6ec25cfae7 Minor improvements 2016-01-17 07:11:39 +01:00
chris b4c5d8cc21 Jabit Domain was renamed to Jabit Core 2016-01-17 07:10:44 +01:00
chris 2d2916f498 Added some quick & dirty activity to show addresses and the network state 2016-01-14 16:41:10 +01:00
chris abd25f4010 Minor bugfixes & renamed 'security' to 'cryptography 2016-01-13 17:28:18 +01:00
chris eb322d94a2 Minor improvements and preparations for the release 2016-01-01 15:35:16 +01:00
chris 6d21a99b3d Merge branch 'feature/wifi-restriction' into develop 2015-12-27 22:09:18 +01:00
chris 694fa97dce About Libraries 2015-12-27 22:07:44 +01:00
chris fdc2277324 Implemented option to only use WiFi 2015-12-27 20:04:17 +01:00
chris 1c5aed0f6c Merge branch 'feature/server-pow' into develop 2015-12-22 17:53:22 +01:00
chris 41f4571bf6 Server POW should work now 2015-12-21 15:31:48 +01:00
chris b0828ec1e5 Server POW (work in progress) 2015-11-28 20:28:28 +01:00
chris 73383aa2c2 Renamed MessageListActivity to MainActivity
(it is the main activity, after all)
2015-11-22 12:28:31 +01:00
chris b676983ba4 Merge branch 'feature/Contacts' into develop 2015-11-18 16:13:34 +01:00
chris 055bd39a42 Linkify URLs and Bitmessage addresses 2015-11-17 22:19:11 +01:00
chris 9040026965 Select contact and send message 2015-11-11 21:03:03 +01:00
chris c44f702ba5 Merge branch 'feature/SyncAdapter' into develop 2015-11-08 20:09:53 +01:00
chris 54a319638b Added a service based POW engine, so it shouldn't be killed by the system, at least not that easily. (WIP) 2015-10-31 07:49:03 +01:00
chris e98eefe2cc Added notification for errors and warnings 2015-10-29 16:38:50 +01:00
chris 2a17bbe34b Synchronization works, at least basically 2015-10-25 11:29:46 +01:00
chris 7fe7ee42fc Fixed initialisation, added message/broadcast sending to service 2015-10-25 09:50:40 +01:00
chris 725089c604 Fixed NullPointerException and minor improvements 2015-10-24 20:59:24 +02:00
chris f5bf5c8bca Moving Bitmessage context into a foreground service (work in progress) 2015-10-23 22:40:09 +02:00
chris 9b1bf6bdb3 Moving Bitmessage context into a foreground service (work in progress) 2015-10-21 16:43:13 +02:00
chris f19996f79c Removed unused icons 2015-10-21 16:15:57 +02:00
chris e149efcfff Changed string resource 2015-10-20 21:17:52 +02:00
chris ec3f3d2ca9 Create README.md 2015-10-19 15:00:40 +02:00
chris ed5fb69eaf Bugfixes 2015-10-18 21:47:07 +02:00
chris e8de311d7e Merge remote-tracking branch 'origin/develop' into feature/SyncAdapter
Conflicts:
	app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java
	app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java
	app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java
	app/src/main/res/values-de/strings.xml
	app/src/main/res/values/strings.xml
2015-10-18 19:00:51 +02:00
chris 32a36e57f6 Changed to using vector drawables for action icons 2015-10-18 18:20:58 +02:00
chris e232457811 Merge branch 'feature/network-status' into develop 2015-10-18 18:18:23 +02:00
chris e4d7bf4893 Changed to using vector drawables for action icons 2015-10-18 18:17:57 +02:00
chris 64e0479a37 Changed notification icon and updated library versions 2015-10-18 16:42:41 +02:00
chris 13cb804fc2 Added ongoing notification showing network status
- renamed packages to be more consistent
- somewhate refactored the way notifications are made
2015-10-18 13:40:17 +02:00
chris fd415b9c8c Merge branch 'develop' into feature/SyncAdapter 2015-10-12 16:06:30 +02:00
chris 1659aaa1ee Added label "Archive" 2015-10-12 16:00:43 +02:00
chris 67c06b9884 Some sync adapter fixes and changes - still doesn't work 2015-10-12 14:44:01 +02:00
chris 348fa8daed Some sync adapter code and bugfixes around it - not yet functional 2015-10-08 14:11:45 +02:00
chris 3c7fd02613 Fixes / Improvements
- some fixes for tablets
- updated API
- removed BitmessageService (I really hope it won't be needed)
2015-10-06 12:51:09 +02:00
chris a7ebfefdbc tool updates 2015-09-29 07:14:19 +02:00
chris dd8ac629b2 Show subscriptions (actually, all contacts) 2015-09-11 07:59:39 +02:00
chris a17685fd10 Added some actions 2015-09-06 17:45:21 +02:00
chris 496fffe6ee Notifications could still use some fine tuning, but should work fine for now 2015-09-04 08:19:07 +02:00
chris e5b00c7453 Added proper message list, and almost nice notifications 2015-08-28 13:49:53 +02:00
chris 5e2d19df58 DB code rewrite
There's still a problem storing or retreiving messages
2015-08-19 07:01:30 +02:00
chris cb2040b0ce Receiving almost works, unfortunately, JDBC doesn't so I have to rewrite the whole damn repository code. 2015-08-14 17:25:05 +02:00
chris 89a5ada48a Initial commit
A.k.a. "I should have done this some time ago"
2015-08-05 19:48:41 +02:00
318 changed files with 16326 additions and 3827 deletions
+9
View File
@@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: androidsdk/android-28
commands:
- ./gradlew assemble
- ./gradlew check
+171 -8
View File
@@ -1,18 +1,165 @@
# Mac Specific # Created by https://www.gitignore.io
### Android ###
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
/*/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
### Android Patch ###
gen-external-apklibs
### Gradle ###
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Eclipse ###
*.pydevproject
.metadata
.gradle
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# PDT-specific
.buildpath
# sbteclipse plugin
.target
# TeXlipse plugin
.texlipse
### Linux ###
*~
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### OSX ###
.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*
# Files that might appear on external disk
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd .fseventsd
.Spotlight-V100
.TemporaryItems .TemporaryItems
.Trashes
.VolumeIcon.icns .VolumeIcon.icns
# Directories potentially created on remote AFP share # Directories potentially created on remote AFP share
@@ -21,8 +168,24 @@ Network Trash Folder
Network Trash Folder Network Trash Folder
Temporary Items Temporary Items
.apdisk .apdisk
# Jekyll Specific
_site/
# Ruby
Gemfile.lock ### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
-48
View File
@@ -1,48 +0,0 @@
---
layout: default
title: 404
permalink: /404.html
---
<div class="container-fluid index error">
<div class="row">
<div class="col-md-12 content-panel articles">
<h1 class="header author-header">404</h1>
<div class="error-text">
Sorry but this page doesn't seem to exist.
</div>
<div class="error-text">
<a href="{{ site.baseurl }}/">Home</a> |
<a href="{{ site.baseurl }}/posts/">All Posts</a> |
<a href="{{ site.baseurl }}/search/">Search</a>
</div>
{% include social_links.html %}
</div>
</div>
<div class="content-panel related">
{% for post in site.posts limit:1 %}
<div class="related-header">
<a href="{{ site.baseurl }}{{ post.url }}">Suggested Article</a>
</div>
<div class="title">
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
</div>
<div class="excerpt">
{% if post.summary %}
{{ post.summary | strip_html | truncatewords:30 }}
{% else %}
{{ post.excerpt | strip_html | truncatewords:30 }}
{% endif %}
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
</div>
{% endfor %}
</div>
</div>
+46
View File
@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abit@dissem.ch. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/
+16
View File
@@ -0,0 +1,16 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue or another method with the owners of this repository before making a change.
Make sure Abit is the right place for the change - changes on the protocol level might be better placed in [Jabit](https://git.dissem.ch/bitmessage/Jabit).
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. A pull request should always merge into `develop`; only releases are merged into master.
3. A reviewer usually merges the pull requests. They may however request you to do it in case you have sufficient permissions, or may request you to update your branch to include the latest changes from `develop`.
+194 -21
View File
@@ -1,28 +1,201 @@
Copyright (C) 2014 Jacob Tomlinson Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The contents of this website, which consists of the files in TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
* _config.yml
* _data
* _drafts
* _posts
are copyrighted and sole property of its author Jacob Tomlinson. It may not be 1. Definitions.
used, modified, syndicated or distributed without expressed permission from
the author.
However the website theme built using jekyll is Open Source under the following "License" shall mean the terms and conditions for use, reproduction,
GPLv3 license. and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
This program is free software: you can redistribute it and/or modify "Legal Entity" shall mean the union of the acting entity and all
it under the terms of the GNU General Public License as published by other entities that control, are controlled by, or are under common
the Free Software Foundation, either version 3 of the License, or control with that entity. For the purposes of this definition,
(at your option) any later version. "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
This program is distributed in the hope that it will be useful, "You" (or "Your") shall mean an individual or Legal Entity
but WITHOUT ANY WARRANTY; without even the implied warranty of exercising permissions granted by this License.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License "Source" form shall mean the preferred form for making modifications,
along with this program. If not, see <http://www.gnu.org/licenses/>. including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+10 -52
View File
@@ -1,56 +1,14 @@
# Carte Noire # Abit
A simple Jekyll theme for blogging. Not named after the coffee. A Bitmessage client for Android.
![Homepage](http://i.imgur.com/xlmHArV.png) **Please be aware that due to the protocol, sending messages might suck your
battery dry. Also, it causes a lot of traffic and may use up your data plan
rather quickly. Use at your own peril.**
### Article Abit uses the [Jabit Bitmessage library](https://github.com/Dissem/Jabit).
![Article](http://i.imgur.com/8rD8FfC.png)
### Disqus Comments ## Requirements
![Comments](http://i.imgur.com/TBZHQwF.png) You'll need at least Android 4.4 KitKat. Due to the Proof of Work that comes
with the protocol you might want as fast a processor and as many cores as
### Posts grouped by year you can get.
![All Posts](http://i.imgur.com/9bNs2Sc.png)
### JavaScript Search
![Search](http://i.imgur.com/yQqMeSl.png)
### Menu by mmenu
![Menu](http://i.imgur.com/SClrNSH.png)
## Contact
If you wish to contact me regarding this theme please raise an issue on GitHub,
tweet me [@_jacobtomlinson](http://www.twitter.com/_jacobtomlinson) or email me
[jacob@jacobtomlinson.co.uk](mailto:jacob@jacobtomlinson.co.uk).
## Contribution
Pull requests are very welcome.
## Theme
This jekyll theme has been created from scratch. Ideas and inspiration are taken
from other places but the code is my own.
## Tools and Libraries
The following tools and libraries are used in this theme
### JavaScript
* [jQuery](http://jquery.com/)
* [MMenu](http://mmenu.frebsite.nl/)
* [HighlightJS](https://highlightjs.org/)
* [Simple Jekyll Search](https://github.com/christian-fei/Simple-Jekyll-Search)
### CSS
* [Bootstrap](http://getbootstrap.com/)
* [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
### Social
* [AddThis](http://www.addthis.com/)
* [Disqus](https://disqus.com/)
### Other
* [Real Favicon Generator](http://realfavicongenerator.net/)
* [Google Analytics](http://www.google.com/analytics/)
## License
The jekyll theme, HTML, CSS and JavaScript is licensed under GPLv3 (unless stated otherwise in the file).
-40
View File
@@ -1,40 +0,0 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely need to edit after that.
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'jekyll serve'. If you change this file, please restart the server process.
# Site settings
title: Abit
email: chrigu.meyer@gmail.com
description: > # this means to ignore newlines until "baseurl:"
Write an awesome description for your new site here. You can edit this
line in _config.yml. It will appear in your document head meta (for
Google search results) and in your feed.xml site description.
baseurl: "/Abit" # the subpath of your site, e.g. /blog
url: "https://dissem.github.io" # the base hostname & protocol for your site
title_description: "A Bitmessage client for Android™"
title_image: "ic_launcher-web.png"
#google_analytics: "UA-20365477-4"
#disqus_account: cartenoire
github_repository: https://github.com/Dissem/Abit
# Social usernames/URLs
#twitter_username: jekyllrb
github_username: Dissem
# Footer
footer_left: "Made with <i class=\"fa fa-heart\"></i> by <a href=\"https://twitter.com/Dissem\">Christian Basler</a>"
footer_right: "&lt;/&gt; on <a href=\"https://github.com/Dissem/Abit\">Github</a> &nbsp;<i class=\"fa fa-github-alt\"></i>"
# Build settings
markdown: kramdown
kramdown:
input: GFM
syntax_highlighter: rouge
permalink: pretty
exclude: [vendor]
-2
View File
@@ -1,2 +0,0 @@
gravatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=500&d=mm"
jekyll: "https://i.imgur.com/aRQcGSi.png"
-86
View File
@@ -1,86 +0,0 @@
<div class="footer clearfix">
<div class="col-md-6">
{{ site.footer_left }}
</div>
<div class="col-md-6">
{{ site.footer_right }}
</div>
</div>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="{{ site.baseurl }}/js/jquery.mmenu.min.all.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script type="text/javascript">
$(document).ready(function() {
$("#my-menu").mmenu().on( "closed.mm", function() {
$(".menu-button").show();
});
$(".menu-button").click(function() {
$(".menu-button").hide();
$("#my-menu").trigger("open.mm");
});
});
</script>
{% if page.make-smaller-titles %}
<script type="text/javascript">
function setFontSize() {
var title, dateOfTitle, fontSizeOfTitle, listOfA, listOfSmall, listOfArticlesDiv, divWidth;
listOfArticlesDiv = document.getElementsByClassName("articles");
for (i = 0; i < listOfArticlesDiv.length; i++) {
listOfA = document.getElementsByClassName("articles")[i].getElementsByTagName("a");
listOfSmall = document.getElementsByClassName("articles")[i].getElementsByTagName("small");
divWidth = document.getElementsByClassName("articles")[i].offsetWidth;
for (k = 0; k < listOfSmall.length; k++) {
title = $(listOfA[k]);
dateOfTitle = $(listOfSmall[k]);
fontSizeOfTitle = startingFontSize;
title.css("font-size", fontSizeOfTitle);
while (title.width() + dateOfTitle.width() >= divWidth)
title.css("font-size", fontSizeOfTitle -= 0.5);
}
}
}
function getStartFontSize() {
try {
startingFontSize = parseInt($(document.getElementsByClassName("articles")[0].getElementsByTagName("a")[0]).css("font-size"));
setFontSize();
window.addEventListener('resize', setFontSize, true);
} catch (e) {}
}
$(document).ready(getStartFontSize);
</script>
{% endif %}
{% if site.google_analytics %}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{ site.google_analytics }}']);
_gaq.push(['_trackPageview']);
(function () {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
{% endif %}
-45
View File
@@ -1,45 +0,0 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
<meta name="description" content="{{ site.description }}">
<link rel="profile" href="https://gmpg.org/xfn/11" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/style.css" />
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/jquery.mmenu.all.css" />
<link rel="stylesheet" href="{{ site.baseurl }}/css/idea.css">
<!-- Favicons generated at http://realfavicongenerator.net/ -->
<link rel="apple-touch-icon" sizes="57x57" href="{{ site.baseurl }}/favicons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ site.baseurl }}/favicons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ site.baseurl }}/favicons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{ site.baseurl }}/favicons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="{{ site.baseurl }}/favicons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ site.baseurl }}/favicons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ site.baseurl }}/favicons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ site.baseurl }}/favicons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ site.baseurl }}/favicons/apple-touch-icon-180x180.png">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/android-chrome-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="{{ site.baseurl }}/favicons/manifest.json">
<link rel="shortcut icon" href="{{ site.baseurl }}/favicons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-TileImage" content="{{ site.baseurl }}/favicons/mstile-144x144.png">
<meta name="msapplication-config" content="{{ site.baseurl }}/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
{% if site.addthis_id %}
<!-- Go to www.addthis.com/dashboard to customize your tools -->
<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid={{ site.addthis_id }}"></script>
{% endif %}
{% if page.content contains '<script type="math/tex' %}
<!-- MathJax for LaTeX -->
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{% endif %}
</head>
-14
View File
@@ -1,14 +0,0 @@
<nav id="my-menu">
<div>
<p>{{ site.title }}</p>
<ul class="pages">
<li><a href="{{ site.baseurl }}/"><i class="fa fa-home"></i> Home</a></li>
<li><a href="{{ site.baseurl }}/posts/"><i class="fa fa-archive"></i> All Posts</a></li>
<li><a href="{{ site.baseurl }}/search/"><i class="fa fa-search"></i> Search</a></li>
</ul>
{% include social_links.html %}
</div>
</nav>
<div class="menu-button" href="#menu"><i class="fa fa-bars"></i></div>
-8
View File
@@ -1,8 +0,0 @@
<p class="links">
{% if site.twitter_username %}<a href="https://www.twitter.com/{{ site.twitter_username }}" target="_new"><i class="fa fa-twitter"></i></a>{% endif %}
{% if site.linkedin_link %}<a href="{{ site.linkedin_link }}" target="_new"><i class="fa fa-linkedin"></i></a>{% endif %}
{% if site.google_plus_link %}<a href="{{ site.google_plus_link }}" target="_new"><i class="fa fa-google-plus"></i></a>{% endif %}
{% if site.github_username %}<a href="https://github.com/{{ site.github_username }}" target="_new"><i class="fa fa-github-alt"></i></a>{% endif %}
{% if site.stackoverflow_link %}<a href="{{ site.stackoverflow_link }}" target="_new"><i class="fa fa-stack-overflow"></i></a>{% endif %}
<a href="{{ site.baseurl }}/feed.xml" target="_new"><i class="fa fa-rss"></i></a>
</p>
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<body>
{% include header.html %}
<div class="page-content">
<div class="wrap">
{{ content }}
</div>
</div>
{% include footer.html %}
</body>
</html>
-1
View File
@@ -1 +0,0 @@
{{ content }}
-153
View File
@@ -1,153 +0,0 @@
---
layout: default
---
<div class="container-fluid single">
<div class="row">
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
{% if site.data.thumbnail[page.thumbnail] %}
<div class="thumb">
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
</div>
{% elsif page.thumbnail %}
<div class="thumb">
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
</div>
{% endif %}
<h1 class="header" itemprop="name">{{ page.title }}</h1>
<div class="content-panel content">
{% if page.series %}
This post is part of the series '{{ page.series }}':
<ol class="series">
{% for apost in site.posts reversed %}
{% if page.series == apost.series %}
<li>
{% if page.title == apost.title %}
{% assign nextpost = true %}
{{ apost.title }}
{% else %}
{% if nextpost == true %}
{% assign seriesnext = apost %}
{% endif %}
{% assign nextpost = false %}
<a href="{{ apost.url }}">{{ apost.title }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
<span itemprop="articleBody">{{ content }}</span>
{% if page.series %}
{% if seriesnext %}
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
{% endif %}
{% endif %}
{% if site.addthis_id %}
<div class="share">
<!-- Go to www.addthis.com/dashboard to customize your tools -->
<div class="addthis_sharing_toolbox"></div>
</div>
{% endif %}
{% if page.tags and page.tags.size > 0 %}
<div class="tags">
<small>
<i class="fa fa-tags"></i>
{{ page.tags | join: ', ' }}
</small>
</div>
{% endif %}
</div>
{% if site.twitter_username and site.disqus_account %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
</div>
{% elsif site.twitter_username %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
</div>
{% elsif site.disqus_account %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article in the comment section below!
</div>
{% endif %}
{% if site.disqus_account %}
<div class="content-panel comments">
<div id="disqus_thread">
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
</div>
</div>
{% endif %}
{% if site.related_posts.length > 0 %}
<div class="content-panel related clearfix">
{% for post in site.related_posts limit:1 %}
<div class="related-header">
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
</div>
<div class="title">
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
</div>
<div class="excerpt">
{% if post.summary %}
{{ post.summary | strip_html | truncatewords:30 }}
{% else %}
{{ post.excerpt | strip_html | truncatewords:30 }}
{% endif %}
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
</div>
{% endfor %}
<hr />
<div class="previous previous-next">
{% if page.previous %}
<p>
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
</p>
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
{% endif %}
</div>
<div class="next previous-next">
{% if page.next %}
<p>
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
</p>
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% if site.disqus_account %}
<script type="text/javascript">
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
function disqus_config() { this.experiment.enable_scroll_container = true; }
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
/* * * DON'T EDIT BELOW THIS LINE * * */
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
{% endif %}
-187
View File
@@ -1,187 +0,0 @@
---
layout: default
---
{% if page.minutes %}
{% assign minutes = page.minutes %}
{% else %}
{% assign minutes = content | number_of_words | divided_by: 180 %}
{% if minutes == 0 %}{% assign minutes = 1 %}{% endif %}
{% endif %}
<div class="container-fluid single">
<div class="row">
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
{% if site.data.thumbnail[page.thumbnail] %}
<div class="thumb">
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
</div>
{% elsif page.thumbnail %}
<div class="thumb">
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
</div>
{% endif %}
<h1 class="header" itemprop="name">{{ page.title }}</h1>
<div class="author">
<small><i>
{% if page.author %}
by
<span itemprop="author">
{% if site.google_plus_link %}
<a rel="author" href="{{ site.google_plus_link }}">
{% endif %}
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
<span itemprop="name">{{ page.author }}</span>
</span>
{% if site.google_plus_link %}
</a>
{% endif %}
</span>
{% endif %}
on <span itemprop="datePublished" content="2014-08-28">{{ page.date | date: "%B %-d, %Y" }}</span>
{% if page.categories != empty %} under {% for category in page.categories limit:1 %}{{ category }}{% endfor %}{% endif %}
</i></small>
</div>
<div class="read-time">
<small>
{{ minutes }} minute read
</small>
</div>
<div class="content-panel content">
{% if page.series %}
This post is part of the series '{{ page.series }}':
<ol class="series">
{% for apost in site.posts reversed %}
{% if page.series == apost.series %}
<li>
{% if page.title == apost.title %}
{% assign nextpost = true %}
{{ apost.title }}
{% else %}
{% if nextpost == true %}
{% assign seriesnext = apost %}
{% endif %}
{% assign nextpost = false %}
<a href="{{ apost.url }}">{{ apost.title }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
<span itemprop="articleBody">{{ content }}</span>
{% if page.series %}
{% if seriesnext %}
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
{% endif %}
{% endif %}
{% if site.addthis_id %}
<div class="share">
<!-- Go to www.addthis.com/dashboard to customize your tools -->
<div class="addthis_sharing_toolbox"></div>
</div>
{% endif %}
{% if page.tags and page.tags.size > 0 %}
<div class="tags">
<small>
<i class="fa fa-tags"></i>
{{ page.tags | join: ', ' }}
</small>
</div>
{% endif %}
</div>
{% if site.twitter_username and site.disqus_account %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
</div>
{% elsif site.twitter_username %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
</div>
{% elsif site.disqus_account %}
<div class="content-panel feedback">
I <i class="fa fa-heart"></i> feedback.<br />
Let me know what you think of this article in the comment section below!
</div>
{% endif %}
{% if site.disqus_account %}
<div class="content-panel comments">
<div id="disqus_thread">
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
</div>
</div>
{% endif %}
{% if site.related_posts.length > 0 %}
<div class="content-panel related clearfix">
{% for post in site.related_posts limit:1 %}
<div class="related-header">
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
</div>
<div class="title">
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
</div>
<div class="excerpt">
{% if post.summary %}
{{ post.summary | strip_html | truncatewords:30 }}
{% else %}
{{ post.excerpt | strip_html | truncatewords:30 }}
{% endif %}
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
</div>
{% endfor %}
<hr />
<div class="previous previous-next">
{% if page.previous %}
<p>
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
</p>
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
{% endif %}
</div>
<div class="next previous-next">
{% if page.next %}
<p>
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
</p>
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% if site.disqus_account %}
<script type="text/javascript">
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
function disqus_config() { this.experiment.enable_scroll_container = true; }
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
/* * * DON'T EDIT BELOW THIS LINE * * */
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
{% endif %}
-59
View File
@@ -1,59 +0,0 @@
---
layout: post
title: Using thumbnails
date: 2014-06-08 12:32:18
summary: Using thumbnails in your Carte Noire articles.
categories: jekyll
thumbnail: jekyll
tags:
- thumbnails
- carte noire
---
Carte Noire is designed to start each article with an all-white image as a
thumbnail. These are created by adding a `thumbnail` parameter to the article's
[YAML frontmatter][1]. This thumbnail parameter is processed in one of two ways,
images specified in `_data/thumbnails.yml` or using [Font Awesome][2].
## Images
To use your own custom images as a thumbnail you must upload them to a web available
location (I use [Imgur][3]) and then you need to add the url to `_data/thumbnail.yml`
with an associated keyword.
```
jekyll: "http://i.imgur.com/aRQcGSi.png"
```
You then add a `thumbnail` option to the article's frontmatter and provide the keyword
for that thumbnail.
```
thumbnail: jekyll
```
This allows you to re-use thumbnails across multiple articles without having to
specify the url each time.
## Font Awesome
If jekyll can't find a corresponding image in your `thumbnail.yml` file then it
will assume you want to use a Font Awesome icon instead. You can find the full
list of Font Awesome icons [here][4].
So for example if your article is about android and you want to use the [android icon][5]
from font awesome you can just specify the following in your frontmatter.
```
thumbnail: android
```
Then in the future if you decide you want to use your own android icon you can just
add it to `_data/thumbnails.yml` which will override it for all articles using
the android thumbnail.
[1]: http://jekyllrb.com/docs/frontmatter/
[2]: http://fortawesome.github.io/Font-Awesome/
[3]: http://imgur.com/
[4]: http://fortawesome.github.io/Font-Awesome/icons/
[5]: http://fortawesome.github.io/Font-Awesome/icon/android/
-21
View File
@@ -1,21 +0,0 @@
---
layout: post
title: So, What is Jekyll?
date: 2014-06-09 12:32:18
summary: Transform your plain text into static websites and blogs. Simple, static, and blog-aware.
categories: jekyll
thumbnail: jekyll
tags:
- about
- jekyll
---
Jekyll is a tool for transforming your plain text into static websites and
blogs. It is simple, static, and blog-aware. Jekyll uses the
[Liquid](http://docs.shopify.com/themes/liquid-basics) templating
language and has builtin [Markdown](http://daringfireball.net/projects/markdown/)
and [Textile](http://en.wikipedia.org/wiki/Textile_(markup_language)) support.
It also ties in nicely to [Github Pages](https://pages.github.com/).
Learn more about Jekyll on their [website](http://jekyllrb.com/).
-105
View File
@@ -1,105 +0,0 @@
---
layout: post
title: Carte Noire in Action
date: 2014-06-10 12:31:19
summary: See what the different elements looks like.
categories: jekyll
thumbnail: cogs
tags:
- demo
- action
- carte
- noire
---
**Note** - This article is a derivative of ["See pixyll in action"][1], taken from the lovely jekyll theme [pixyll][4].
All links are easy to [locate and discern](#), yet don't detract from the harmony
of a paragraph. The _same_ goes for italics and __bold__ elements. Even the the strikeout
works if <del>for some reason you need to update your post</del>. For consistency's sake,
<ins>The same goes for insertions</ins>, of course.
### Code, with syntax highlighting
Code blocks use the [peppermint][2] theme.
{% highlight ruby %}
class Awesome < ActiveRecord::Base
include EvenMoreAwesome
validates_presence_of :something
validates :email, email_format: true
def initialize(email, name = nil)
self.email = email
self.name = name
end
end
{% endhighlight %}
```html
<!DOCTYPE html>
<title>Title</title>
<style>body {width: 500px;}</style>
<script type="application/javascript">
function $init() {return true;}
</script>
<body>
<p checked class="title" id='title'>Title</p>
<!-- here goes the rest of the page -->
</body>
```
# Headings!
They're responsive, and well-proportioned (in `padding`, `line-height`, `margin`, and `font-size`).
##### They draw the perfect amount of attention
This allows your content to have the proper informational and contextual hierarchy. Yay.
### There are lists, too
* Apples
* Oranges
* Potatoes
* Milk
1. Mow the lawn
2. Feed the dog
3. Dance
### Images look great, too
![Thumper](https://i.imgur.com/DMCHDqF.jpg)
### Stylish blockquotes included
You can use the markdown quote syntax, `>` for simple quotes.
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse quis porta mauris.
### LaTeX support
The default math delimiters are \$\$. Hence `$$ E = m \cdot c^2 $$` yields $$ E = m \cdot c^2 $$
And here's something more fancy:
$$ \zeta(s) = \frac{1}{\Gamma(s)} \int \limits_0^\infty x^{s-1} \sum_{n=1}^\infty e^{-nx} \mathrm{d}x = \frac{1}{\Gamma(s)} \int \limits_0^\infty \frac{x^{s-1}}{e^x - 1} \mathrm{d}x $$
### There's more being added all the time
Checkout the [Github repository][3] to request,
or add, features.
Happy writing.
[1]: http://pixyll.com/jekyll/pixyll/2014/06/10/see-pixyll-in-action/
[2]: https://noahfrederick.com/log/lion-terminal-theme-peppermint/
[3]: https://github.com/jacobtomlinson/carte-noire
[4]: http://pixyll.com/
@@ -1,28 +0,0 @@
---
layout: post
title: Welcome to Carte Noire
date: 2015-03-23 15:31:19
author: Jacob Tomlinson
summary: Carte Noire is a dark blog theme for Jekyll focusing on a clear reading experience.
categories: jekyll
thumbnail: heart
tags:
- welcome
- to
- carte
- noire
---
Welcome to Carte Noire.
Carte Noire began as a new theme for [my personal blog][1], but has now taken
on a life of its own as a free theme for Jekyll.
The theme has been designed with simplicity and readability in mind. It makes
use of third party services such as Disqus ad AddThis to ensure the blog has
all the features you would expect from a dynamic application such as Wordpress
but with the hosting and maintenance simplicity of Jekyll.
Please use/copy/share Carte Noire!
[1]: http://www.jacobtomlinson.co.uk/
+1
View File
@@ -0,0 +1 @@
/build
+124
View File
@@ -0,0 +1,124 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'idea'
ext {
appName = "Abit"
}
if (project.hasProperty("project.configs")
&& new File(project.property("project.configs") + appName + ".gradle").exists()) {
apply from: project.property("project.configs") + appName + ".gradle"
}
//noinspection GroovyMissingReturnStatement
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
signingConfigs {
release
}
defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 21
targetSdkVersion 28
versionCode 23
versionName "1.0-rc1"
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
packagingOptions {
exclude 'META-INF/core.kotlin_module'
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
//ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
ext.supportVersion = '27.1.1'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version"
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.multidex:multidex:2.0.0'
implementation 'androidx.core:core-ktx:1.0.0'
implementation 'androidx.sqlite:sqlite-ktx:2.0.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.0.0'
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
testImplementation "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
implementation 'org.slf4j:slf4j-android:1.7.25'
implementation 'com.mikepenz:materialize:1.2.0-rc01@aar'
implementation('com.mikepenz:materialdrawer:6.1.0-rc01.2@aar') {
transitive = true
}
implementation('com.mikepenz:aboutlibraries:6.2.0-rc01@aar') {
transitive = true
}
implementation "com.mikepenz:iconics-core:3.1.0-rc01@aar"
implementation "com.mikepenz:iconics-views:3.1.0-rc01@aar"
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
implementation 'com.github.angads25:filepicker:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation "org.robolectric:shadows-multidex:3.7.1"
androidTestImplementation "androidx.multidex:multidex:2.0.0"
}
idea.module {
downloadJavadoc = true
downloadSources = true
}
+17
View File
@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/chris/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
+173
View File
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="ch.dissem.apps.abit">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MessageDetailActivity"
android:label="@string/title_message_detail"
android:parentActivityName=".MainActivity"
tools:ignore="UnusedAttribute">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<activity
android:name=".AddressDetailActivity"
android:label="@string/title_subscription_detail"
android:parentActivityName=".MainActivity"
tools:ignore="UnusedAttribute">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<activity
android:name=".dialog.FullNodeDialogActivity"
android:label="@string/full_node"
android:theme="@style/Theme.AppCompat.Light.Dialog" />
<activity
android:name=".ComposeMessageActivity"
android:label="@string/compose_message"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="bitmessage" />
<data android:scheme="bitmsg" />
<data android:scheme="bm" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="text/plain" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".CreateAddressActivity"
android:label="@string/title_activity_open_bitmessage_link"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="bitmessage" />
<data android:scheme="bitmsg" />
<data android:scheme="bm" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<activity
android:name=".ImportIdentityActivity"
android:label="@string/title_import_identity"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data
android:host="*"
android:mimeType="*/*"
android:pathPattern=".*\\.dat"
android:scheme="file" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<service
android:name=".service.ProofOfWorkService"
android:exported="false" />
<!-- Exports -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="ch.dissem.apps.abit.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".service.BitmessageIntentService"
android:exported="false" />
<!-- Receive Wi-Fi connection state changes -->
<receiver
android:name=".service.StartServiceReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".service.NodeStartupService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".service.CleanupService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".service.BatchProcessorService"
android:exported="false" />
<activity
android:name=".StatusActivity"
android:label="@string/title_activity_status"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
</application>
</manifest>
@@ -0,0 +1,8 @@
CREATE TABLE Inventory (
hash BINARY(32) NOT NULL PRIMARY KEY,
stream INTEGER NOT NULL,
expires INTEGER NOT NULL,
data BLOB NOT NULL,
type INTEGER NOT NULL,
version INTEGER NOT NULL
);
@@ -0,0 +1,8 @@
CREATE TABLE Address (
address VARCHAR(40) NOT NULL PRIMARY KEY,
version INTEGER NOT NULL,
alias VARCHAR(255),
public_key BLOB,
private_key BLOB,
subscribed BIT DEFAULT '0'
);
@@ -0,0 +1,42 @@
CREATE TABLE Message (
id INTEGER PRIMARY KEY AUTOINCREMENT,
iv BINARY(32) UNIQUE,
type VARCHAR(20) NOT NULL,
sender VARCHAR(40) NOT NULL,
recipient VARCHAR(40),
data BLOB NOT NULL,
sent INTEGER,
received INTEGER,
status VARCHAR(20) NOT NULL,
initial_hash BINARY(64) UNIQUE,
FOREIGN KEY (sender) REFERENCES Address (address),
FOREIGN KEY (recipient) REFERENCES Address (address)
);
CREATE TABLE Label (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label VARCHAR(255) NOT NULL,
type VARCHAR(20),
color INT NOT NULL DEFAULT 4278190080, -- FF000000
ord INTEGER,
CONSTRAINT UC_label UNIQUE (label),
CONSTRAINT UC_order UNIQUE (ord)
);
CREATE TABLE Message_Label (
message_id INTEGER NOT NULL,
label_id INTEGER NOT NULL,
PRIMARY KEY (message_id, label_id),
FOREIGN KEY (message_id) REFERENCES Message (id),
FOREIGN KEY (label_id) REFERENCES Label (id)
);
INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0);
INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10);
INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20);
INSERT INTO Label(label, type, ord) VALUES ('Broadcast', 'BROADCAST', 50);
INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90);
INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100);
@@ -0,0 +1,7 @@
-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration
-- wasn't really necessary yet.
--
-- This file is here to reduce confusion regarding to the original migration files.
--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64);
--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash);
@@ -0,0 +1,7 @@
CREATE TABLE POW (
initial_hash BINARY(64) PRIMARY KEY,
data BLOB NOT NULL,
version BIGINT NOT NULL,
nonce_trials_per_byte BIGINT NOT NULL,
extra_bytes BIGINT NOT NULL
);
@@ -0,0 +1 @@
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';
@@ -0,0 +1,2 @@
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
ALTER TABLE POW ADD COLUMN message_id BIGINT;
@@ -0,0 +1,4 @@
ALTER TABLE Message ADD COLUMN ack_data BINARY(32);
ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0;
ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0;
ALTER TABLE Message ADD COLUMN next_try BIGINT;
@@ -0,0 +1,9 @@
CREATE TABLE Node (
stream BIGINT NOT NULL,
address BINARY(32) NOT NULL,
port INT NOT NULL,
services BIGINT NOT NULL,
time BIGINT NOT NULL,
PRIMARY KEY (stream, address, port)
);
CREATE INDEX idx_time on Node(time);
@@ -0,0 +1 @@
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);
@@ -0,0 +1,11 @@
ALTER TABLE Message ADD COLUMN conversation BINARY[16];
CREATE TABLE Message_Parent (
parent BINARY(64) NOT NULL,
child BINARY(64) NOT NULL,
pos INT NOT NULL,
conversation BINARY[16] NOT NULL,
PRIMARY KEY (parent, child),
FOREIGN KEY (child) REFERENCES Message (iv)
);

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,147 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.ListFragment
import android.view.View
import android.widget.ListView
import ch.dissem.apps.abit.listener.ListSelectionListener
/**
* @author Christian Basler
*/
abstract class AbstractItemListFragment<in L, T> : ListFragment(), ListHolder<L> {
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
@Suppress("UNCHECKED_CAST")
private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T>
/**
* The current activated item position. Only used on tablets.
*/
private var activatedPosition = ListView.INVALID_POSITION
private var activateOnItemClick: Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Restore the previously serialized activated item position.
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION))
}
}
override fun onResume() {
super.onResume()
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
listView.choiceMode = if (activateOnItemClick)
ListView.CHOICE_MODE_SINGLE
else
ListView.CHOICE_MODE_NONE
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// Activities containing this fragment must implement its callbacks.
if (context is ListSelectionListener<*>) {
@Suppress("UNCHECKED_CAST")
callbacks = context as ListSelectionListener<T>
} else {
throw IllegalStateException("Activity must implement fragment's callbacks.")
}
}
override fun onDetach() {
super.onDetach()
// Reset the active callbacks interface to the dummy implementation.
@Suppress("UNCHECKED_CAST")
callbacks = DummyCallback as ListSelectionListener<T>
}
override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) {
super.onListItemClick(listView, view, position, id)
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
@Suppress("UNCHECKED_CAST")
(listView.getItemAtPosition(position) as? T)?.let {
callbacks.onItemSelected(it)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (activatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition)
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
if (isVisible) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
listView.choiceMode = if (activateOnItemClick)
ListView.CHOICE_MODE_SINGLE
else
ListView.CHOICE_MODE_NONE
}
}
private fun setActivatedPosition(position: Int) {
if (position == ListView.INVALID_POSITION) {
listView.setItemChecked(activatedPosition, false)
} else {
listView.setItemChecked(position, true)
}
activatedPosition = position
}
override fun showPreviousList() = false
/**
* A dummy implementation of the [ListSelectionListener] interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
internal object DummyCallback : ListSelectionListener<Any> {
override fun onItemSelected(item: Any) = Unit // NO OP
}
companion object {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
internal const val STATE_ACTIVATED_POSITION = "activated_position"
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
/**
* An activity representing a single Subscription detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a [MainActivity].
*
*
* This activity is mostly just a 'shell' activity containing nothing
* more than a [AddressDetailFragment].
*/
class AddressDetailActivity : DetailActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
val arguments = Bundle()
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM))
val fragment = AddressDetailFragment()
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.add(R.id.content, fragment)
.commit()
}
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.Toast
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.qrCode
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.wif.WifExporter
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import kotlinx.android.synthetic.main.fragment_address_detail.*
/**
* A fragment representing a single Message detail screen.
* This fragment is either contained in a [MainActivity]
* in two-pane mode (on tablets) or a [MessageDetailActivity]
* on handsets.
*/
class AddressDetailFragment : Fragment() {
/**
* The content this fragment is presenting.
*/
private var item: BitmessageAddress? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
if (arguments.containsKey(ARG_ITEM)) {
item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress
}
}
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.address, menu)
val ctx = activity!!
Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail)
Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share)
Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val item = item ?: return false
val ctx = activity ?: return false
when (menuItem.itemId) {
R.id.write_message -> {
val identity = Singleton.getIdentity(ctx)
if (identity == null) {
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show()
} else {
val intent = Intent(ctx, ComposeMessageActivity::class.java)
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity)
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item)
startActivity(intent)
}
return true
}
R.id.delete -> {
val warning = if (item.privateKey != null)
R.string.delete_identity_warning
else
R.string.delete_contact_warning
AlertDialog.Builder(ctx)
.setMessage(warning)
.setPositiveButton(android.R.string.yes) { _, _ ->
Singleton.getAddressRepository(ctx).remove(item)
MainActivity.apply {
if (item.privateKey != null) {
removeIdentityEntry(item)
}
}
this.item = null
ctx.onBackPressed()
}
.setNegativeButton(android.R.string.no, null)
.show()
return true
}
R.id.export -> {
AlertDialog.Builder(ctx)
.setMessage(R.string.confirm_export)
.setPositiveButton(android.R.string.yes) { _, _ ->
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_TITLE,
"$item$EXPORT_POSTFIX"
)
putExtra(
Intent.EXTRA_TEXT,
WifExporter(Singleton.getBitmessageContext(ctx)).apply {
addIdentity(item)
}.toString()
)
}
startActivity(Intent.createChooser(shareIntent, null))
}
.setNegativeButton(android.R.string.no, null)
.show()
return true
}
R.id.share -> {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, item.address)
startActivity(Intent.createChooser(shareIntent, null))
return true
}
else -> return false
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
= inflater.inflate(R.layout.fragment_address_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Show the dummy content as text in a TextView.
item?.let { item ->
activity?.let { activity ->
when {
item.isChan -> activity.setTitle(R.string.title_chan_detail)
item.privateKey != null -> activity.setTitle(R.string.title_identity_detail)
item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail)
else -> activity.setTitle(R.string.title_contact_detail)
}
}
avatar.setImageDrawable(Identicon(item))
name.setText(item.toString())
name.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do
override fun afterTextChanged(s: Editable) {
item.alias = s.toString()
}
})
address.text = item.address
address.isSelected = true
stream_number.text = getString(R.string.stream_number, item.stream)
if (item.privateKey == null) {
active.isChecked = item.isSubscribed
active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked }
if (item.pubkey == null) {
pubkey_available.alpha = 0.3f
pubkey_available_desc.setText(R.string.pubkey_not_available)
}
} else {
active.visibility = View.GONE
pubkey_available.visibility = View.GONE
pubkey_available_desc.visibility = View.GONE
}
// QR code
qr_code.setImageBitmap(item.qrCode())
}
}
override fun onPause() {
item?.let { item ->
Singleton.getAddressRepository(context!!).save(item)
if (item.privateKey != null) {
MainActivity.apply { updateIdentityEntry(item) }
}
}
super.onPause()
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
const val ARG_ITEM = "item"
const val EXPORT_POSTFIX = ".keys.dat"
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
/**
* Fragment that shows a list of all contacts, the ones we subscribed to first.
*/
class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() {
private lateinit var adapter: ArrayAdapter<BitmessageAddress>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = object : ArrayAdapter<BitmessageAddress>(
activity!!,
R.layout.subscription_row,
R.id.name,
LinkedList()
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val result: View
val v: ViewHolder
if (convertView == null) {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.subscription_row, parent, false)
v = ViewHolder(
ctx = context,
avatar = view.findViewById(R.id.avatar),
name = view.findViewById(R.id.name),
streamNumber = view.findViewById(R.id.stream_number),
subscribed = view.findViewById(R.id.subscribed)
)
view.tag = v
result = view
} else {
v = convertView.tag as ViewHolder
result = convertView
}
getItem(position)?.let { item ->
v.avatar.setImageDrawable(Identicon(item))
v.name.text = item.toString()
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
v.subscribed.visibility =
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
}
return result
}
}
listAdapter = adapter
}
override fun onResume() {
super.onResume()
initFab(activity as MainActivity)
reloadList()
}
override fun reloadList() {
adapter.clear()
context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context)
doAsync {
addressRepo.getContactIds()
.map { addressRepo.getAddress(it) }
.forEach { address -> uiThread { adapter.add(address) } }
}
}
}
private fun initFab(activity: MainActivity) {
activity.updateTitle(getString(R.string.contacts_and_subscriptions))
val menu = FabSpeedDialMenu(activity)
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
activity.initFab(R.drawable.ic_action_add_contact, menu)
.addOnMenuItemClickListener { _, _, itemId ->
when (itemId) {
// FIXME
// 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
// .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
// .initiateScan()
2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
startActivity(intent)
}
else -> {
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_address_list, container, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && data.hasExtra("SCAN_RESULT")) {
val uri = Uri.parse(data.getStringExtra("SCAN_RESULT"))
val intent = Intent(activity, CreateAddressActivity::class.java)
intent.data = uri
startActivity(intent)
}
}
override fun updateList(label: Void) = reloadList()
private data class ViewHolder(
val ctx: Context,
val avatar: ImageView,
val name: TextView,
val streamNumber: TextView,
val subscribed: View
)
}
@@ -0,0 +1,116 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
import kotlinx.android.synthetic.main.toolbar_layout.*
/**
* Compose a new message.
*/
class ComposeMessageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.toolbar_layout)
setSupportActionBar(toolbar)
supportActionBar?.apply {
setHomeAsUpIndicator(R.drawable.ic_action_close)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(false)
}
if (supportFragmentManager.findFragmentById(R.id.content) == null) {
// Display the fragment as the main content.
val fragment = ComposeMessageFragment()
fragment.arguments = intent.extras
supportFragmentManager
.beginTransaction()
.replace(R.id.content, fragment)
.commit()
}
}
companion object {
const val EXTRA_DRAFT = "ch.dissem.abit.Message.DRAFT"
const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"
const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"
const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"
const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"
const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"
const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"
const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"
fun launchReplyTo(fragment: Fragment, item: Plaintext) =
fragment.startActivity(
getReplyIntent(
ctx = fragment.activity
?: throw IllegalStateException("Fragment not attached to an activity"),
item = item
)
)
fun launchReplyTo(activity: Activity, item: Plaintext) =
activity.startActivity(getReplyIntent(activity, item))
private fun getReplyIntent(ctx: Context, item: Plaintext): Intent {
val replyIntent = Intent(ctx, ComposeMessageActivity::class.java)
val receivingIdentity = item.to
if (receivingIdentity?.isChan == true) {
// reply to chan, not to the sender of the message
replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity)
// I hate when people send as chan, so it won't be the default behaviour.
replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx))
} else {
replyIntent.putExtra(EXTRA_RECIPIENT, item.from)
replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity)
}
// if the original message was sent using extended encoding, use it as well
// so features like threading can be supported
if (item.encoding == EXTENDED) {
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED)
}
replyIntent.putExtra(EXTRA_PARENT, item)
item.subject?.let { subject ->
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
"RE:",
ignoreCase = true
)
) {
""
} else {
"RE: "
}
replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject)
}
replyIntent.putExtra(
EXTRA_CONTENT,
"\n\n------------------------------------------------------\n${item.text ?: ""}"
)
return replyIntent
}
}
}
@@ -0,0 +1,292 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.*
import android.widget.AdapterView
import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_DRAFT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
import ch.dissem.apps.abit.adapter.ContactAdapter
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.entity.valueobject.extended.Message
import kotlinx.android.synthetic.main.fragment_compose_message.*
/**
* Compose a new message.
*/
class ComposeMessageFragment : Fragment() {
private lateinit var identity: BitmessageAddress
private var recipient: BitmessageAddress? = null
private var subject: String = ""
private var content: String = ""
private var broadcast: Boolean = false
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
private val parents = mutableListOf<InventoryVector>()
private var draft: Plaintext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
arguments?.apply {
val draft = getSerializable(EXTRA_DRAFT) as Plaintext?
if (draft != null) {
this@ComposeMessageFragment.draft = draft
identity = draft.from
recipient = draft.to
subject = draft.subject ?: ""
content = draft.text ?: ""
encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE
parents.addAll(draft.parents)
} else {
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && id?.privateKey == null) {
id = Singleton.getIdentity(context!!)
}
if (id?.privateKey != null) {
identity = id
} else {
throw IllegalStateException("No identity set for ComposeMessageFragment")
}
broadcast = getBoolean(EXTRA_BROADCAST, false)
if (containsKey(EXTRA_RECIPIENT)) {
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
}
if (containsKey(EXTRA_SUBJECT)) {
subject = getString(EXTRA_SUBJECT) ?: throw IllegalStateException("EXTRA_SUBJECT expected")
}
if (containsKey(EXTRA_CONTENT)) {
content = getString(EXTRA_CONTENT) ?: throw IllegalStateException("EXTRA_CONTENT expected")
}
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
if (containsKey(EXTRA_PARENT)) {
val parent = getSerializable(EXTRA_PARENT) as Plaintext
parent.inventoryVector?.let { parents.add(it) }
}
}
} ?: throw IllegalStateException("No identity set for ComposeMessageFragment")
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_compose_message, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { ctx ->
val identities = Singleton.getAddressRepository(ctx).getIdentities()
sender_input.adapter = ContactAdapter(ctx, identities, true)
val index = identities.indexOf(Singleton.getIdentity(ctx))
if (index >= 0) {
sender_input.setSelection(index)
}
if (broadcast) {
recipient_input.visibility = View.GONE
} else {
val adapter = ContactAdapter(
ctx,
Singleton.getAddressRepository(ctx).getContacts()
)
recipient_input.setAdapter(adapter)
recipient_input.onItemClickListener =
AdapterView.OnItemClickListener { _, _, pos, _ -> recipient = adapter.getItem(pos) }
recipient_input.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View,
position: Int,
id: Long
) {
recipient = adapter.getItem(position)
}
override fun onNothingSelected(parent: AdapterView<*>) =
Unit // leave current selection
}
recipient?.let { recipient_input.setText(it.toString()) }
}
subject_input.setText(subject)
body_input.setText(content)
when {
recipient == null -> recipient_input.requestFocus()
subject.isEmpty() -> subject_input.requestFocus()
else -> {
body_input.requestFocus()
body_input.setSelection(0)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
inflater.inflate(R.menu.compose, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.send -> {
send()
return true
}
R.id.select_encoding -> {
val encodingDialog = SelectEncodingDialogFragment()
val args = Bundle()
args.putSerializable(EXTRA_ENCODING, encoding)
encodingDialog.arguments = args
encodingDialog.setTargetFragment(this, 0)
encodingDialog.show(fragmentManager, "select encoding dialog")
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) =
if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding
} else {
super.onActivityResult(requestCode, resultCode, data)
}
private fun build(ctx: Context): Plaintext {
val builder: Plaintext.Builder
if (broadcast) {
builder = Plaintext.Builder(BROADCAST)
} else {
val inputString = recipient_input.text.toString()
if (recipient == null || recipient?.toString() != inputString) {
try {
recipient = BitmessageAddress(inputString)
} catch (e: Exception) {
val contacts = Singleton.getAddressRepository(ctx).getContacts()
for (contact in contacts) {
if (inputString.equals(contact.alias, ignoreCase = true)) {
recipient = contact
if (inputString == contact.alias)
break
}
}
}
}
builder = Plaintext.Builder(MSG)
.to(recipient)
}
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
sender?.let { builder.from(it) }
if (!ctx.preferences.requestAcknowledgements) {
builder.preventAck()
}
when (encoding) {
Plaintext.Encoding.SIMPLE -> builder.message(
subject_input.text.toString(),
body_input.text.toString()
)
Plaintext.Encoding.EXTENDED -> builder.message(
ExtendedEncoding(
Message(
subject = subject_input.text.toString(),
body = body_input.text.toString(),
parents = parents,
files = emptyList()
)
)
)
else -> {
Toast.makeText(
ctx,
ctx.getString(R.string.error_unsupported_encoding, encoding),
Toast.LENGTH_LONG
).show()
builder.message(
subject_input.text.toString(),
body_input.text.toString()
)
}
}
draft?.id?.let { builder.id(it) }
return builder.build()
}
override fun onPause() {
if (draft?.labels?.any { it.type == Label.Type.DRAFT } != false) {
context?.let { ctx ->
draft = build(ctx).also { msg ->
Singleton.labeler.markAsDraft(msg)
Singleton.getMessageRepository(ctx).save(msg)
}
Toast.makeText(ctx, "Message saved as draft", Toast.LENGTH_LONG).show()
} ?: throw IllegalStateException("Context is not available")
}
super.onPause()
}
override fun onDestroyView() {
identity = sender_input.selectedItem as BitmessageAddress
// recipient is set when one is selected
subject = subject_input.text?.toString() ?: ""
content = body_input.text?.toString() ?: ""
super.onDestroyView()
}
private fun send() {
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
if (recipient == null) {
Toast.makeText(ctx, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG)
.show()
return
}
build(ctx).let { message ->
draft = message
Singleton.getBitmessageContext(ctx).send(message)
}
ctx.finish()
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.*
import ch.dissem.apps.abit.adapter.ConversationAdapter
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables
import ch.dissem.bitmessage.entity.Conversation
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import kotlinx.android.synthetic.main.fragment_conversation_detail.*
import java.util.*
/**
* A fragment representing a single Message detail screen.
* This fragment is either contained in a [MainActivity]
* in two-pane mode (on tablets) or a [MessageDetailActivity]
* on handsets.
*/
class ConversationDetailFragment : Fragment() {
/**
* The content this fragment is presenting.
*/
private var itemId: UUID? = null
private var item: Conversation? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
if (arguments.containsKey(ARG_ITEM_ID)) {
// Load the dummy content specified by the fragment
// arguments. In a real-world scenario, use a Loader
// to load content from a content provider.
itemId = arguments.getSerializable(ARG_ITEM_ID) as UUID
}
}
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_conversation_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
item = itemId?.let { Singleton.getConversationService(ctx).getConversation(it) }
// Show the dummy content as text in a TextView.
item?.let { item ->
subject.text = item.subject
avatar.setImageDrawable(MultiIdenticon(item.participants))
messages.adapter = ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel)
messages.layoutManager = LinearLayoutManager(activity)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.conversation, menu)
activity?.let { activity ->
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val messageRepo = Singleton.getMessageRepository(
context ?: throw IllegalStateException("No context available")
)
item?.let { item ->
when (menuItem.itemId) {
R.id.delete -> {
item.messages.forEach {
Singleton.labeler.delete(it)
messageRepo.remove(it)
}
MainActivity.apply { updateUnread() }
activity?.onBackPressed()
return true
}
R.id.archive -> {
item.messages.forEach {
Singleton.labeler.archive(it)
messageRepo.save(it)
}
MainActivity.apply { updateUnread() }
return true
}
else -> return false
}
}
return false
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
const val ARG_ITEM_ID = "item_id"
}
}
@@ -0,0 +1,332 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.adapter.EventListener
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.ConversationService
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.*
import java.util.*
private const val PAGE_SIZE = 15
/**
* A list fragment representing a list of Messages. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a [MessageDetailFragment].
*
*
* Activities containing this fragment MUST implement the [ListSelectionListener]
* interface.
*/
class ConversationListFragment : Fragment(), ListHolder<Label> {
private var isLoading = false
private var isLastPage = false
private var layoutManager: LinearLayoutManager? = null
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
&& firstVisibleItemPosition >= 0
) {
loadMoreItems()
}
}
}
}
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var conversationService: ConversationService
private var activateOnItemClick: Boolean = false
private var subscription: Disposable? = null
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableConversationAdapter?.activateOnItemClick = activateOnItemClick
this.activateOnItemClick = activateOnItemClick
}
private val backStack = Stack<Label>()
fun loadMoreItems() {
isLoading = true
swipeableConversationAdapter?.let { messageAdapter ->
doAsync {
val conversationIds = messageRepo.findConversations(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE,
context?.preferences?.separateIdentities == true
)
conversationIds.forEach { conversationId ->
val conversation = conversationService.getConversation(conversationId)
uiThread {
messageAdapter.add(conversation)
}
}
isLoading = false
isLastPage = conversationIds.size < PAGE_SIZE
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
val activity = activity as MainActivity
initFab(activity)
messageRepo = Singleton.getMessageRepository(activity)
conversationService = Singleton.getConversationService(activity)
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
subscription?.dispose()
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
val mainActivity = activity as? MainActivity
swipeableConversationAdapter?.clear(label)
if (label == null) {
mainActivity?.updateTitle(getString(R.string.app_name))
swipeableConversationAdapter?.notifyDataSetChanged()
return
}
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
deleteAllMenuItem?.isVisible = false
MainActivity.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
} else {
updateTitle(label.toString())
}
}
loadMoreItems()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = context ?: throw IllegalStateException("No context available")
val listener = object : EventListener {
override fun onItemDeleted(position: Int) {
swipeableConversationAdapter?.getItem(position)?.let { item ->
item.messages.forEach {
Singleton.labeler.delete(it)
messageRepo.save(it)
}
}
swipeableConversationAdapter?.removeAt(position)
}
override fun onItemArchived(position: Int) {
swipeableConversationAdapter?.getItem(position)?.let { item ->
item.messages.forEach {
Singleton.labeler.archive(it)
messageRepo.save(it)
}
}
swipeableConversationAdapter?.removeAt(position)
}
override fun onItemSelected(position: Int) {
swipeableConversationAdapter?.selectedPosition = position
if (position != RecyclerView.NO_POSITION) {
swipeableConversationAdapter?.getItem(position)?.let { item ->
MainActivity.apply { onItemSelected(item) }
}
}
}
}
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.layoutManager = layoutManager
swipeableConversationAdapter = SwipeableConversationAdapter(context).apply {
activateOnItemClick = this@ConversationListFragment.activateOnItemClick
eventListener = listener
}
recycler_view.adapter = swipeableConversationAdapter
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
val dirs = when (currentLabel.value?.type) {
Label.Type.TRASH -> ItemTouchHelper.LEFT
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
}
val swipeHandler = SwipeToDeleteCallback(context, dirs, listener)
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(recycler_view)
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
}
private fun initFab(context: MainActivity) {
val menu = FabSpeedDialMenu(context)
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
context.initFab(R.drawable.ic_action_compose_message, menu)
.addOnMenuItemClickListener { _, _, itemId ->
val identity = Singleton.getIdentity(context)
if (identity == null) {
Toast.makeText(
activity, R.string.no_identity_warning,
Toast.LENGTH_LONG
).show()
} else {
when (itemId) {
1 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
intent.putExtra(EXTRA_BROADCAST, true)
startActivity(intent)
}
2 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
startActivity(intent)
}
else -> {
}
}
}
}
}
override fun onDestroyView() {
swipeableConversationAdapter = null
layoutManager = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.empty_trash -> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
deleteAllMessages(label)
}
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
context?.apply {
alert(
R.string.delete_all_messages_in_list,
R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
}
return true
}
else -> return false
}
}
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.onNext(label)
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.onNext(backStack.pop())
true
}
}
@@ -0,0 +1,146 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.app.Activity
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.util.Base64
import android.util.Base64.URL_SAFE
import android.widget.Button
import android.widget.EditText
import android.widget.Switch
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.V2Pubkey
import ch.dissem.bitmessage.entity.payload.V3Pubkey
import ch.dissem.bitmessage.entity.payload.V4Pubkey
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.regex.Pattern
class CreateAddressActivity : AppCompatActivity() {
private var pubkeyBytes: ByteArray? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data
if (uri != null)
setContentView(R.layout.activity_open_bitmessage_link)
else
setContentView(R.layout.activity_create_bitmessage_address)
val address = findViewById<TextView>(R.id.address)
val label = findViewById<EditText>(R.id.label)
val subscribe = findViewById<Switch>(R.id.subscribe)
if (uri != null) {
val addressText = getAddress(uri)
val parameters = getParameters(uri)
for (parameter in parameters) {
val matcher = KEY_VALUE_PATTERN.matcher(parameter)
if (matcher.find()) {
val key = matcher.group(1).toLowerCase()
val value = matcher.group(2)
when (key) {
"label" -> label.setText(value.trim { it <= ' ' })
"action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true)
"pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE)
else -> LOG.debug("Unknown attribute: $key=$value")
}
}
}
address.text = addressText
}
val cancel = findViewById<Button>(R.id.cancel)
cancel.setOnClickListener {
setResult(Activity.RESULT_CANCELED)
finish()
}
findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) }
}
private fun onOK(address: TextView, label: EditText, subscribe: Switch) {
val addressText = address.text.toString().trim { it <= ' ' }
try {
val bmAddress = BitmessageAddress(addressText)
bmAddress.alias = label.text.toString()
val bmc = Singleton.getBitmessageContext(applicationContext)
bmc.addContact(bmAddress)
if (subscribe.isChecked) {
bmc.addSubscribtion(bmAddress)
}
pubkeyBytes?.let { pubkeyBytes ->
try {
val pubkeyStream = ByteArrayInputStream(pubkeyBytes)
val stream = bmAddress.stream
when (bmAddress.version.toInt()) {
2 -> V2Pubkey.read(pubkeyStream, stream)
3 -> V3Pubkey.read(pubkeyStream, stream)
4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream))
else -> null
}?.let { bmAddress.pubkey = it }
} catch (ignore: Exception) {
}
}
setResult(Activity.RESULT_OK)
finish()
} catch (e: RuntimeException) {
address.error = getString(R.string.error_illegal_address)
}
}
private fun getAddress(uri: Uri): String {
val result = StringBuilder()
val schemeSpecificPart = uri.schemeSpecificPart
if (!schemeSpecificPart.startsWith("BM-")) {
result.append("BM-")
}
when {
schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')))
schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')))
else -> result.append(schemeSpecificPart)
}
return result.toString()
}
private fun getParameters(uri: Uri): Array<String> {
val index = uri.schemeSpecificPart.indexOf('?')
return if (index >= 0) {
uri.schemeSpecificPart
.substring(index + 1)
.split("&".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
} else {
emptyArray()
}
}
companion object {
private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java)
private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$")
}
}
@@ -0,0 +1,48 @@
package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import androidx.core.app.NavUtils
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*
/**
* @author Christian Basler
*/
abstract class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.scrolling_toolbar_layout)
setSupportActionBar(toolbar)
// Show the Up button in the action bar.
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
}
MaterializeBuilder()
.withActivity(this)
.withStatusBarColorRes(R.color.colorPrimaryDark)
.withTranslucentStatusBarProgrammatically(true)
.withStatusBarPadding(true)
.build()
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> {
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. Use NavUtils to allow users
// to navigate up one level in the application structure. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -0,0 +1,181 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.graphics.*
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import android.text.TextPaint
import ch.dissem.bitmessage.entity.BitmessageAddress
import org.jetbrains.anko.collections.forEachWithIndex
import kotlin.math.sqrt
/**
* @author Christian Basler
*/
class Identicon(input: BitmessageAddress) : Drawable() {
private val paint = Paint().apply {
style = Paint.Style.FILL
isAntiAlias = true
}
private val hash = input.ripe
private val isChan = input.isChan
private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply {
for (row in 0 until SIZE) {
if (!isChan || row < 5 || row > 6) {
for (column in 0..CENTER_COLUMN) {
if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) {
this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0
this[row][SIZE - column - 1] = this[row][column]
}
}
}
}
}
private val color = Color.HSVToColor(
floatArrayOf(
(Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(),
0.8f,
1.0f
)
)
private val background = Color.HSVToColor(
floatArrayOf(
(Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(),
0.8f,
1.0f
)
)
private val textPaint = TextPaint().apply {
textAlign = Paint.Align.CENTER
color = 0xFF607D8B.toInt()
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
}
override fun draw(canvas: Canvas) {
val width = bounds.width().toFloat()
val height = bounds.height().toFloat()
draw(canvas, 0f, 0f, width, height)
}
internal fun draw(canvas: Canvas, offsetX: Float, offsetY: Float, width: Float, height: Float) {
var x: Float
var y: Float
val cellWidth = width / SIZE.toFloat()
val cellHeight = height / SIZE.toFloat()
paint.color = background
canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
paint.color = color
for (row in 0 until SIZE) {
for (column in 0 until SIZE) {
if (fields[row][column]) {
x = offsetX + cellWidth * column
y = offsetY + cellHeight * row
canvas.drawCircle(
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint
)
}
}
}
if (isChan) {
textPaint.textSize = 2 * cellHeight
canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
}
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
paint.colorFilter = cf
}
override fun getOpacity() = PixelFormat.TRANSPARENT
companion object {
private const val SIZE = 9
private const val CENTER_COLUMN = 5
}
}
class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt private val backgroundColor: Int = 0xFFAEC2CC.toInt()) :
Drawable() {
private val paint = Paint().apply {
style = Paint.Style.FILL
isAntiAlias = true
color = backgroundColor
}
private val identicons = input.asSequence().sortedBy { it.isChan }.map { Identicon(it) }.take(4).toList()
override fun draw(canvas: Canvas) {
val width = bounds.width().toFloat()
val height = bounds.height().toFloat()
when (identicons.size) {
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
1 -> identicons.first().draw(canvas, 0f, 0f, width, height)
2 -> {
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
val w = width / 2
val h = height / 2
var x = 0f
val y = height / 4
identicons.forEach {
it.draw(canvas, x, y, w, h)
x += w
}
}
3 -> {
val scale = 2f / (1f + 2f * sqrt(3f))
val w = width * scale
val h = height * scale
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
}
4 -> {
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
val scale = 1f / (1f + sqrt(2f))
val borderScale = 0.5f - scale
val w = width * scale
val h = height * scale
val x = width * borderScale
val y = height * borderScale
identicons.forEachWithIndex { i, identicon ->
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
}
}
}
}
override fun setAlpha(alpha: Int) {
identicons.forEach { it.alpha = alpha }
}
override fun setColorFilter(colorFilter: ColorFilter?) {
identicons.forEach { it.colorFilter = colorFilter }
}
override fun getOpacity() = PixelFormat.TRANSPARENT
}
@@ -0,0 +1,89 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.wif.WifImporter
import org.ini4j.InvalidFileFormatException
import org.jetbrains.anko.longToast
/**
* @author Christian Basler
*/
class ImportIdentitiesFragment : Fragment() {
private lateinit var adapter: AddressSelectorAdapter
private lateinit var importer: WifImporter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val ctx = activity ?: throw IllegalStateException("No activity available")
super.onViewCreated(view, savedInstanceState)
val wifData = arguments?.getString(WIF_DATA) ?: throw IllegalStateException("No WIF data")
val bmc = Singleton.getBitmessageContext(ctx)
try {
importer = WifImporter(bmc, wifData)
} catch (e: InvalidFileFormatException) {
ctx.longToast(R.string.invalid_wif_file)
ctx.finish()
return
}
adapter = AddressSelectorAdapter(importer.getIdentities())
val layoutManager = LinearLayoutManager(
activity,
RecyclerView.VERTICAL,
false
)
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.HORIZONTAL))
view.findViewById<Button>(R.id.finish).setOnClickListener {
importer.importAll(adapter.selected)
MainActivity.apply {
for (selected in adapter.selected) {
addIdentityEntry(selected)
}
}
ctx.finish()
}
}
companion object {
const val WIF_DATA = "wif_data"
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import androidx.fragment.app.transaction
/**
* @author Christian Basler
*/
class ImportIdentityActivity : DetailActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
if (wifData == null) {
supportFragmentManager.transaction {
replace(R.id.content, InputWifFragment())
}
} else {
val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
val fragment = ImportIdentitiesFragment().apply {
arguments = bundle
}
supportFragmentManager.transaction {
replace(R.id.content, fragment)
}
}
}
}
@@ -0,0 +1,102 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.transaction
import com.github.angads25.filepicker.model.DialogConfigs
import com.github.angads25.filepicker.model.DialogProperties
import com.github.angads25.filepicker.view.FilePickerDialog
import kotlinx.android.synthetic.main.fragment_import_input.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
/**
* @author Christian Basler
*/
class InputWifFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_import_input, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
next.setOnClickListener {
val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString())
val fragment = ImportIdentitiesFragment().apply {
arguments = bundle
}
fragmentManager?.transaction {
replace(R.id.content, fragment)
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.import_input_data, menu)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val properties = DialogProperties()
properties.selection_mode = DialogConfigs.SINGLE_MODE
properties.selection_type = DialogConfigs.FILE_SELECT
properties.root = File(DialogConfigs.DEFAULT_DIR)
properties.error_dir = File(DialogConfigs.DEFAULT_DIR)
properties.extensions = null
val dialog = FilePickerDialog(activity, properties)
dialog.setTitle(getString(R.string.select_file_title))
dialog.setDialogSelectionListener { files ->
if (files.isNotEmpty()) {
try {
FileInputStream(files[0]).use { inputStream ->
val data = ByteArrayOutputStream()
val buffer = ByteArray(1024)
var length: Int = inputStream.read(buffer)
while (length != -1) {
data.write(buffer, 0, length)
length = inputStream.read(buffer)
}
wif_input.setText(data.toByteArray().toString())
}
} catch (e: IOException) {
Toast.makeText(
activity,
R.string.error_loading_data,
Toast.LENGTH_SHORT
).show()
}
}
}
dialog.show()
return true
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
/**
* @author Christian Basler
*/
interface ListHolder<in L> {
fun reloadList()
fun updateList(label: L)
fun setActivateOnItemClick(activateOnItemClick: Boolean)
fun showPreviousList(): Boolean
}
@@ -0,0 +1,585 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle
import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.transaction
import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.getColor
import ch.dissem.apps.abit.util.getIcon
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.AccountHeaderBuilder
import com.mikepenz.materialdrawer.Drawer
import com.mikepenz.materialdrawer.DrawerBuilder
import com.mikepenz.materialdrawer.model.*
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Nameable
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView
import uk.co.deanwild.materialshowcaseview.shape.Shape
import uk.co.deanwild.materialshowcaseview.target.Target
import java.io.Serializable
import java.lang.ref.WeakReference
import java.util.*
/**
* An activity representing a list of Messages. This activity
* has different presentations for handset and tablet-size devices. On
* handsets, the activity presents a list of items, which when touched,
* lead to a [MessageDetailActivity] representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
*
*
* The activity makes heavy use of fragments. The list of items is a
* [MessageListFragment] and the item details
* (if present) is a [MessageDetailFragment].
*
*
* This activity also implements the required
* [ListSelectionListener] interface
* to listen for item selections.
*
*/
class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private var active: Boolean = false
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
var hasDetailPane: Boolean = false
private set
private var subscription: Disposable? = null
private lateinit var bmc: BitmessageContext
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer
private lateinit var nodeSwitch: SwitchDrawerItem
val floatingActionButton: FabSpeedDial?
get() = findViewById(R.id.fab)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this)
messageRepo = Singleton.getMessageRepository(this)
setContentView(R.layout.activity_main)
fab.hide()
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val listFragment = ConversationListFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.item_list, listFragment)
.commit()
if (findViewById<View>(R.id.message_detail_container) != null) {
// The detail container view will be present only in the
// large-screen layouts (res/values-large and
// res/values-sw600dp). If this view is present, then the
// activity should be in two-pane mode.
hasDetailPane = true
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true)
}
createDrawer(toolbar)
// handle intents
val intent = intent
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE))
}
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext
ComposeMessageActivity.launchReplyTo(this, item)
}
if (drawer.isDrawerOpen) {
MaterialShowcaseView.Builder(this)
.setMaskColour(R.color.colorPrimary)
.setTitleText(R.string.full_node)
.setContentText(R.string.full_node_description)
.setDismissOnTouch(true)
.setDismissText(R.string.got_it)
.setShape(object : Shape {
var w = 0
var h = 0
override fun updateTarget(target: Target) {
w = target.bounds.width()
h = target.bounds.height()
}
override fun getHeight() = h
override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
val r = h.toFloat() / 2
canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
}
override fun getWidth() = w
})
.setTarget(drawer.stickyFooter)
.setDelay(1000)
.show()
}
}
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
if (active) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.item_list, listFragment)
supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let {
transaction.remove(it)
}
transaction.addToBackStack(null).commit()
if (hasDetailPane) {
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true)
}
}
}
private fun createDrawer(toolbar: Toolbar) {
val profiles = ArrayList<IProfile<*>>()
profiles.add(
ProfileSettingDrawerItem()
.withName(getString(R.string.add_identity))
.withDescription(getString(R.string.add_identity_summary))
.withIcon(
IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
.actionBar()
.paddingDp(5)
.colorRes(R.color.icons)
)
.withIdentifier(ADD_IDENTITY.toLong())
)
profiles.add(
ProfileSettingDrawerItem()
.withName(getString(R.string.manage_identity))
.withIcon(GoogleMaterial.Icon.gmd_settings)
.withIdentifier(MANAGE_IDENTITY.toLong())
)
// Create the AccountHeader
accountHeader = AccountHeaderBuilder()
.withActivity(this)
.withHeaderBackground(R.drawable.header)
.withProfiles(profiles)
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
.withOnAccountHeaderListener(
ProfileSelectionListener(
this@MainActivity,
supportFragmentManager
)
)
.build()
if (profiles.size > 2) { // There's always the add and manage identity items
accountHeader.setActiveProfile(profiles[0], true)
}
val drawerItems = ArrayList<IDrawerItem<*, *>>()
drawerItems.add(
PrimaryDrawerItem()
.withIdentifier(LABEL_ARCHIVE.id as Long)
.withName(R.string.archive)
.withTag(LABEL_ARCHIVE)
.withIcon(CommunityMaterial.Icon.cmd_archive)
)
drawerItems.add(DividerDrawerItem())
drawerItems.add(
PrimaryDrawerItem()
.withName(R.string.contacts_and_subscriptions)
.withIcon(GoogleMaterial.Icon.gmd_contacts)
)
drawerItems.add(
PrimaryDrawerItem()
.withName(R.string.settings)
.withIcon(GoogleMaterial.Icon.gmd_settings)
)
nodeSwitch = SwitchDrawerItem()
.withIdentifier(ID_NODE_SWITCH)
.withName(R.string.online)
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
.withChecked(preferences.online)
.withOnCheckedChangeListener { _, _, isChecked ->
preferences.online = isChecked
if (isChecked) {
network.enableNode(true)
}
}
drawer = DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withAccountHeader(accountHeader)
.withDrawerItems(drawerItems)
.addStickyDrawerItems(nodeSwitch)
.withOnDrawerItemClickListener(DrawerItemClickListener())
.withShowDrawerOnFirstLaunch(true)
.build()
loadDrawerItemsAsynchronously()
}
private fun loadDrawerItemsAsynchronously() {
doAsync {
val identities = bmc.addresses.getIdentities()
if (identities.isEmpty()) {
// Create an initial identity
Singleton.getIdentity(this@MainActivity)
}
uiThread {
for (identity in identities) {
addIdentityEntry(identity)
}
}
}
doAsync {
val labels = bmc.labels.getLabels()
uiThread {
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
currentLabel.onNext(intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label)
} else if (currentLabel.value == null) {
currentLabel.onNext(labels[0])
}
for (label in labels) {
addLabelEntry(label)
}
currentLabel.value?.let { label ->
drawer.setSelection(label.id as Long)
}
updateUnread()
}
}
}
override fun onBackPressed() {
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
super.onBackPressed()
}
}
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean {
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
val tag = item.tag
if (tag is Label) {
currentLabel.onNext(tag)
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
if (itemList !is ConversationListFragment) {
changeList(ConversationListFragment())
}
} else {
if (itemList !is MessageListFragment) {
changeList(MessageListFragment())
}
}
return false
} else if (item is Nameable<*>) {
when (item.name.textRes) {
R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) {
itemList.reloadList()
} else {
changeList(AddressListFragment())
}
return false
}
R.string.settings -> {
supportFragmentManager?.transaction {
replace(R.id.item_list, SettingsFragment())
addToBackStack(null)
}
return false
}
R.string.full_node -> return true
else -> return false
}
}
return false
}
}
override fun onResume() {
network.enableNode(false)
updateUnread()
Singleton.getMessageListener(this).resetNotification()
subscription = currentLabel.subscribe { label ->
if (label.id is Long) {
drawer.setSelection(label.id as Long, false)
}
}
active = true
super.onResume()
}
override fun onPause() {
subscription?.dispose()
super.onPause()
active = false
}
fun addIdentityEntry(identity: BitmessageAddress) {
val newProfile = ProfileDrawerItem()
.withIcon(Identicon(identity))
.withName(identity.toString())
.withNameShown(true)
.withEmail(identity.address)
.withTag(identity)
if (accountHeader.profiles != null) {
// we know that there are 2 setting elements.
// Set the new profile above them ;)
accountHeader.addProfile(
newProfile, accountHeader.profiles.size - 2
)
} else {
accountHeader.addProfiles(newProfile)
}
}
private fun addLabelEntry(label: Label) {
val item = PrimaryDrawerItem()
.withIdentifier(label.id as Long)
.withName(label.toString())
.withTag(label)
.withIcon(label.getIcon())
.withIconColor(label.getColor(0xFF000000.toInt()))
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
}
fun updateIdentityEntry(identity: BitmessageAddress) {
for (profile in accountHeader.profiles) {
if (profile is ProfileDrawerItem) {
if (identity == profile.tag) {
profile
.withName(identity.toString())
.withTag(identity)
return
}
}
}
}
fun removeIdentityEntry(identity: BitmessageAddress) {
for (profile in accountHeader.profiles) {
if (profile is ProfileDrawerItem) {
if (identity == profile.tag) {
accountHeader.removeProfile(profile)
return
}
}
}
}
fun updateUnread() {
for (item in drawer.drawerItems) {
if (item.tag is Label) {
val label = item.tag as Label
if (label !== LABEL_ARCHIVE) {
val unread = messageRepo.countUnread(label, preferences.separateIdentities)
if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString())
} else {
(item as PrimaryDrawerItem).withBadge(null as String?)
}
runOnUiThread {
drawer.updateItem(item)
}
}
}
}
}
/**
* Callback method from [ListSelectionListener]
* indicating that the item with the given ID was selected.
*/
override fun onItemSelected(item: Serializable) {
if (hasDetailPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
val fragment = when (item) {
is Conversation -> {
ConversationDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
}
is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) {
ComposeMessageFragment().apply {
arguments = Bundle().apply {
putSerializable(ComposeMessageActivity.EXTRA_DRAFT, item)
}
}
} else {
MessageDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(MessageDetailFragment.ARG_ITEM, item)
}
}
}
}
is BitmessageAddress -> {
AddressDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(AddressDetailFragment.ARG_ITEM, item)
}
}
}
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
}
supportFragmentManager.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
val detailIntent = when (item) {
is Conversation -> {
Intent(this, MessageDetailActivity::class.java).apply {
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) {
Intent(this, ComposeMessageActivity::class.java).apply {
putExtra(ComposeMessageActivity.EXTRA_DRAFT, item)
}
} else {
Intent(this, MessageDetailActivity::class.java).apply {
putExtra(MessageDetailFragment.ARG_ITEM, item)
}
}
}
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java).apply {
putExtra(AddressDetailFragment.ARG_ITEM, item)
}
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
}
startActivity(detailIntent)
}
}
fun setDetailView(fragment: Fragment) {
if (hasDetailPane) {
supportFragmentManager
.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
}
}
fun updateTitle(title: CharSequence) {
supportActionBar?.title = title
}
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
fab.hide()
fab.removeAllOnMenuItemClickListeners()
val mainFab = fab.mainFab
mainFab.setImageResource(drawableRes)
fab.setMenu(menu)
fab.addOnStateChangeListener { isOpened: Boolean ->
if (isOpened) {
// It will be turned 45 degrees, which makes an x out of the +
mainFab.setImageResource(R.drawable.ic_action_add)
} else {
mainFab.setImageResource(drawableRes)
}
}
fab.show()
fab.closeMenu()
return fab
}
companion object {
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
const val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
const val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
const val ADD_IDENTITY = 1
const val MANAGE_IDENTITY = 2
private const val ID_NODE_SWITCH: Long = 1
private var instance: WeakReference<MainActivity>? = null
/**
* Runs the given code in the main activity context, if it currently exists. Otherwise,
* it's ignored.
*/
fun apply(run: MainActivity.() -> Unit) {
instance?.get()?.let { run.invoke(it) }
}
}
}
@@ -0,0 +1,60 @@
package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import androidx.core.app.NavUtils
import android.view.MenuItem
import ch.dissem.bitmessage.entity.Plaintext
/**
* An activity representing a single Message detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a [MainActivity].
*
* This activity is mostly just a 'shell' activity containing nothing
* more than a [MessageDetailFragment].
*/
class MessageDetailActivity : DetailActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
val arguments = Bundle()
val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
val itemId = intent.getSerializableExtra(ConversationDetailFragment.ARG_ITEM_ID)
arguments.putSerializable(ConversationDetailFragment.ARG_ITEM_ID, itemId)
val fragment = if (item is Plaintext) {
MessageDetailFragment()
} else {
ConversationDetailFragment()
}
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.add(R.id.content, fragment)
.commit()
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -0,0 +1,271 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.text.util.Linkify
import android.text.util.Linkify.WEB_URLS
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.adapter.LabelAdapter
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.apps.abit.util.getDrawable
import ch.dissem.apps.abit.util.getString
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import kotlinx.android.synthetic.main.fragment_message_detail.*
import java.util.*
/**
* A fragment representing a single Message detail screen.
* This fragment is either contained in a [MainActivity]
* in two-pane mode (on tablets) or a [MessageDetailActivity]
* on handsets.
*/
class MessageDetailFragment : Fragment() {
/**
* The content this fragment is presenting.
*/
private var item: Plaintext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
if (arguments.containsKey(ARG_ITEM)) {
// Load the dummy content specified by the fragment
// arguments. In a real-world scenario, use a Loader
// to load content from a content provider.
item = arguments.getSerializable(ARG_ITEM) as Plaintext
}
}
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
// Show the dummy content as text in a TextView.
item?.let { item ->
subject.text = item.subject
status.setImageResource(item.status.getDrawable())
status.contentDescription = getString(item.status.getString())
avatar.setImageDrawable(Identicon(item.from))
val senderClickListener: (View) -> Unit = {
MainActivity.apply {
onItemSelected(item.from)
}
}
avatar.setOnClickListener(senderClickListener)
sender.setOnClickListener(senderClickListener)
sender.text = item.from.toString()
item.to?.let { to ->
recipient.text = to.toString()
} ?: {
if (item.type == Plaintext.Type.BROADCAST) {
recipient.setText(R.string.broadcast)
}
}.invoke()
val labelAdapter = LabelAdapter(ctx, item.labels)
labels.adapter = labelAdapter
labels.layoutManager = GridLayoutManager(activity, 2)
text.text = item.text
Linkify.addLinks(text, WEB_URLS)
Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
Linkify.TransformFilter { match, _ -> match.group() }
)
text.linksClickable = true
text.setTextIsSelectable(true)
val messageRepo = Singleton.getMessageRepository(ctx)
if (item.isUnread()) {
Singleton.labeler.markAsRead(item)
(activity as? MainActivity)?.updateUnread()
messageRepo.save(item)
}
val parents = ArrayList<Plaintext>(item.parents.size)
for (parentIV in item.parents) {
val parent = messageRepo.getMessage(parentIV)
if (parent != null) {
parents.add(parent)
}
}
showRelatedMessages(ctx, view, R.id.parents, parents)
showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item))
}
}
private fun showRelatedMessages(
ctx: Context,
rootView: View, @IdRes id: Int,
messages: List<Plaintext>
) {
val recyclerView = rootView.findViewById<RecyclerView>(id)
val adapter = RelatedMessageAdapter(ctx, messages)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message, menu)
activity?.let { activity ->
Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply)
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(
activity, menu, R.id.mark_unread, GoogleMaterial.Icon
.gmd_markunread
)
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val messageRepo = Singleton.getMessageRepository(
context ?: throw IllegalStateException("No context available")
)
item?.let { item ->
when (menuItem.itemId) {
R.id.reply -> {
ComposeMessageActivity.launchReplyTo(this, item)
return true
}
R.id.delete -> {
if (isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
(activity as? MainActivity)?.updateUnread()
activity?.onBackPressed()
return true
}
R.id.mark_unread -> {
Singleton.labeler.markAsUnread(item)
messageRepo.save(item)
(activity as? MainActivity)?.updateUnread()
return true
}
R.id.archive -> {
if (item.isUnread() && activity is MainActivity) {
(activity as MainActivity).updateUnread()
}
Singleton.labeler.archive(item)
messageRepo.save(item)
(activity as? MainActivity)?.updateUnread()
return true
}
else -> return false
}
}
return false
}
private class RelatedMessageAdapter internal constructor(
private val ctx: Context,
private val messages: List<Plaintext>
) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RelatedMessageAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val message = messages[position]
viewHolder.avatar.setImageDrawable(Identicon(message.from))
viewHolder.status.setImageResource(message.status.getDrawable())
viewHolder.sender.text = message.from.toString()
viewHolder.extract.text = prepareMessageExtract(message.text)
viewHolder.item = message
}
// Returns the total count of items in the list
override fun getItemCount() = messages.size
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal val avatar = itemView.findViewById<ImageView>(R.id.avatar)
internal val status = itemView.findViewById<ImageView>(R.id.status)
internal val sender = itemView.findViewById<TextView>(R.id.sender)
internal val extract = itemView.findViewById<TextView>(R.id.text)
internal var item: Plaintext? = null
init {
itemView.setOnClickListener {
if (ctx is MainActivity) {
item?.let { ctx.onItemSelected(it) }
} else {
val detailIntent = Intent(ctx, MessageDetailActivity::class.java)
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
ctx.startActivity(detailIntent)
}
}
}
}
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
const val ARG_ITEM = "item"
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
}
}
@@ -0,0 +1,308 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import android.view.*
import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.adapter.EventListener
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.valueobject.Label
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.*
import java.util.*
private const val PAGE_SIZE = 15
/**
* A list fragment representing a list of Messages. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a [MessageDetailFragment].
*
*
* Activities containing this fragment MUST implement the [ListSelectionListener]
* interface.
*/
class MessageListFragment : Fragment(), ListHolder<Label> {
private var isLoading = false
private var isLastPage = false
private var subscription: Disposable? = null
private var layoutManager: LinearLayoutManager? = null
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - PAGE_SIZE
&& firstVisibleItemPosition >= 0
) {
loadMoreItems()
}
}
}
}
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableMessageAdapter?.activateOnItemClick = activateOnItemClick
this.activateOnItemClick = activateOnItemClick
}
private val backStack = Stack<Label>()
fun loadMoreItems() {
isLoading = true
swipeableMessageAdapter?.let { messageAdapter ->
doAsync {
val label = currentLabel.value
val messages = messageRepo.findMessages(
label,
messageAdapter.itemCount,
PAGE_SIZE,
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
)
uiThread {
messageAdapter.addAll(messages)
isLoading = false
isLastPage = messages.size < PAGE_SIZE
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
val activity = activity as MainActivity
initFab(activity)
messageRepo = Singleton.getMessageRepository(activity)
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
subscription?.dispose()
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
// If the menu item isn't available yet, we should wait - the method will be called again once it's
// initialized.
emptyTrashMenuItem?.let { menuItem ->
val mainActivity = activity as? MainActivity
swipeableMessageAdapter?.clear(label)
if (label == null) {
mainActivity?.updateTitle(getString(R.string.app_name))
swipeableMessageAdapter?.notifyDataSetChanged()
return
}
menuItem.isVisible = label.type == Label.Type.TRASH
MainActivity.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
} else {
updateTitle(label.toString())
}
}
loadMoreItems()
}
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = context ?: throw IllegalStateException("No context available")
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.layoutManager = layoutManager
swipeableMessageAdapter = SwipeableMessageAdapter(context).apply {
activateOnItemClick = this@MessageListFragment.activateOnItemClick
}
recycler_view.adapter = swipeableMessageAdapter // requires *wrapped* swipeableMessageAdapter
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
val dirs = when (currentLabel.value?.type) {
Label.Type.TRASH -> ItemTouchHelper.LEFT
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
}
val swipeHandler = SwipeToDeleteCallback(context, dirs, object : EventListener {
override fun onItemDeleted(position: Int) {
context.toast("Deleted")
}
override fun onItemArchived(position: Int) {
context.toast("Archived")
}
override fun onItemSelected(position: Int) {
context.toast("Selected")
}
})
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(recycler_view)
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
}
private fun initFab(context: MainActivity) {
val menu = FabSpeedDialMenu(context)
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
context.initFab(R.drawable.ic_action_compose_message, menu)
.addOnMenuItemClickListener { _, _, itemId ->
val identity = Singleton.getIdentity(context)
if (identity == null) {
Toast.makeText(
activity, R.string.no_identity_warning,
Toast.LENGTH_LONG
).show()
} else {
when (itemId) {
1 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
intent.putExtra(EXTRA_BROADCAST, true)
startActivity(intent)
}
2 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
startActivity(intent)
}
else -> {
}
}
}
}
}
override fun onDestroyView() {
swipeableMessageAdapter = null
layoutManager = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
// currentLabel.value?.let { doUpdateList(it) }
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.empty_trash -> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
deleteAllMessages(label)
}
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
context?.apply {
alert(
R.string.delete_all_messages_in_list,
R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
}
return true
}
else -> return false
}
}
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.onNext(label)
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.onNext(backStack.pop())
true
}
}
@@ -0,0 +1,265 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.widget.Toast
import androidx.core.content.FileProvider.getUriForFile
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import ch.dissem.apps.abit.service.BatchProcessorService
import ch.dissem.apps.abit.service.SimpleJob
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Plaintext
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.indeterminateProgressDialog
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread
import java.util.*
/**
* @author Christian Basler
*/
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
findPreference("export")?.onPreferenceClickListener = exportClickListener()
findPreference("import")?.onPreferenceClickListener = importClickListener()
findPreference("status")?.onPreferenceClickListener = statusClickListener()
connectivityChangeListener().let {
findPreference("wifi_only")?.onPreferenceChangeListener = it
findPreference("require_charging")?.onPreferenceChangeListener = it
}
val emulateConversations = findPreference("emulate_conversations") as? SwitchPreferenceCompat
val conversationInit = findPreference("emulate_conversations_initialize")
emulateConversations?.onPreferenceChangeListener = emulateConversationChangeListener(conversationInit)
conversationInit?.onPreferenceClickListener = conversationInitClickListener()
conversationInit?.isEnabled = emulateConversations?.isChecked ?: false
}
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
(activity as? MainActivity)?.let { activity ->
val libsBuilder = LibsBuilder()
.withActivityTitle(activity.getString(R.string.about))
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutDescription(getString(R.string.about_app))
if (activity.hasDetailPane) {
activity.setDetailView(libsBuilder.supportFragment())
} else {
libsBuilder.start(activity)
}
}
return@OnPreferenceClickListener true
}
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
val ctx = activity?.applicationContext
?: throw IllegalStateException("Context not available")
cleanup.isEnabled = false
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
doAsync {
val bmc = Singleton.getBitmessageContext(ctx)
bmc.internals.nodeRegistry.clear()
bmc.cleanup()
ctx.preferences.cleanupExportDirectory()
uiThread {
Toast.makeText(
ctx,
R.string.cleanup_notification_end,
Toast.LENGTH_LONG
).show()
cleanup.isEnabled = true
}
}
return@OnPreferenceClickListener true
}
private fun exportClickListener() = Preference.OnPreferenceClickListener {
val ctx = activity ?: throw IllegalStateException("No context available")
ctx.indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
doAsync {
val exportDirectory = ctx.preferences.exportDirectory
exportDirectory.mkdirs()
val file = Exports.exportData(exportDirectory, ctx)
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
val intent = Intent(android.content.Intent.ACTION_SEND)
intent.type = "application/zip"
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
uiThread {
dismiss()
}
}
}
return@OnPreferenceClickListener true
}
private fun importClickListener() = Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/zip"
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
return@OnPreferenceClickListener true
}
private fun statusClickListener() = Preference.OnPreferenceClickListener {
val activity = activity as MainActivity
if (activity.hasDetailPane) {
activity.setDetailView(StatusFragment())
} else {
activity.startActivity<StatusActivity>()
}
return@OnPreferenceClickListener true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val ctx = activity ?: throw IllegalStateException("No context available")
when (requestCode) {
WRITE_EXPORT_REQUEST_CODE -> ctx.preferences.cleanupExportDirectory()
READ_IMPORT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK && data?.data != null) {
ctx.indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
doAsync {
Exports.importData(data.data!!, ctx)
uiThread {
dismiss()
}
}
}
}
}
}
}
override fun onAttach(ctx: Context?) {
super.onAttach(ctx)
ctx?.let {
if (it is MainActivity) {
it.floatingActionButton?.hide()
it.updateTitle(getString(R.string.settings))
}
}
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (service is BatchProcessorService.BatchBinder) {
val messageRepo = Singleton.getMessageRepository(service.service)
val conversationService = Singleton.getConversationService(service.service)
service.process(
SimpleJob<Plaintext>(
messageRepo.count(),
{ messageRepo.findNextLegacyMessages(it) },
{ msg ->
if (msg.encoding == Plaintext.Encoding.SIMPLE) {
conversationService.getSubject(listOf(msg))?.let { subject ->
msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
messageRepo.save(msg)
Thread.yield()
}
}
},
R.drawable.ic_notification_batch,
R.string.emulate_conversations_batch
)
)
}
}
override fun onServiceDisconnected(name: ComponentName) {
}
}
private fun conversationInitClickListener() = Preference.OnPreferenceClickListener {
val ctx = activity?.applicationContext
?: throw IllegalStateException("Context not available")
ctx.bindService(Intent(ctx, BatchProcessorService::class.java), connection, Context.BIND_AUTO_CREATE)
true
}
private fun emulateConversationChangeListener(conversationInit: Preference?) =
Preference.OnPreferenceChangeListener { _, newValue ->
conversationInit?.isEnabled = newValue as Boolean
true
}
private fun connectivityChangeListener() =
Preference.OnPreferenceChangeListener { _, _ ->
activity?.network?.scheduleNodeStart()
true
}
// The why-is-it-so-damn-hard-to-group-preferences section
// FIXME: maybe this is once again necessary, maybe not. Test!
// override fun getCallbackFragment(): Fragment = this
//
// override fun onPreferenceStartScreen(
// preferenceFragmentCompat: PreferenceFragment,
// preferenceScreen: PreferenceScreen
// ): Boolean {
// fragmentManager?.beginTransaction()?.let { ft ->
// val fragment = SettingsFragment()
// fragment.arguments = Bundle().apply {
// putString(PreferenceFragment.ARG_PREFERENCE_ROOT, preferenceScreen.key)
// }
// ft.add(R.id.item_list, fragment, preferenceScreen.key)
// ft.addToBackStack(preferenceScreen.key)
// ft.commit()
// }
// return true
// }
//
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// super.onViewCreated(view, savedInstanceState)
// context?.let { ctx -> view.setBackgroundColor(ContextCompat.getColor(ctx, R.color.contentBackground)) }
// }
// End of the why-is-it-so-damn-hard-to-group-preferences section
// Afterthought: here it looks so simple: https://developer.android.com/guide/topics/ui/settings.html
// Remind me, why do we need to use PreferenceFragmentCompat?
companion object {
const val WRITE_EXPORT_REQUEST_CODE = 1
const val READ_IMPORT_REQUEST_CODE = 2
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.activity_status.*
class StatusActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_status)
setSupportActionBar(toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(false)
}
MaterializeBuilder()
.withActivity(this)
.withStatusBarColorRes(R.color.colorPrimaryDark)
.withTranslucentStatusBarProgrammatically(true)
.withStatusBarPadding(true)
.build()
val bmc = Singleton.getBitmessageContext(this)
val status = StringBuilder()
for (address in bmc.addresses.getIdentities()) {
status.append(address.address).append('\n')
}
status.append('\n')
status.append(bmc.status())
content.text = status
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
class StatusFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_status, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bmc = Singleton.getBitmessageContext(
context ?: throw IllegalStateException("No context available")
)
val status = StringBuilder()
for (address in bmc.addresses.getIdentities()) {
status.append(address.address).append('\n')
}
status.append('\n')
status.append(bmc.status())
view.findViewById<TextView>(R.id.content).text = status
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.BitmessageAddress
import java.util.*
/**
* @author Christian Basler
*/
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
private val data = identities.asSequence().map { Selectable(it) }.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.select_identity_row, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val selectable = data[position]
holder.data = selectable
holder.checkbox.isChecked = selectable.selected
holder.checkbox.text = selectable.data.toString()
holder.address.text = selectable.data.address
}
override fun getItemCount() = data.size
class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) {
var data: Selectable<BitmessageAddress>? = null
val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!!
val address = v.findViewById<TextView>(R.id.address)!!
init {
checkbox.setOnCheckedChangeListener { _, isChecked ->
data?.selected = isChecked
}
}
}
val selected: List<BitmessageAddress>
get() {
return data.asSequence()
.filter { it.selected }
.mapTo(LinkedList()) { it.data }
}
}
@@ -0,0 +1,145 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.BitmessageAddress
import java.util.*
/**
* An adapter for contacts. Can be filtered by alias or address.
*/
class ContactAdapter(
ctx: Context,
private val originalData: List<BitmessageAddress>,
private val slim: Boolean = false
) :
BaseAdapter(), Filterable {
private val inflater = LayoutInflater.from(ctx)
private var data: List<BitmessageAddress> = originalData
override fun getCount() = data.size
override fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val viewHolder = if (convertView == null) {
ViewHolder(
inflater.inflate(
if (slim) {
R.layout.contact_row_slim
} else {
R.layout.contact_row
},
parent, false
)
)
} else {
convertView.tag as ViewHolder
}
val item = getItem(position)
viewHolder.avatar.setImageDrawable(Identicon(item))
viewHolder.name.text = item.toString()
viewHolder.address?.text = item.address
return viewHolder.view
}
override fun getFilter(): Filter = ContactFilter()
private inner class ViewHolder(val view: View) {
val avatar: ImageView = view.findViewById(R.id.avatar)
val name: TextView = view.findViewById(R.id.name)
val address: TextView? = view.findViewById(R.id.address)
init {
view.tag = this
}
}
private inner class ContactFilter : Filter() {
override fun performFiltering(prefix: CharSequence?): Filter.FilterResults {
val results = Filter.FilterResults()
if (prefix?.isEmpty() == false) {
val prefixString = prefix.toString().toLowerCase()
val newValues = ArrayList<BitmessageAddress>()
originalData
.forEach { value ->
value.alias?.toLowerCase()?.let { alias ->
if (alias.startsWith(prefixString)) {
newValues.add(value)
} else {
val words =
alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
for (word in words) {
if (word.startsWith(prefixString)) {
newValues.add(value)
break
}
}
}
} ?: {
val address = value.address.toLowerCase()
if (address.contains(prefixString)) {
newValues.add(value)
}
}.invoke()
}
if (newValues.isEmpty()) {
try {
newValues.add(BitmessageAddress(prefix.toString()))
} catch (_: Exception) {
}
}
results.values = newValues
results.count = newValues.size
} else {
results.values = originalData
results.count = originalData.size
}
return results
}
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
@Suppress("UNCHECKED_CAST")
data = results.values as List<BitmessageAddress>
if (results.count > 0) {
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}
@@ -0,0 +1,162 @@
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ch.dissem.apps.abit.*
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants
import ch.dissem.apps.abit.util.getDrawable
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.MessageRepository
import io.reactivex.subjects.BehaviorSubject
class ConversationAdapter internal constructor(
ctx: Context,
private val parent: Fragment,
conversation: Conversation,
label: BehaviorSubject<Label>
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
private val messageRepo = Singleton.getMessageRepository(ctx)
private var filteredMessages = label.value?.let { l -> conversation.messages.filter { m -> m.labels.any { it == l } } } ?: emptyList()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ConversationAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val messageView = inflater.inflate(R.layout.item_message_detail, parent, false)
// Return a new holder instance
return ViewHolder(messageView, this.parent, messageRepo)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: ConversationAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val message = filteredMessages[position]
viewHolder.apply {
item = message
avatar.setImageDrawable(Identicon(message.from))
sender.text = message.from.toString()
val senderClickListener: (View) -> Unit = {
MainActivity.apply {
onItemSelected(message.from)
}
}
avatar.setOnClickListener(senderClickListener)
sender.setOnClickListener(senderClickListener)
recipient.text = message.to.toString()
status.setImageResource(message.status.getDrawable())
text.text = message.text
Linkify.addLinks(text, Linkify.WEB_URLS)
Linkify.addLinks(text,
Constants.BITMESSAGE_ADDRESS_PATTERN,
Constants.BITMESSAGE_URL_SCHEMA, null,
Linkify.TransformFilter { match, _ -> match.group() }
)
labelAdapter.labels = message.labels.toList()
// FIXME: I think that's not quite correct
if (message.isUnread()) {
Singleton.labeler.markAsRead(message)
messageRepo.save(message)
MainActivity.apply { updateUnread() }
}
}
}
override fun getItemCount() = filteredMessages.size
inner class ViewHolder(
itemView: View,
parent: Fragment,
messageRepo: MessageRepository
) : RecyclerView.ViewHolder(itemView) {
var item: Plaintext? = null
val avatar = itemView.findViewById<ImageView>(R.id.avatar)!!
val sender = itemView.findViewById<TextView>(R.id.sender)!!
val recipient = itemView.findViewById<TextView>(R.id.recipient)!!
val status = itemView.findViewById<ImageView>(R.id.status)!!
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.apply {
setOnClickListener { view ->
PopupMenu(itemView.context, view).apply {
menuInflater.inflate(R.menu.message, menu)
setOnMenuItemClickListener { menuItem ->
item?.let { item ->
when (menuItem.itemId) {
R.id.reply -> {
ComposeMessageActivity.launchReplyTo(parent, item)
true
}
R.id.delete -> {
if (MessageDetailFragment.isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
filteredMessages.indexOf(item).let { i ->
filteredMessages -= item
notifyItemRemoved(i)
}
MainActivity.apply {
updateUnread()
}
true
}
R.id.mark_unread -> {
Singleton.labeler.markAsUnread(item)
messageRepo.save(item)
MainActivity.apply { updateUnread() }
true
}
R.id.archive -> {
Singleton.labeler.archive(item)
messageRepo.save(item)
MainActivity.apply { updateUnread() }
true
}
else -> false
}
} ?: false
}
show()
}
}
}
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
linksClickable = true
setTextIsSelectable(true)
}
val labelAdapter = LabelAdapter(itemView.context, emptySet())
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
adapter = labelAdapter
layoutManager = GridLayoutManager(itemView.context, 2)
}
}
}
@@ -0,0 +1,54 @@
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.content.res.ColorStateList
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.getColor
import ch.dissem.apps.abit.util.getIcon
import ch.dissem.apps.abit.util.getText
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.iconics.view.IconicsImageView
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
var labels = labels.toList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val contactView = inflater.inflate(R.layout.item_label, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val label = labels[position]
viewHolder.icon.icon?.icon(label.getIcon())
viewHolder.label.text = label.getText(ctx)
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
}
override fun getItemCount() = labels.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
var label = itemView.findViewById<TextView>(R.id.label)!!
fun setBackground(@ColorInt color: Int) {
itemView.backgroundTintList = ColorStateList.valueOf(color)
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
/**
* @author Christian Basler
*/
class Selectable<out T>(val data: T) {
var selected = false
}
@@ -0,0 +1,312 @@
/*
* Copyright 2015 Haruki Hasegawa
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.graphics.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.MultiIdenticon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.apps.abit.util.getDrawable
import ch.dissem.apps.abit.util.getString
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import java.util.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
abstract class SwipeableAdapter<T, H>(val ctx: Context) :
RecyclerView.Adapter<H>() where H : SwipeableAdapter.AbstractViewHolder {
protected val data = LinkedList<T>()
var eventListener: EventListener? = null
protected var label: Label? = null
var selectedPosition = -1
set(value) {
val oldPosition = field
field = value
notifyItemChanged(oldPosition)
notifyItemChanged(value)
}
var activateOnItemClick: Boolean = false
protected val labelUnknown: String = ctx.getString(R.string.unknown)
open class AbstractViewHolder(v: View, adapter: SwipeableAdapter<*, *>) : RecyclerView.ViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
init {
itemView.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
container.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
}
}
init {
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
override fun onBindViewHolder(holder: H, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
setData(holder, item)
}
}
abstract fun setData(holder: H, item: T)
fun add(item: T) {
val index = data.size
data.add(item)
notifyItemInserted(index)
}
fun addFirst(item: T) {
data.addFirst(item)
notifyItemInserted(0)
}
fun addAll(items: Collection<T>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: T) {
val itemId = getItemId(item)
val index = data.indexOfFirst { getItemId(it) == itemId }
if (index >= 0) {
removeAt(index)
}
}
fun removeAt(index: Int) {
data.removeAt(index)
notifyItemRemoved(index)
}
override fun getItemId(position: Int): Long {
return getItemId(data[position])
}
abstract fun getItemId(item: T): Long
abstract fun update(item: T)
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
fun getItem(position: Int) = data[position]
override fun getItemCount() = data.size
}
class SwipeableConversationAdapter(ctx: Context) : SwipeableAdapter<Conversation, SwipeableConversationAdapter.ViewHolder>(ctx) {
override fun getItemId(item: Conversation) = item.id.leastSignificantBits
override fun update(item: Conversation) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.conversation_row, parent, false)
return ViewHolder(v, this)
}
override fun setData(holder: ViewHolder, item: Conversation) {
holder.apply {
avatar.setImageDrawable(MultiIdenticon(item.participants))
sender.text = item.participants.sortedBy {
(it.alias?.let { 0 } ?: 1) + if (it.isChan) 2 else 0
}.map { it.alias ?: labelUnknown }.distinct().joinToString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.extract)
item.messages.count { it.labels.contains(label) }.let { size ->
if (size <= 1) {
count.text = ""
} else {
count.text = size.toString()
}
}
if (item.hasUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
class ViewHolder(v: View, adapter: SwipeableConversationAdapter) : AbstractViewHolder(v, adapter) {
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
val count = v.findViewById<TextView>(R.id.count)!!
}
}
class SwipeableMessageAdapter(ctx: Context) : SwipeableAdapter<Plaintext, SwipeableMessageAdapter.ViewHolder>(ctx) {
override fun getItemId(item: Plaintext) = item.id as Long
override fun update(item: Plaintext) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.message_row, parent, false)
return ViewHolder(v, this)
}
override fun setData(holder: ViewHolder, item: Plaintext) {
holder.apply {
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(item.status.getDrawable())
status.contentDescription = holder.status.context.getString(item.status.getString())
sender.text = item.from.toString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.text)
if (item.isUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
class ViewHolder(v: View, adapter: SwipeableMessageAdapter) : AbstractViewHolder(v, adapter) {
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
}
}
class SwipeToDeleteCallback(ctx: Context, swipeDirs: Int, private val eventListener: EventListener) : ItemTouchHelper.SimpleCallback(0, swipeDirs) {
private val backgroundLeft = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_left)!!
private val backgroundRight = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_right)!!
private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
/**
* To disable "swipe" for specific item return 0 here.
* For example:
* if (viewHolder?.itemViewType == YourAdapter.SOME_TYPE) return 0
* if (viewHolder?.adapterPosition == 0) return 0
*/
if (viewHolder.adapterPosition == 10) return 0
return super.getMovementFlags(recyclerView, viewHolder)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val isCanceled = dX == 0f && !isCurrentlyActive
if (isCanceled) {
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
return
}
if (dX < 0) {
backgroundLeft.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
backgroundLeft.draw(c)
} else {
backgroundRight.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
backgroundRight.draw(c)
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
c?.drawRect(left, top, right, bottom, clearPaint)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.LEFT -> eventListener.onItemDeleted(viewHolder.adapterPosition)
ItemTouchHelper.RIGHT -> eventListener.onItemArchived(viewHolder.adapterPosition)
}
}
}
interface EventListener {
fun onItemDeleted(position: Int)
fun onItemArchived(position: Int)
fun onItemSelected(position: Int)
}
@@ -0,0 +1,119 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.dialog
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDialogFragment
import ch.dissem.apps.abit.ImportIdentityActivity
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_identity.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread
/**
* @author Christian Basler
*/
class AddIdentityDialogFragment : AppCompatDialogFragment() {
private lateinit var bmc: BitmessageContext
private var parent: ViewGroup? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
bmc = Singleton.getBitmessageContext(context!!)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(R.string.add_identity)
parent = container
return inflater.inflate(R.layout.dialog_add_identity, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ok.setOnClickListener(View.OnClickListener {
val ctx = activity?.baseContext ?: throw IllegalStateException("No context available")
when (radioGroup.checkedRadioButtonId) {
R.id.create_identity -> {
Toast.makeText(ctx,
R.string.toast_long_running_operation,
Toast.LENGTH_SHORT).show()
doAsync {
val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK)
uiThread {
Toast.makeText(ctx,
R.string.toast_identity_created,
Toast.LENGTH_SHORT).show()
MainActivity.apply {
addIdentityEntry(identity)
}
}
}
}
R.id.import_identity -> ctx.startActivity<ImportIdentityActivity>()
R.id.add_chan -> addChanDialog()
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
else -> return@OnClickListener
}
dismiss()
})
dismiss.setOnClickListener { dismiss() }
}
private fun addChanDialog() {
val activity = activity ?: throw IllegalStateException("No activity available")
val ctx = activity.baseContext ?: throw IllegalStateException("No context available")
val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent)
AlertDialog.Builder(activity)
.setTitle(R.string.add_chan)
.setView(dialogView)
.setPositiveButton(R.string.ok) { _, _ ->
val passphrase = dialogView.findViewById<TextView>(R.id.passphrase)
Toast.makeText(ctx, R.string.toast_long_running_operation,
Toast.LENGTH_SHORT).show()
val pass = passphrase.text.toString()
doAsync {
val chan = bmc.createChan(pass)
chan.alias = pass
bmc.addresses.save(chan)
uiThread {
Toast.makeText(ctx,
R.string.toast_chan_created,
Toast.LENGTH_SHORT).show()
MainActivity.apply { addIdentityEntry(chan) }
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun getTheme() = R.style.FixedDialog
}
@@ -0,0 +1,96 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
/**
* @author Christian Basler
*/
class DeterministicIdentityDialogFragment : AppCompatDialogFragment() {
private lateinit var bmc: BitmessageContext
override fun onAttach(context: Context?) {
super.onAttach(context)
bmc = Singleton.getBitmessageContext(context!!)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(R.string.add_deterministic_address)
return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false)
}
override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) {
super.onViewCreated(dialogView, savedInstanceState)
ok.setOnClickListener {
dismiss()
val context = activity?.baseContext ?: throw IllegalStateException("No context available")
val passphraseText = passphrase.text.toString()
Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show()
doAsync {
val identities = bmc.createDeterministicAddresses(
passphraseText,
number_of_identities.text.toString().toInt(),
Pubkey.LATEST_VERSION,
1L,
shorter.isChecked
)
for ((i, identity) in identities.withIndex()) {
if (identities.size == 1) {
identity.alias = label.text.toString()
} else {
identity.alias = "${label.text} (${i + 1})"
}
bmc.addresses.save(identity)
}
uiThread {
val messageRes = if (identities.size == 1) {
R.string.toast_identity_created
} else {
R.string.toast_identities_created
}
Toast.makeText(context,
messageRes,
Toast.LENGTH_SHORT).show()
MainActivity.apply {
identities.forEach { identity ->
addIdentityEntry(identity)
}
}
}
}
}
dismiss.setOnClickListener { dismiss() }
}
override fun getTheme() = R.style.FixedDialog
}
@@ -0,0 +1,43 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.dialog
import android.app.Activity
import android.os.Bundle
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import kotlinx.android.synthetic.main.dialog_full_node.*
/**
* @author Christian Basler
*/
class FullNodeDialogActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_full_node)
ok.setOnClickListener {
preferences.wifiOnly = false
network.scheduleNodeStart()
finish()
}
dismiss.setOnClickListener {
network.scheduleNodeStart()
finish()
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.dialog
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
import kotlinx.android.synthetic.main.dialog_select_message_encoding.*
import org.slf4j.LoggerFactory
/**
* @author Christian Basler
*/
class SelectEncodingDialogFragment : AppCompatDialogFragment() {
private lateinit var encoding: Plaintext.Encoding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE
dialog.setTitle(R.string.select_encoding_title)
return inflater.inflate(R.layout.dialog_select_message_encoding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (encoding) {
SIMPLE -> radioGroup.check(R.id.simple)
EXTENDED -> radioGroup.check(R.id.extended)
else -> LOG.warn("Unexpected encoding: $encoding")
}
ok.setOnClickListener(View.OnClickListener {
encoding = when (radioGroup.checkedRadioButtonId) {
R.id.extended -> EXTENDED
R.id.simple -> SIMPLE
else -> {
dismiss()
return@OnClickListener
}
}
val result = Intent()
result.putExtra(EXTRA_ENCODING, encoding)
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result)
dismiss()
})
dismiss.setOnClickListener { dismiss() }
}
companion object {
private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java)
}
}
@@ -0,0 +1,56 @@
package ch.dissem.apps.abit.drawer
import android.app.Dialog
import android.content.Context
import android.graphics.Point
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import android.widget.RelativeLayout
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.qrCode
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.interfaces.IProfile
class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener {
override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean {
if (current) {
// Show QR code in modal dialog
val dialog = Dialog(ctx)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val imageView = ImageView(ctx)
imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
imageView.setOnClickListener { dialog.dismiss() }
dialog.addContentView(
imageView,
RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
val window = dialog.window
if (window != null) {
val display = window.windowManager.defaultDisplay
val size = Point()
display.getSize(size)
val dim = if (size.x < size.y) size.x else size.y
val lp = WindowManager.LayoutParams()
lp.copyFrom(window.attributes)
lp.width = dim
lp.height = dim
window.attributes = lp
}
dialog.show()
return true
}
return false
}
override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false
}
@@ -0,0 +1,54 @@
package ch.dissem.apps.abit.drawer
import android.content.Context
import android.content.Intent
import androidx.fragment.app.FragmentManager
import android.view.View
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import ch.dissem.apps.abit.*
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
class ProfileSelectionListener(
private val ctx: Context,
private val fragmentManager: FragmentManager
) : AccountHeader.OnAccountHeaderListener {
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
when (profile.identifier.toInt()) {
MainActivity.ADD_IDENTITY -> addIdentityDialog()
MainActivity.MANAGE_IDENTITY -> {
val identity = Singleton.getIdentity(ctx)
if (identity == null) {
Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show()
} else {
val show = Intent(ctx, AddressDetailActivity::class.java)
show.putExtra(AddressDetailFragment.ARG_ITEM, identity)
ctx.startActivity(show)
}
}
else -> if (profile is ProfileDrawerItem) {
val tag = profile.tag
if (tag is BitmessageAddress) {
Singleton.setIdentity(tag)
MainActivity.apply {
updateUnread()
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
if (itemList is ListHolder<*>) {
itemList.reloadList()
}
}
}
}
}
// false if it should close the drawer
return false
}
private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog")
}
@@ -0,0 +1,29 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.listener
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
interface ListSelectionListener<in T> {
/**
* Callback for when an item has been selected.
*/
fun onItemSelected(item: T)
}
@@ -0,0 +1,96 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.listener
import android.content.Context
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.notification.NewMessageNotification
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.ConversationService
import java.util.*
import java.util.concurrent.Executors
/**
* Listens for decrypted Bitmessage messages. Does show a notification.
*
*
* Should show a notification when the app isn't running, but update the message list when it is.
* Also,
* notifications should be combined.
*
*/
class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
override fun setContext(ctx: BitmessageContext) {
messageRepo = ctx.messages
conversationService = ConversationService(messageRepo)
}
private val unacknowledged = LinkedList<Plaintext>()
private var numberOfUnacknowledgedMessages = 0
private val notification = NewMessageNotification(ctx)
private val pool = Executors.newSingleThreadExecutor()
private lateinit var messageRepo: MessageRepository
private lateinit var conversationService: ConversationService
init {
emulateConversations = ctx.preferences.emulateConversations
}
override fun receive(plaintext: Plaintext) {
pool.submit {
updateConversation(plaintext)
unacknowledged.addFirst(plaintext)
numberOfUnacknowledgedMessages++
if (unacknowledged.size > 5) {
unacknowledged.removeLast()
}
if (numberOfUnacknowledgedMessages == 1) {
notification.singleNotification(plaintext)
} else {
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages)
}
notification.show()
// If MainActivity is shown, update the sidebar badges
MainActivity.apply { updateUnread() }
}
}
fun resetNotification() {
pool.submit {
notification.hide()
unacknowledged.clear()
numberOfUnacknowledgedMessages = 0
}
}
private fun updateConversation(plaintext: Plaintext) {
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
conversationService.getSubject(listOf(plaintext))?.let { subject ->
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
messageRepo.save(plaintext)
}
}
}
companion object {
private var emulateConversations = false
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.R
import org.jetbrains.anko.notificationManager
/**
* Some base class to create and handle notifications.
*/
abstract class AbstractNotification(ctx: Context) {
protected val ctx = ctx.applicationContext!!
private val manager = ctx.notificationManager
var notification: Notification? = null
protected set
protected var showing = false
private set
/**
* @return an id unique to this notification class
*/
protected abstract val notificationId: Int
open fun show() {
manager.notify(notificationId, notification)
showing = true
}
fun hide() {
showing = false
manager.cancel(notificationId)
}
protected fun initChannel(channelId: String, @ColorRes color: Int = R.color.colorPrimary) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
ctx.getText(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
).apply {
lightColor = ContextCompat.getColor(ctx, color)
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
}
)
}
}
companion object {
internal const val ONGOING_CHANNEL_ID = "abit.ongoing"
internal const val MESSAGE_CHANNEL_ID = "abit.message"
internal const val ERROR_CHANNEL_ID = "abit.error"
init {
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.content.Context
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Job
/**
* Ongoing notification while proof of work is in progress.
*/
class BatchNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
init {
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
notification = builder.build()
}
override val notificationId = ONGOING_NOTIFICATION_ID
fun update(job: Job): BatchNotification {
builder.setContentTitle(ctx.getString(job.description))
.setSmallIcon(job.icon)
.setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0)
notification = builder.build()
show()
return this
}
companion object {
const val ONGOING_NOTIFICATION_ID = 4
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.content.Context
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.R
/**
* Easily create notifications with error messages. Use carefully, users probably won't like them.
* (But they are useful during development/testing)
* @author Christian Basler
*/
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, ERROR_CHANNEL_ID)
.setContentTitle(ctx.getString(R.string.app_name))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
init {
initChannel(ERROR_CHANNEL_ID, R.color.colorPrimaryDark)
}
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
builder.setSmallIcon(R.drawable.ic_notification_warning)
.setContentText(ctx.getString(resId, *args))
notification = builder.build()
return this
}
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
builder.setSmallIcon(R.drawable.ic_notification_error)
.setContentText(ctx.getString(resId, *args))
notification = builder.build()
return this
}
override val notificationId = ERROR_NOTIFICATION_ID
companion object {
const val ERROR_NOTIFICATION_ID = 4
}
}
@@ -0,0 +1,145 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.apps.abit.service.NodeStartupService
import java.util.*
import kotlin.concurrent.fixedRateTimer
/**
* Shows the network status (as long as the client is connected as a full node)
*/
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
private var timer: Timer? = null
init {
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
val showAppIntent = Intent(ctx, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
builder
.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setShowWhen(false)
.setContentIntent(pendingIntent)
}
@SuppressLint("StringFormatMatches")
private fun update(): Boolean {
val running = NodeStartupService.isRunning
builder.setOngoing(running)
val connections = NodeStartupService.status.getProperty("network", "connections")
if (!running) {
builder.setSmallIcon(R.drawable.ic_notification_full_node_disconnected)
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
} else if (connections == null || connections.properties.isEmpty()) {
builder.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
builder.setContentText(ctx.getString(R.string.connection_info_pending))
} else {
builder.setSmallIcon(R.drawable.ic_notification_full_node)
val info = StringBuilder()
for (stream in connections.properties) {
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
val nodeCount = stream.getProperty("nodes")!!.value as Int?
if (nodeCount == 1) {
info.append(
ctx.getString(
R.string.connection_info_1,
streamNumber
)
)
} else {
info.append(
ctx.getString(
R.string.connection_info_n,
streamNumber, nodeCount
)
)
}
info.append('\n')
}
builder.setContentText(info)
}
builder.mActions.clear()
val intent = Intent(ctx, BitmessageIntentService::class.java)
if (running) {
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
builder.addAction(
R.drawable.ic_notification_node_stop,
ctx.getString(R.string.full_node_stop),
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
)
} else {
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true)
builder.addAction(
R.drawable.ic_notification_node_start,
ctx.getString(R.string.full_node_restart),
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)
)
}
notification = builder.build()
return running
}
override fun show() {
super.show()
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
if (!update()) {
cancel()
}
super@NetworkNotification.show()
}
}
fun showShutdown() {
timer?.cancel()
update()
super.show()
}
override val notificationId = NETWORK_NOTIFICATION_ID
fun connecting() {
builder.setOngoing(true)
builder.setContentText(ctx.getString(R.string.connection_info_pending))
val intent = Intent(ctx, BitmessageIntentService::class.java)
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
builder.mActions.clear()
builder.addAction(
R.drawable.ic_notification_node_stop,
ctx.getString(R.string.full_node_stop),
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
)
notification = builder.build()
}
companion object {
const val NETWORK_NOTIFICATION_ID = 2
}
}
@@ -0,0 +1,132 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.InboxStyle
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
import ch.dissem.apps.abit.util.toBitmap
import ch.dissem.bitmessage.entity.Plaintext
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
init {
initChannel(MESSAGE_CHANNEL_ID, R.color.colorPrimary)
}
fun singleNotification(plaintext: Plaintext): NewMessageNotification {
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text)
plaintext.subject?.let { subject ->
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setLargeIcon(Identicon(plaintext.from).toBitmap(192))
.setContentTitle(plaintext.from.toString())
.setContentText(plaintext.subject)
.setStyle(BigTextStyle().bigText(bigText))
.setContentInfo("Info")
builder.setContentIntent(
createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)
)
builder.addAction(
R.drawable.ic_action_reply, ctx.getString(R.string.reply),
createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)
)
builder.addAction(
R.drawable.ic_action_delete, ctx.getString(R.string.delete),
createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)
)
notification = builder.build()
return this
}
private fun createActivityIntent(action: String, message: Plaintext): PendingIntent {
val intent = Intent(ctx, MainActivity::class.java).putExtra(action, message)
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
}
private fun createServiceIntent(
ctx: Context,
action: String,
message: Plaintext
): PendingIntent {
val intent = Intent(ctx, BitmessageIntentService::class.java)
intent.putExtra(action, message)
return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
}
/**
* @param unacknowledged will be accessed from different threads, so make sure wherever it's
* * accessed it will be in a `synchronized(unacknowledged)
* * {}` block
*/
fun multiNotification(
unacknowledged: Collection<Plaintext>,
numberOfUnacknowledgedMessages: Int
): NewMessageNotification {
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
.setContentText(ctx.getString(R.string.app_name))
val inboxStyle = InboxStyle()
synchronized(unacknowledged) {
for (msg in unacknowledged) {
val sb = SpannableString(msg.from.toString() + " " + msg.subject)
sb.setSpan(
SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
.SPAN_INCLUSIVE_EXCLUSIVE
)
inboxStyle.addLine(sb)
}
}
builder.setStyle(inboxStyle)
val intent = Intent(ctx, MainActivity::class.java)
intent.action = MainActivity.ACTION_SHOW_INBOX
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
builder.setContentIntent(pendingIntent)
notification = builder.build()
return this
}
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
companion object {
private const val NEW_MESSAGE_NOTIFICATION_ID = 1
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
}
}
@@ -0,0 +1,105 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.ProofOfWorkService
import ch.dissem.apps.abit.util.PowStats
import java.util.*
import kotlin.concurrent.fixedRateTimer
/**
* Ongoing notification while proof of work is in progress.
*/
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_notification_proof_of_work)
.setContentTitle(ctx.getString(R.string.proof_of_work_title))
private var startTime = 0L
private var progress = 0
private var progressMax = 0
private var timer: Timer? = null
init {
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
update(0)
}
override val notificationId = ONGOING_NOTIFICATION_ID
fun update(numberOfItems: Int): ProofOfWorkNotification {
val showMessageIntent = Intent(ctx, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentText(if (numberOfItems == 0)
ctx.getString(R.string.proof_of_work_text_0)
else
ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
.setContentIntent(pendingIntent)
notification = builder.build()
return this
}
fun start(item: ProofOfWorkService.PowItem) {
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
startTime = System.currentTimeMillis()
progress = 0
progressMax = delta
builder.setProgress(progressMax, progress, false)
notification = builder.build()
show()
timer = fixedRateTimer(initialDelay = 2000, period = 2000){
val elapsedTime = System.currentTimeMillis() - startTime
progress = elapsedTime.toInt()
progressMax = progress + delta
builder.setProgress(progressMax, progress, false)
notification = builder.build()
show()
}
}
fun finished() {
timer?.cancel()
progress = 0
progressMax = 0
if (showing) {
builder.setProgress(0, 0, false)
notification = builder.build()
show()
}
}
companion object {
const val ONGOING_NOTIFICATION_ID = 3
}
}
@@ -0,0 +1,220 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.Cursor
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.V3Pubkey
import ch.dissem.bitmessage.entity.payload.V4Pubkey
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
import ch.dissem.bitmessage.factory.Factory
import ch.dissem.bitmessage.ports.AddressRepository
import ch.dissem.bitmessage.utils.Encode
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.*
/**
* [AddressRepository] implementation using the Android SQL API.
*/
class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag)
override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag)
private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? {
for (address in find(where)) {
if (address.version > 3) {
if (Arrays.equals(ripeOrTag, address.tag)) return address
} else {
if (Arrays.equals(ripeOrTag, address.ripe)) return address
}
}
return null
}
override fun getIdentities() = find("private_key IS NOT NULL")
override fun getChans() = find("chan = '1'")
override fun getSubscriptions() = find("subscribed = '1'")
override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) {
find("subscribed = '1' AND version > 3")
} else {
find("subscribed = '1' AND version <= 3")
}
override fun getContacts() = find("private_key IS NULL OR chan = '1'")
/**
* Returns the contacts in the following order:
*
* * Subscribed addresses come first
* * Addresses with aliases (alphabetically)
* * Addresses without aliases are omitted
*
*
* @return the ordered list of ids (address strings)
*/
fun getContactIds(): List<String> = findIds(
"($COLUMN_PRIVATE_KEY IS NULL OR $COLUMN_CHAN = '1') AND $COLUMN_ALIAS IS NOT NULL",
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS, $COLUMN_ADDRESS"
)
private fun findIds(where: String, orderBy: String): List<String> {
val result = LinkedList<String>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS)
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
orderBy
).use { c ->
while (c.moveToNext()) {
result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS)))
}
}
return result
}
private fun find(where: String): List<BitmessageAddress> {
val result = LinkedList<BitmessageAddress>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null, null
).use { c ->
while (c.moveToNext()) {
result.add(getAddress(c))
}
}
return result
}
private fun getAddress(c: Cursor): BitmessageAddress {
fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let {
BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it)))
}
fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address ->
c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes ->
Factory.readPubkey(
version = address.version, stream = address.stream,
input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size,
encrypted = false
).let {
address.pubkey = if (address.version == 4L && it is V3Pubkey) {
V4Pubkey(it)
} else {
it
}
}
}
}
return (getIdentity(c) ?: getContact(c)).apply {
alias = c.getString(c.getColumnIndex(COLUMN_ALIAS))
isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1
isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1
}
}
override fun save(address: BitmessageAddress) = if (exists(address)) {
update(address)
} else {
insert(address)
}
private fun exists(address: BitmessageAddress): Boolean {
sql.readableDatabase.rawQuery(
"SELECT COUNT(*) FROM Address WHERE address=?",
arrayOf(address.address)
).use { cursor ->
cursor.moveToFirst()
return cursor.getInt(0) > 0
}
}
private fun update(address: BitmessageAddress) {
// Create a new map of values, where column names are the keys
val values = getContentValues(address)
val update = sql.writableDatabase.update(TABLE_NAME, values, "address=?", arrayOf(address.address))
if (update < 0) {
LOG.error("Could not update address {}", address)
}
}
private fun insert(address: BitmessageAddress) {
// Create a new map of values, where column names are the keys
val values = getContentValues(address).apply {
put(COLUMN_ADDRESS, address.address)
put(COLUMN_VERSION, address.version)
put(COLUMN_CHAN, address.isChan)
}
val insert = sql.writableDatabase.insert(TABLE_NAME, null, values)
if (insert < 0) {
LOG.error("Could not insert address {}", address)
}
}
private fun getContentValues(address: BitmessageAddress) = ContentValues().apply {
address.alias?.let { put(COLUMN_ALIAS, it) }
address.pubkey?.let { pubkey ->
val out = ByteArrayOutputStream()
pubkey.writer().writeUnencrypted(out)
put(COLUMN_PUBLIC_KEY, out.toByteArray())
}
address.privateKey?.let { put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
if (address.isChan) {
put(COLUMN_CHAN, true)
}
put(COLUMN_SUBSCRIBED, address.isSubscribed)
}
override fun remove(address: BitmessageAddress) {
sql.writableDatabase.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
}
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()
companion object {
private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java)
private const val TABLE_NAME = "Address"
private const val COLUMN_ADDRESS = "address"
private const val COLUMN_VERSION = "version"
private const val COLUMN_ALIAS = "alias"
private const val COLUMN_PUBLIC_KEY = "public_key"
private const val COLUMN_PRIVATE_KEY = "private_key"
private const val COLUMN_SUBSCRIBED = "subscribed"
private const val COLUMN_CHAN = "chan"
}
}
@@ -0,0 +1,178 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.sqlite.SQLiteConstraintException
import ch.dissem.bitmessage.entity.ObjectMessage
import ch.dissem.bitmessage.entity.payload.ObjectType
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.factory.Factory
import ch.dissem.bitmessage.ports.Inventory
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
import ch.dissem.bitmessage.utils.UnixTime.now
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* [Inventory] implementation using the Android SQL API.
*/
class AndroidInventory(private val sql: SqlHelper) : Inventory {
private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>()
override fun getInventory(vararg streams: Long): List<InventoryVector> {
val result = LinkedList<InventoryVector>()
val now = now
for (stream in streams) {
for ((key, value) in getCache(stream)) {
if (value > now) {
result.add(key)
}
}
}
return result
}
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
cache[stream] = result
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
sql.readableDatabase.query(
TABLE_NAME, projection,
"stream = $stream", null, null, null, null
).use { c ->
while (c.moveToNext()) {
val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH))
val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES))
InventoryVector.fromHash(blob)?.let { result.put(it, expires) }
}
}
LOG.info("Stream #$stream inventory size: ${result.size}")
return result
}
return cache[stream] ?: synchronized(cache) {
return@synchronized cache[stream] ?: addToCache(stream)
}
}
override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys }
override fun getObject(vector: InventoryVector): ObjectMessage? {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
sql.readableDatabase.query(
TABLE_NAME, projection,
"hash = X'$vector'", null, null, null, null
).use { c ->
if (!c.moveToFirst()) {
LOG.info("Object requested that we don't have. IV: {}", vector)
return null
}
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size)
}
}
override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
val where = StringBuilder("1=1")
if (stream > 0) {
where.append(" AND stream = ").append(stream)
}
if (version > 0) {
where.append(" AND version = ").append(version)
}
if (types.isNotEmpty()) {
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
}
val result = LinkedList<ObjectMessage>()
sql.readableDatabase.query(
TABLE_NAME, projection,
where.toString(), null, null, null, null
).use { c ->
while (c.moveToNext()) {
val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) }
}
}
return result
}
override fun storeObject(objectMessage: ObjectMessage) {
val iv = objectMessage.inventoryVector
if (getCache(objectMessage.stream).containsKey(iv))
return
LOG.trace("Storing object {}", iv)
try {
// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
put(COLUMN_HASH, objectMessage.inventoryVector.hash)
put(COLUMN_STREAM, objectMessage.stream)
put(COLUMN_EXPIRES, objectMessage.expiresTime)
put(COLUMN_DATA, Encode.bytes(objectMessage))
put(COLUMN_TYPE, objectMessage.type)
put(COLUMN_VERSION, objectMessage.version)
}
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
getCache(objectMessage.stream)[iv] = objectMessage.expiresTime
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector)
override fun cleanup() {
val fiveMinutesAgo = now - 5 * MINUTE
sql.writableDatabase.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
private const val TABLE_NAME = "Inventory"
private const val COLUMN_HASH = "hash"
private const val COLUMN_STREAM = "stream"
private const val COLUMN_EXPIRES = "expires"
private const val COLUMN_DATA = "data"
private const val COLUMN_TYPE = "type"
private const val COLUMN_VERSION = "version"
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.DatabaseUtils
import ch.dissem.apps.abit.util.getText
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractLabelRepository
import ch.dissem.bitmessage.ports.MessageRepository
import org.jetbrains.anko.db.transaction
import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) :
AbstractLabelRepository() {
override fun find(where: String): List<Label> {
val result = LinkedList<Label>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR)
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
COLUMN_ORDER
).use { c ->
while (c.moveToNext()) {
result.add(getLabel(c, context))
}
}
return result
}
override fun save(label: Label) {
val db = sql.writableDatabase
if (label.id != null) {
val values = ContentValues()
values.put(COLUMN_LABEL, label.toString())
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
} else {
db.transaction {
val exists = DatabaseUtils.queryNumEntries(
db,
TABLE_NAME,
"label=?",
arrayOf(label.toString())
) > 0
if (exists) {
val values = ContentValues()
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString()))
} else {
val values = ContentValues()
values.put(COLUMN_LABEL, label.toString())
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.insertOrThrow(TABLE_NAME, null, values)
}
}
}
}
internal fun findLabels(msgId: Any) =
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
companion object {
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
private const val TABLE_NAME = "Label"
private const val COLUMN_ID = "id"
private const val COLUMN_LABEL = "label"
private const val COLUMN_TYPE = "type"
private const val COLUMN_COLOR = "color"
private const val COLUMN_ORDER = "ord"
internal fun getLabel(c: Cursor, context: Context): Label {
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
val text: String? = type?.getText(null, context)
val label = Label(
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
type,
c.getInt(c.getColumnIndex(COLUMN_COLOR))
)
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
return label
}
}
}
@@ -0,0 +1,359 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractMessageRepository
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Strings.hex
import org.jetbrains.anko.db.transaction
import java.io.ByteArrayInputStream
import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
if (label === LABEL_ARCHIVE || label === null) {
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
} else {
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
}
override fun findMessages(label: Label?, offset: Int, limit: Int) =
if (label === LABEL_ARCHIVE) {
super.findMessages(null as Label?, offset, limit)
} else {
super.findMessages(label, offset, limit)
}
fun count() = DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
null,
null
).toInt()
private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
if (separateIdentities) {
val identity = prefs.currentIdentity
return if (prefs.separateIdentities && identity != null) {
"AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
} else {
"" to emptyArray()
}
} else {
return "" to emptyArray()
}
}
override fun countUnread(label: Label?) = countUnread(label, false)
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
when {
label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
}
}
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
label == null -> if (selectIdentityQuery.isNotBlank()) {
"type = 'BROADCAST' OR recipient=? OR sender=?"
} else {
null
}
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
}
val result = LinkedList<UUID>()
sql.readableDatabase.query(
true,
TABLE_NAME,
projection,
where,
selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
while (c.moveToNext()) {
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
result.add(asUuid(uuidBytes))
}
}
return result
}
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
val inventoryVector = message.inventoryVector
if (inventoryVector == null || message.parents.isEmpty()) {
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
return
}
val childIV = inventoryVector.hash
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
// save new parents
var order = 0
val values = ContentValues()
for (parentIV in message.parents) {
getMessage(parentIV)?.let { parent ->
mergeConversations(db, parent.conversationId, message.conversationId)
order++
values.put("parent", parentIV.hash)
values.put("child", childIV)
values.put("pos", order)
values.put("conversation", UuidUtils.asBytes(message.conversationId))
db.insertOrThrow(PARENTS_TABLE_NAME, null, values)
}
}
}
/**
* Replaces every occurrence of the source conversation ID with the target ID
* @param db is used to keep everything within one transaction
* *
* @param source ID of the conversation to be merged
* *
* @param target ID of the merge target
*/
private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
val values = ContentValues()
values.put("conversation", UuidUtils.asBytes(target))
val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
db.update(TABLE_NAME, values, where, null)
db.update(PARENTS_TABLE_NAME, values, where, null)
}
override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
private fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
val result = LinkedList<Plaintext>()
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_ACK_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
)
sql.readableDatabase.query(
TABLE_NAME, projection,
"$where $selectIdentityQuery", selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
while (c.moveToNext()) {
result.add(getMessage(c))
}
}
return result
}
private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature(
Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))),
ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA)))
).build {
id = c.getLong(c.getColumnIndex(COLUMN_ID))
inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV)))
c.getString(c.getColumnIndex(COLUMN_SENDER))?.let {
from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
}
c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let {
to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
}
ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))
sent = c.getLong(c.getColumnIndex(COLUMN_SENT))
received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED))
status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))
ttl = c.getLong(c.getColumnIndex(COLUMN_TTL))
retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES))
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
if (!c.isNull(nextTryColumn)) {
nextTry = c.getLong(nextTryColumn)
}
conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))
labels = findLabels(id!!)
}
private fun findLabels(msgId: Any) =
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
override fun save(message: Plaintext) {
saveContactIfNecessary(message.from)
saveContactIfNecessary(message.to)
val db = sql.writableDatabase
db.transaction {
// save message
if (message.id == null) {
insert(db, message)
} else {
update(db, message)
}
updateParents(db, message)
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
// save labels
val values = ContentValues()
for (label in message.labels) {
values.put(JT_COLUMN_LABEL, label.id as Long?)
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
}
}
}
private fun getValues(message: Plaintext) = ContentValues(14).apply {
put(COLUMN_IV, message.inventoryVector?.hash)
put(COLUMN_TYPE, message.type.name)
put(COLUMN_SENDER, message.from.address)
put(COLUMN_RECIPIENT, message.to?.address)
put(COLUMN_DATA, Encode.bytes(message))
put(COLUMN_ACK_DATA, message.ackData)
put(COLUMN_SENT, message.sent)
put(COLUMN_RECEIVED, message.received)
put(COLUMN_STATUS, message.status.name)
put(COLUMN_INITIAL_HASH, message.initialHash)
put(COLUMN_TTL, message.ttl)
put(COLUMN_RETRIES, message.retries)
put(COLUMN_NEXT_TRY, message.nextTry)
put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
}
private fun insert(db: SQLiteDatabase, message: Plaintext) {
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
message.id = id
}
private fun update(db: SQLiteDatabase, message: Plaintext) {
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
}
override fun remove(message: Plaintext) {
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
}
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
val result = mutableListOf<Plaintext>()
val projection = arrayOf(
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_ACK_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
)
sql.readableDatabase.query(
TABLE_NAME, projection,
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
"$COLUMN_ID ASC",
"$limit"
).use { c ->
while (c.moveToNext()) {
result.add(getMessage(c))
}
}
return result
}
companion object {
private const val TABLE_NAME = "Message"
private const val COLUMN_ID = "id"
private const val COLUMN_IV = "iv"
private const val COLUMN_TYPE = "type"
private const val COLUMN_SENDER = "sender"
private const val COLUMN_RECIPIENT = "recipient"
private const val COLUMN_DATA = "data"
private const val COLUMN_ACK_DATA = "ack_data"
private const val COLUMN_SENT = "sent"
private const val COLUMN_RECEIVED = "received"
private const val COLUMN_STATUS = "status"
private const val COLUMN_TTL = "ttl"
private const val COLUMN_RETRIES = "retries"
private const val COLUMN_NEXT_TRY = "next_try"
private const val COLUMN_INITIAL_HASH = "initial_hash"
private const val COLUMN_CONVERSATION = "conversation"
private const val PARENTS_TABLE_NAME = "Message_Parent"
private const val JOIN_TABLE_NAME = "Message_Label"
private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id"
}
}
@@ -0,0 +1,203 @@
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDoneException
import android.database.sqlite.SQLiteStatement
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
import ch.dissem.bitmessage.ports.NodeRegistry
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
import ch.dissem.bitmessage.utils.Collections
import ch.dissem.bitmessage.utils.SqlStrings
import ch.dissem.bitmessage.utils.Strings.hex
import ch.dissem.bitmessage.utils.UnixTime
import ch.dissem.bitmessage.utils.UnixTime.DAY
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
import ch.dissem.bitmessage.utils.UnixTime.now
import ch.dissem.bitmessage.utils.max
import org.jetbrains.anko.db.transaction
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.concurrent.getOrSet
const val MAX_ENTRY_AGE = 7 * DAY
/**
* @author Christian Basler
*/
class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
private val loadExistingStatement = ThreadLocal<SQLiteStatement>()
private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap()
get() {
if (field.isEmpty())
field = loadStableNodes()
return field
}
init {
cleanUp()
}
private fun cleanUp() {
sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString()))
}
override fun clear() {
sql.writableDatabase.delete(TABLE_NAME, null, null)
}
private fun loadExistingTime(node: NetworkAddress): Long? {
val statement: SQLiteStatement = loadExistingStatement.getOrSet {
sql.writableDatabase.compileStatement(
"SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?"
)
}
statement.bindLong(1, node.stream)
statement.bindBlob(2, node.IPv6)
statement.bindLong(3, node.port.toLong())
return try {
statement.simpleQueryForLong()
} catch (e: SQLiteDoneException) {
null
}
}
override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> {
val result = LinkedList<NetworkAddress>()
sql.readableDatabase.query(
TABLE_NAME,
arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME),
"stream IN (?)",
arrayOf(SqlStrings.join(*streams)), null, null,
"time DESC",
limit.toString()
).use { c ->
while (c.moveToNext()) {
result.add(NetworkAddress(
time = c.getLong(c.getColumnIndex(COLUMN_TIME)),
stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)),
services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)),
IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)),
port = c.getInt(c.getColumnIndex(COLUMN_PORT))
))
}
}
if (result.isEmpty()) {
streams
.asSequence()
.mapNotNull { stableNodes[it] }
.filterNot { it.isEmpty() }
.mapTo(result) { Collections.selectRandom(it) }
}
return result
}
override fun offerAddresses(nodes: List<NetworkAddress>) {
sql.writableDatabase.transaction {
cleanUp()
nodes
.filter {
// Don't accept nodes from the future, it might be a trap
it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE
}
.forEach { node ->
synchronized(this) {
val existing = loadExistingTime(node)
if (existing == null) {
insert(node)
} else if (node.time > existing) {
update(node)
}
}
}
}
}
private fun insert(node: NetworkAddress) {
try {
// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
put(COLUMN_STREAM, node.stream)
put(COLUMN_ADDRESS, node.IPv6)
put(COLUMN_PORT, node.port)
put(COLUMN_SERVICES, node.services)
put(COLUMN_TIME,
if (node.time > UnixTime.now) {
// This might be an attack, let's not use those nodes with priority
UnixTime.now - 7 * UnixTime.DAY
} else {
node.time
}
)
}
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun update(node: NetworkAddress) {
try {
val time = if (node.time > UnixTime.now) {
// This might be an attack, let's not use those nodes with priority
UnixTime.now - 7 * UnixTime.DAY
} else {
node.time
}
// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
put(COLUMN_SERVICES, node.services)
put(COLUMN_TIME, max(node.time, time))
}
sql.writableDatabase.update(
TABLE_NAME,
values,
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun remove(node: NetworkAddress) {
try {
sql.writableDatabase.delete(
TABLE_NAME,
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun cleanup() {
try {
sql.writableDatabase.delete(
TABLE_NAME,
"time<${UnixTime.now - 8 * DAY}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
private const val TABLE_NAME = "Node"
private const val COLUMN_STREAM = "stream"
private const val COLUMN_ADDRESS = "address"
private const val COLUMN_PORT = "port"
private const val COLUMN_SERVICES = "services"
private const val COLUMN_TIME = "time"
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.sqlite.SQLiteConstraintException
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.LinkedList
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.entity.ObjectMessage
import ch.dissem.bitmessage.factory.Factory
import ch.dissem.bitmessage.ports.ProofOfWorkRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Singleton.cryptography
import ch.dissem.bitmessage.utils.Strings.hex
/**
* @author Christian Basler
*/
class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder {
private lateinit var bmc: InternalContext
override fun setContext(context: InternalContext) {
this.bmc = context
}
override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID)
sql.readableDatabase.query(
TABLE_NAME, projection,
"initial_hash=X'${hex(initialHash)}'",
null, null, null, null
).use { c ->
if (c.moveToFirst()) {
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
ProofOfWorkRepository.Item(
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
)
} else {
ProofOfWorkRepository.Item(
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
)
}
}
}
throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}")
}
override fun getItems(): List<ByteArray> {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_INITIAL_HASH)
val result = LinkedList<ByteArray>()
sql.readableDatabase.query(
TABLE_NAME, projection, null, null, null, null, null
).use { c ->
while (c.moveToNext()) {
val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH))
result.add(initialHash)
}
}
return result
}
override fun putObject(item: ProofOfWorkRepository.Item) {
try {
// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
put(COLUMN_DATA, Encode.bytes(item.objectMessage))
put(COLUMN_VERSION, item.objectMessage.version)
put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
put(COLUMN_EXTRA_BYTES, item.extraBytes)
item.message?.let { message ->
put(COLUMN_EXPIRATION_TIME, item.expirationTime)
put(COLUMN_MESSAGE_ID, message.id as Long?)
}
}
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) =
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
override fun removeObject(initialHash: ByteArray) {
sql.writableDatabase.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
private const val TABLE_NAME = "POW"
private const val COLUMN_INITIAL_HASH = "initial_hash"
private const val COLUMN_DATA = "data"
private const val COLUMN_VERSION = "version"
private const val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
private const val COLUMN_EXTRA_BYTES = "extra_bytes"
private const val COLUMN_EXPIRATION_TIME = "expiration_time"
private const val COLUMN_MESSAGE_ID = "message_id"
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.UuidUtils
import java.util.*
/**
* Handles database migration and provides access.
*/
class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION)
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf(
0 to {
executeMigration(db, "V1.0__Create_table_inventory")
executeMigration(db, "V1.1__Create_table_address")
executeMigration(db, "V1.2__Create_table_message")
},
1 to {
// executeMigration(db, "V2.0__Update_table_message");
executeMigration(db, "V2.1__Create_table_POW")
},
2 to {
executeMigration(db, "V3.0__Update_table_address")
},
3 to {
executeMigration(db, "V3.1__Update_table_POW")
executeMigration(db, "V3.2__Update_table_message")
},
4 to {
executeMigration(db, "V3.3__Create_table_node")
},
5 to {
executeMigration(db, "V3.4__Add_label_outbox")
},
6 to {
executeMigration(db, "V4.0__Create_table_message_parent")
},
7 to {
setMissingConversationIds(db)
}
).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() }
/**
* Set UUIDs for all messages that have no conversation ID
*/
private fun setMissingConversationIds(db: SQLiteDatabase) = db.query(
"Message", arrayOf("id"),
"conversation IS NULL", null, null, null, null
).use { c ->
while (c.moveToNext()) {
val id = c.getLong(0)
setMissingConversationId(id, db)
}
}
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
val values = ContentValues(1).apply {
put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
}
db.update("Message", values, "id=?", arrayOf(id.toString()))
}
private fun executeMigration(db: SQLiteDatabase, name: String) {
for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) {
db.execSQL(statement)
}
}
companion object {
// If you change the database schema, you must increment the database version.
private const val DATABASE_VERSION = 7
const val DATABASE_NAME = "jabit.db"
}
}
@@ -0,0 +1,117 @@
package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.notification.BatchNotification
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
import org.jetbrains.anko.doAsync
import java.util.*
class BatchProcessorService : Service() {
private lateinit var notification: BatchNotification
override fun onCreate() {
notification = BatchNotification(this)
}
override fun onBind(intent: Intent) = BatchBinder(this)
class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() {
private val notification = service.notification
fun process(job: Job) = synchronized(queue) {
ContextCompat.startForegroundService(
service,
Intent(service, BatchProcessorService::class.java)
)
service.startForeground(
ONGOING_NOTIFICATION_ID,
notification.notification
)
if (!working) {
working = true
service.processQueue(job)
} else {
queue.add(job)
}
}
}
private fun processQueue(job: Job) {
doAsync {
var next: Job? = job
while (next != null) {
next.process(notification)
synchronized(queue) {
next = queue.poll()
if (next == null) {
working = false
stopForeground(true)
stopSelf()
}
}
}
}
}
companion object {
private var working = false
private val queue = LinkedList<Job>()
}
}
interface Job {
val icon: Int
@DrawableRes get
val description: Int
@StringRes get
val numberOfItems: Int
var numberOfProcessedItems: Int
/**
* Runs the job. This shouldn't happen in a separate thread, as this is handled by the service.
*/
fun process(notification: BatchNotification)
}
data class SimpleJob<T>(
override val numberOfItems: Int,
/**
* Provides the next batch of items, given the last item of the previous batch,
* or null for the first batch.
*/
private val provider: (T?) -> List<T>,
/**
* Processes an item.
*/
private val processor: (T) -> Unit,
override val icon: Int,
override val description: Int
) : Job {
override var numberOfProcessedItems: Int = 0
override fun process(notification: BatchNotification) {
notification.update(this)
var batch = provider.invoke(null)
while (batch.isNotEmpty()) {
Thread.yield()
batch.forEach {
processor.invoke(it)
Thread.yield()
}
numberOfProcessedItems += batch.size
notification.update(this)
batch = provider.invoke(batch.last())
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service
import android.app.IntentService
import android.content.Intent
import ch.dissem.apps.abit.util.network
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
/**
* @author Christian Basler
*/
class BitmessageIntentService : IntentService("BitmessageIntentService") {
private lateinit var bmc: BitmessageContext
override fun onCreate() {
super.onCreate()
bmc = Singleton.getBitmessageContext(this)
}
override fun onHandleIntent(intent: Intent?) {
intent?.let {
if (it.hasExtra(EXTRA_DELETE_MESSAGE)) {
val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext
bmc.labeler.delete(item)
bmc.messages.save(item)
Singleton.getMessageListener(this).resetNotification()
}
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
network.enableNode()
}
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
network.disableNode()
}
}
}
companion object {
const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"
const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"
const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"
}
}
@@ -0,0 +1,18 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import org.jetbrains.anko.doAsync
class CleanupService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
doAsync {
Singleton.getBitmessageContext(this@CleanupService).cleanup()
jobFinished(params, false)
}
return true
}
override fun onStopJob(params: JobParameters?) = false
}
@@ -0,0 +1,103 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.doAsync
/**
* Starts the full node if
* * it is active
* * it is not already running
*
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
*/
class NodeStartupService : JobService() {
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
private lateinit var notification: NetworkNotification
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (bmc.isRunning() && !preferences.connectionAllowed) {
bmc.shutdown()
}
}
}
override fun onCreate() {
super.onCreate()
notification = NetworkNotification(this)
}
override fun onStartJob(params: JobParameters?): Boolean {
if (preferences.online) {
registerReceiver(
connectivityReceiver,
IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
)
startForeground(0, notification.notification)
NodeStartupService.running = false
if (!isRunning) {
running = true
notification.connecting()
if (!bmc.isRunning()) {
bmc.startup()
}
notification.show()
}
}
return true
}
override fun onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown()
}
running = false
notification.showShutdown()
doAsync {
bmc.cleanup()
}
try {
unregisterReceiver(connectivityReceiver)
} catch (_: IllegalArgumentException) {
// For some reason, onStartJob wasn't called so the receiver isn't registered.
// Let's just ignore this.
}
stopSelf()
}
/**
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
* depending on Android version.
*/
override fun onStopJob(params: JobParameters?): Boolean {
network.scheduleNodeStart()
return false
}
companion object {
@Volatile
private var running = false
val isRunning: Boolean
get() = running && Singleton.bitmessageContext?.isRunning() == true
val status: Property
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
import ch.dissem.apps.abit.util.PowStats
import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import java.util.*
/**
* The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be
* killed by the system before the nonce is found.
*/
class ProofOfWorkService : Service() {
private lateinit var notification: ProofOfWorkNotification
override fun onCreate() {
notification = ProofOfWorkNotification(this)
}
override fun onBind(intent: Intent) = PowBinder(this)
class PowBinder internal constructor(private val service: ProofOfWorkService) : Binder() {
private val notification = service.notification
fun process(item: PowItem) = synchronized(queue) {
ContextCompat.startForegroundService(
service,
Intent(service, ProofOfWorkService::class.java)
)
service.startForeground(
ONGOING_NOTIFICATION_ID,
notification.notification
)
if (!calculating) {
calculating = true
service.calculateNonce(item)
} else {
queue.add(item)
notification.update(queue.size).show()
}
}
}
data class PowItem(
val initialHash: ByteArray,
val targetValue: ByteArray,
val callback: ProofOfWorkEngine.Callback
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PowItem
if (!Arrays.equals(initialHash, other.initialHash)) return false
if (!Arrays.equals(targetValue, other.targetValue)) return false
return true
}
override fun hashCode(): Int {
var result = Arrays.hashCode(initialHash)
result = 31 * result + Arrays.hashCode(targetValue)
return result
}
}
private fun calculateNonce(item: PowItem) {
notification.start(item)
val startTime = System.currentTimeMillis()
engine.calculateNonce(
item.initialHash,
item.targetValue,
object : ProofOfWorkEngine.Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
notification.finished()
val time = System.currentTimeMillis() - startTime
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
try {
item.callback.onNonceCalculated(initialHash, nonce)
} finally {
var next: PowItem? = null
synchronized(queue) {
next = queue.poll()
if (next == null) {
calculating = false
stopForeground(true)
stopSelf()
} else {
notification.update(queue.size).show()
}
}
next?.let { calculateNonce(it) }
}
}
})
}
companion object {
// Object to use as a thread-safe lock
private val engine = MultiThreadedPOWEngine()
private val queue = LinkedList<PowItem>()
private var calculating: Boolean = false
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import java.util.LinkedList
import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder
import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import android.content.Context.BIND_AUTO_CREATE
/**
* Proof of Work engine that uses the Proof of Work service.
*/
class ServicePowEngine(private val ctx: Context) : ProofOfWorkEngine {
private val queue = LinkedList<PowItem>()
private var service: PowBinder? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) = synchronized(lock) {
this@ServicePowEngine.service = service as PowBinder
while (!queue.isEmpty()) {
service.process(queue.poll())
}
}
override fun onServiceDisconnected(name: ComponentName) {
service = null
}
}
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
val item = PowItem(initialHash, target, callback)
synchronized(lock) {
service?.process(item) ?: {
queue.add(item)
ctx.bindService(Intent(ctx, ProofOfWorkService::class.java), connection, BIND_AUTO_CREATE)
}.invoke()
}
}
companion object {
private val lock = Any()
}
}
@@ -0,0 +1,191 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service
import android.content.Context
import android.widget.Toast
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.MessageListener
import ch.dissem.apps.abit.repository.*
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService
import ch.dissem.bitmessage.utils.TTL
import ch.dissem.bitmessage.utils.UnixTime.DAY
import io.reactivex.subjects.BehaviorSubject
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.lang.ref.WeakReference
/**
* Provides singleton objects across the application.
*/
object Singleton {
var currentLabel = BehaviorSubject.create<Label>()
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
val labeler = DefaultLabeler().apply {
listener = { message, added, removed ->
MainActivity.apply {
runOnUiThread {
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
currentLabel.value?.let {label ->
when {
label.type == Label.Type.TRASH
&& added.all { it.type == Label.Type.TRASH }
&& removed.any { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from trash
swipeableMessageAdapter.remove(message)
}
label.type == Label.Type.UNREAD
&& added.all { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message)
}
label == AndroidLabelRepository.LABEL_ARCHIVE && !added.isEmpty() -> {
// work-around for messages in archive, which isn't an actual label but an absence of labels
swipeableMessageAdapter.remove(message)
}
added.contains(label) -> {
// in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in
swipeableMessageAdapter.addFirst(message)
}
removed.contains(label) -> {
swipeableMessageAdapter.remove(message)
}
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
swipeableMessageAdapter.update(message)
}
}
}
}
}
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
updateUnread()
}
}
}
}
var bitmessageContext: BitmessageContext? = null
private set
private var conversationService: ConversationService? = null
private var messageListener: MessageListener? = null
private var identity: BitmessageAddress? = null
private var powRepo: AndroidProofOfWorkRepository? = null
private var creatingIdentity: Boolean = false
fun getBitmessageContext(context: Context): BitmessageContext =
init({ bitmessageContext }, { bitmessageContext = it }) {
BitmessageContext.build {
TTL.pubkey = 2 * DAY
val ctx = context.applicationContext
val sqlHelper = SqlHelper(ctx)
proofOfWorkEngine = ServicePowEngine(ctx)
cryptography = SpongyCryptography()
nodeRegistry = AndroidNodeRegistry(sqlHelper)
inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper)
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx)
labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false
preferences.port = context.preferences.listeningPort
}
}
fun updateMessageListAdapterInListener(adapter: SwipeableMessageAdapter) {
swipeableMessageAdapter = WeakReference(adapter)
}
fun getMessageListener(ctx: Context) = init({ messageListener }, { messageListener = it }) { MessageListener(ctx) }
fun getLabelRepository(ctx: Context) = getBitmessageContext(ctx).labels as AndroidLabelRepository
fun getMessageRepository(ctx: Context) = getBitmessageContext(ctx).messages as AndroidMessageRepository
fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository
fun getIdentity(ctx: Context): BitmessageAddress? =
init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc ->
val identities = bmc.addresses.getIdentities()
if (identities.isNotEmpty()) {
identities[0]
} else {
if (!creatingIdentity) {
creatingIdentity = true
doAsync {
val identity = bmc.createIdentity(false,
Pubkey.Feature.DOES_ACK)
identity.alias = ctx.getString(R.string.alias_default_identity)
bmc.addresses.save(identity)
uiThread {
Singleton.identity = identity
Toast.makeText(ctx,
R.string.toast_identity_created,
Toast.LENGTH_SHORT).show()
MainActivity.apply { addIdentityEntry(identity) }
}
}
}
null
}
}
fun setIdentity(identity: BitmessageAddress) {
if (identity.privateKey == null)
throw IllegalArgumentException("Identity expected, but no private key available")
Singleton.identity = identity
}
fun getConversationService(ctx: Context) = init(ctx, { conversationService }, { conversationService = it }) { ConversationService(it.messages) }
private inline fun <T> init(crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: () -> T): T =
getter() ?: {
synchronized(Singleton) {
getter() ?: {
val v = creator()
setter(v)
v
}.invoke()
}
}.invoke()
private inline fun <T> init(ctx: Context, crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: (BitmessageContext) -> T): T =
getter() ?: {
val bmc = getBitmessageContext(ctx)
synchronized(Singleton) {
getter() ?: {
val v = creator(bmc)
setter(v)
v
}.invoke()
}
}.invoke()
}
@@ -0,0 +1,19 @@
package ch.dissem.apps.abit.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_BOOT_COMPLETED
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
/**
* Starts the Bitmessage "full node" service if conditions allow it
*/
class StartServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == ACTION_BOOT_COMPLETED && context.preferences.online) {
context.network.enableNode(false)
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.util
import android.content.Context
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.Plaintext
import java.io.IOException
import java.util.*
/**
* Helper class to work with Assets.
*/
object Assets {
fun readSqlStatements(ctx: Context, name: String): List<String> {
try {
val `in` = ctx.assets.open(name)
val scanner = Scanner(`in`, "UTF-8").useDelimiter(";")
val result = LinkedList<String>()
while (scanner.hasNext()) {
val statement = scanner.next().trim { it <= ' ' }
if ("" != statement) {
result.add(statement)
}
}
return result
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
fun Plaintext.Status.getDrawable() = when (this) {
Plaintext.Status.RECEIVED -> 0
Plaintext.Status.DRAFT -> R.drawable.draft
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
Plaintext.Status.SENT -> R.drawable.sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
else -> 0
}
fun Plaintext.Status.getString() = when (this) {
Plaintext.Status.RECEIVED -> R.string.status_received
Plaintext.Status.DRAFT -> R.string.status_draft
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
Plaintext.Status.SENT -> R.string.status_sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
else -> 0
}
@@ -0,0 +1,37 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.util
import java.util.regex.Pattern
/**
* @author Christian Basler
*/
object Constants {
const val PREFERENCE_ONLINE = "online"
const val PREFERENCE_WIFI_ONLY = "wifi_only"
const val PREFERENCE_REQUIRE_CHARGING = "require_charging"
const val PREFERENCE_EMULATE_CONVERSATIONS = "emulate_conversations"
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
const val PREFERENCE_POW_COUNT = "pow_count"
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
const val BITMESSAGE_URL_SCHEMA = "bitmessage:"
val BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b")!!
}
@@ -0,0 +1,103 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color.BLACK
import android.graphics.Color.WHITE
import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Base64.URL_SAFE
import android.view.Menu
import android.view.MenuItem
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.Drawables.QR_CODE_SIZE
import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.IIcon
import org.slf4j.LoggerFactory
import java.io.ByteArrayOutputStream
/**
* Some helper methods to work with drawables.
*/
object Drawables {
internal val LOG = LoggerFactory.getLogger(Drawables::class.java)
internal const val QR_CODE_SIZE = 350
fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
val item = menu.findItem(menuItem)
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
return item
}
}
fun Drawable.toBitmap(width: Int, height: Int = width): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
setBounds(0, 0, canvas.width, canvas.height)
draw(canvas)
return bitmap
}
fun BitmessageAddress.qrCode(): Bitmap? {
val link = StringBuilder()
link.append(Constants.BITMESSAGE_URL_SCHEMA)
link.append(address)
if (alias != null) {
link.append("?label=").append(alias)
}
pubkey?.apply {
link.append(if (alias == null) '?' else '&')
val pubkey = ByteArrayOutputStream()
writer().writeUnencrypted(pubkey)
link.append("pubkey=")
.append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
}
val result: BitMatrix
try {
result = MultiFormatWriter().encode(
link.toString(),
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
)
} catch (e: WriterException) {
Drawables.LOG.error(e.message, e)
return null
}
val w = result.width
val h = result.height
val pixels = IntArray(w * h)
for (y in 0 until h) {
val offset = y * w
for (x in 0 until w) {
pixels[offset + x] = if (result.get(x, y)) BLACK else WHITE
}
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
return bitmap
}
@@ -0,0 +1,95 @@
package ch.dissem.apps.abit.util
import android.content.Context
import android.net.Uri
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.exports.ContactExport
import ch.dissem.bitmessage.exports.MessageExport
import ch.dissem.bitmessage.utils.UnixTime
import com.beust.klaxon.JsonArray
import com.beust.klaxon.Parser
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
/**
* Helper object for data export and import.
*/
object Exports {
fun exportData(target: File, ctx: Context): File {
val temp = if (target.isDirectory) {
File(target, "export-${UnixTime.now}.zip")
} else {
target
}
ZipOutputStream(FileOutputStream(temp)).use { zip ->
zip.putNextEntry(ZipEntry("contacts.json"))
val addressRepo = Singleton.getAddressRepository(ctx)
val exportContacts = ContactExport.exportContacts(addressRepo.getContacts())
zip.write(
exportContacts.toJsonString(true).toByteArray()
)
zip.closeEntry()
val labelRepo = Singleton.getLabelRepository(ctx)
zip.putNextEntry(ZipEntry("labels.json"))
val exportLabels = MessageExport.exportLabels(labelRepo.getLabels())
zip.write(
exportLabels.toJsonString(true).toByteArray()
)
zip.closeEntry()
zip.putNextEntry(ZipEntry("messages.json"))
val messageRepo = Singleton.getMessageRepository(ctx)
val exportMessages = MessageExport.exportMessages(messageRepo.getAllMessages())
zip.write(
exportMessages.toJsonString(true).toByteArray()
)
zip.closeEntry()
}
return temp
}
fun importData(zipFile: Uri, ctx: Context) {
val bmc = Singleton.getBitmessageContext(ctx)
val labels = mutableMapOf<String, Label>()
processEntry(ctx, zipFile, "contacts.json") { json ->
ContactExport.importContacts(json).forEach { contact ->
bmc.addresses.save(contact)
}
}
bmc.labels.getLabels().forEach { label ->
labels[label.toString()] = label
}
processEntry(ctx, zipFile, "labels.json") { json ->
MessageExport.importLabels(json).forEach { label ->
if (!labels.contains(label.toString())) {
bmc.labels.save(label)
labels[label.toString()] = label
}
}
}
processEntry(ctx, zipFile, "messages.json") { json ->
MessageExport.importMessages(json, labels).forEach { message ->
bmc.messages.save(message)
}
}
}
private fun processEntry(ctx: Context, zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) =
ZipInputStream(ctx.contentResolver.openInputStream(zipFile)).use { zip ->
var nextEntry = zip.nextEntry
while (nextEntry != null) {
if (nextEntry.name == entry) {
processor(Parser().parse(zip) as JsonArray<*>)
}
nextEntry = zip.nextEntry
}
}
}
@@ -0,0 +1,42 @@
package ch.dissem.apps.abit.util
import android.content.Context
import androidx.annotation.ColorInt
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
/*
* Helper methods to help with translating the default labels, getting label colors and so on.
*/
fun Label.getText(ctx: Context): String = type?.getText(toString(), ctx) ?: toString()
fun Label.Type.getText(alternative: String?, ctx: Context) = when (this) {
Label.Type.INBOX -> ctx.getString(R.string.inbox)
Label.Type.DRAFT -> ctx.getString(R.string.draft)
Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
Label.Type.SENT -> ctx.getString(R.string.sent)
Label.Type.UNREAD -> ctx.getString(R.string.unread)
Label.Type.TRASH -> ctx.getString(R.string.trash)
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
else -> alternative
}
fun Label.getIcon(): IIcon = when (type) {
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
Label.Type.SENT -> CommunityMaterial.Icon.cmd_send
Label.Type.BROADCAST -> CommunityMaterial.Icon.cmd_rss
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
else -> CommunityMaterial.Icon.cmd_label
}
@ColorInt
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
color
} else default
@@ -0,0 +1,77 @@
package ch.dissem.apps.abit.util
import android.app.Activity
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
import ch.dissem.apps.abit.service.CleanupService
import ch.dissem.apps.abit.service.NodeStartupService
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
val Context.network get() = NetworkUtils.getInstance(this)
class NetworkUtils internal constructor(private val ctx: Context) {
private val jobScheduler by lazy { ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler }
fun enableNode(ask: Boolean = true) {
if (ask && !ctx.preferences.connectionAllowed) {
// Ask for connection
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
ctx.startActivity(dialogIntent)
} else {
scheduleNodeStart()
}
}
fun disableNode() {
jobScheduler.cancelAll()
}
fun scheduleNodeStart() {
JobInfo.Builder(0, ComponentName(ctx, NodeStartupService::class.java)).let { builder ->
when {
ctx.preferences.wifiOnly ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
else ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
}
builder.setRequiresCharging(ctx.preferences.requireCharging)
builder.setPersisted(true)
jobScheduler.schedule(builder.build())
}
JobInfo.Builder(1, ComponentName(ctx, CleanupService::class.java)).let { builder ->
builder.setPeriodic(TimeUnit.DAYS.toMillis(1))
builder.setRequiresDeviceIdle(true)
builder.setRequiresCharging(true)
jobScheduler.schedule(builder.build())
}
}
companion object {
private var instance: WeakReference<NetworkUtils>? = null
internal fun getInstance(ctx: Context): NetworkUtils {
var networkUtils = instance?.get()
if (networkUtils == null) {
networkUtils = NetworkUtils(ctx.applicationContext)
instance = WeakReference(networkUtils)
}
return networkUtils
}
}
}

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