Compare commits

...

51 Commits

Author SHA1 Message Date
6d7b77fd4b Update .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-08 14:55:02 +01:00
6b8066d473 Add .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-08 14:41:36 +01:00
0405d9e04f 🤯 Update dependencies, switch to AndroidX 2018-10-15 20:00:52 +02:00
e67a4ea71b 🐛 Fix connectivity issue 2018-08-24 17:34:58 +02:00
a9602368fb 🎉 Separate messages by identity
Also, allow deleting all messages/conversations in a list
2018-08-24 17:32:45 +02:00
9f2508c1a5 Merge branch 'feature/fix-connectivity-issues' into develop 2018-08-21 07:39:57 +02:00
6128fd32f9 👻 Clean up services 2018-08-21 07:39:16 +02:00
ccfdff7fd8 😎 Make code more Kotliney 2018-07-11 16:59:50 +02:00
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
87bc01701c ⬆️ Bump dependencies 2018-07-05 18:40:48 +02:00
6878f80a54 🐛 Fix connectivity issues 2018-06-14 07:07:48 +02:00
a01f116065 🐛 Fix connectivity issues 2018-06-14 06:56:07 +02:00
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
8b89d81970 😴 Minor improvements 2018-06-13 19:48:00 +02:00
76317a2488 🔀 Merge branch 'feature/conversations' into develop 2018-06-12 16:58:19 +02:00
85f114a33d 🔌 Add preference to connect on charging only 2018-06-12 16:56:15 +02:00
9e7f247763 ⬆️ Bump android build tools version 2018-06-12 16:54:00 +02:00
ec4615b639 🐘 Tweak memory usage 2018-06-12 16:52:42 +02:00
2ddd78dfe2 📡 Improve and simplify connectivity handling 2018-06-04 12:55:00 +02:00
90bb538692 🚀 Improve performance 2018-05-25 20:48:42 +02:00
9cc07f73ae 🐛 Prevent ANR 2018-05-24 21:08:06 +02:00
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
725ec60fd4 📄 Add some project documentation
🤝 Code of Conduct
👩‍💻 Contribution
📄 License
2018-05-17 11:06:20 +00:00
60c4a4d8a0 ⬆️ Kotlin version bump 2018-04-22 12:06:10 +02:00
6585876b25 🚸 Add number of messages to conversation list item 2018-04-22 11:29:39 +02:00
b1fd9d9ef9 😴 Minor code style improvements 2018-04-20 17:51:14 +02:00
e05d27bfbc 🎨 Conversation rendering improvements 2018-04-20 07:04:31 +02:00
be7a7f1af6 🎨 Identicon rendering improvements 2018-04-20 07:00:55 +02:00
61e579c0d4 🚸 Improve settings structure 2018-04-17 19:55:56 +02:00
eee1be873a 🎨 Add new icon vor Oreo and later 2018-04-17 09:42:25 +02:00
4c213d3e9c Merge branch 'develop' into feature/conversations 2018-04-14 20:44:19 +02:00
76cb5df998 🚸 Improved network notification 2018-04-14 20:42:53 +02:00
3026ae8505 🐛 Fixed bug 2018-04-14 20:42:10 +02:00
412180f443 Add batch service for migrating existing messages 2018-04-14 20:27:29 +02:00
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
85562efc0d ⬆️ Update Android build tools 2018-04-13 12:39:20 +02:00
a89f80f400 🎨 minor multi-identicon improvements/fixes 2018-04-13 07:45:31 +02:00
1426b786e8 Add message grouping by subject 2018-04-03 22:14:46 +02:00
6a311a0346 Fix minor lint warning 2018-04-03 22:14:00 +02:00
9b75a8c2ef Fixed tests 2018-04-03 22:13:23 +02:00
4e5ba4401a Improved multi identicon background colour 2018-04-03 22:12:13 +02:00
f374748f71 Fixed tests and updated dependencies 2018-04-03 22:11:37 +02:00
49e77199b0 Improve utilities 2018-03-23 17:50:43 +01:00
46e5bb7ece Improve conversation view 2018-03-18 07:00:21 +01:00
8004865e01 Nicer labels 2018-03-12 21:18:10 +01:00
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
40f8bc87a2 Bump dependencies 2018-03-05 09:43:40 +01:00
0d1cfff883 Fix lint issues 2018-02-27 12:54:30 +01:00
d7b7b11cdf Fix deleting messages from archive 2018-02-27 06:43:45 +01:00
d8d5f70b37 Merge tag '1.0-rc1.1' into develop
1.0-rc1.1
2018-02-25 23:38:45 +01:00
16f1dfa6f6 Merge tag '1.0-rc1' into develop
1.0-rc1
2018-02-24 08:50:31 +01:00
166 changed files with 3452 additions and 2691 deletions

9
.drone.yml Normal file
View File

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: androidsdk/android-28
commands:
- ./gradlew assemble
- ./gradlew check

46
CODE_OF_CONDUCT.md Normal file
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
CONTRIBUTING.md Normal file
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`.

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
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.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"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.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
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.

View File

@ -13,13 +13,16 @@ if (project.hasProperty("project.configs")
//noinspection GroovyMissingReturnStatement //noinspection GroovyMissingReturnStatement
android { android {
compileSdkVersion 27 compileSdkVersion 28
buildToolsVersion "26.0.2" buildToolsVersion "28.0.3"
signingConfigs {
release
}
defaultConfig { defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}" applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 28
versionCode 23 versionCode 23
versionName "1.0-rc1" versionName "1.0-rc1"
multiDexEnabled true multiDexEnabled true
@ -51,63 +54,68 @@ android {
//ext.jabitVersion = '2.0.4' //ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-refactoring-SNAPSHOT' ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
ext.supportVersion = '27.0.2' ext.supportVersion = '27.1.1'
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version" implementation "org.jetbrains.anko:anko:$anko_version"
implementation "com.android.support:appcompat-v7:$supportVersion" implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "com.android.support:preference-v7:$supportVersion" implementation 'androidx.preference:preference:1.0.0'
implementation "com.android.support:cardview-v7:$supportVersion" implementation 'androidx.cardview:cardview:1.0.0'
implementation "com.android.support:support-v13:$supportVersion" implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation "com.android.support:preference-v14:$supportVersion" implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation "com.android.support:design:$supportVersion" implementation 'com.google.android.material:material:1.0.0'
implementation "com.android.support:multidex:1.0.2" 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-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion" implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion" implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion" implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
implementation "ch.dissem.jabit:jabit-exports:$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 'org.slf4j:slf4j-android:1.7.25'
implementation 'com.mikepenz:materialize:1.1.2@aar' implementation 'com.mikepenz:materialize:1.2.0-rc01@aar'
implementation('com.mikepenz:materialdrawer:6.0.2@aar') { implementation('com.mikepenz:materialdrawer:6.1.0-rc01.2@aar') {
transitive = true transitive = true
} }
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') { implementation('com.mikepenz:aboutlibraries:6.2.0-rc01@aar') {
transitive = true transitive = true
} }
implementation "com.mikepenz:iconics-core:3.0.0@aar" implementation "com.mikepenz:iconics-core:3.1.0-rc01@aar"
implementation "com.mikepenz:iconics-views:3.0.0@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:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar' implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar' implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
implementation 'com.google.zxing:core:3.3.1' implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8' implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
implementation 'com.github.amlcurran.showcaseview:library:5.4.3' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
transitive = true
}
implementation 'com.github.angads25:filepicker:1.1.1' implementation 'com.github.angads25:filepicker:1.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.0.2' 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 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.13.0' testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.hamcrest:hamcrest-library:1.3' testImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0' testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.6.1' testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation "org.robolectric:shadows-multidex:3.6.1" testImplementation "org.robolectric:shadows-multidex:3.7.1"
androidTestImplementation "com.android.support:multidex:1.0.2" androidTestImplementation "androidx.multidex:multidex:2.0.0"
} }
idea.module { idea.module {

View File

@ -6,15 +6,15 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<application <application
android:name="android.support.multidex.MultiDexApplication" android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -119,23 +119,13 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".service.BitmessageService"
android:exported="false" />
<service <service
android:name=".service.ProofOfWorkService" android:name=".service.ProofOfWorkService"
android:exported="false" /> android:exported="false" />
<!-- Synchronization -->
<provider
android:name=".synchronization.StubProvider"
android:authorities="ch.dissem.apps.abit.provider"
android:exported="false"
android:syncable="true" />
<!-- Exports --> <!-- Exports -->
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="ch.dissem.apps.abit.fileprovider" android:authorities="ch.dissem.apps.abit.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
@ -144,60 +134,32 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<service
android:name=".synchronization.AuthenticatorService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".synchronization.SyncService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
<service <service
android:name=".service.BitmessageIntentService" android:name=".service.BitmessageIntentService"
android:exported="false" /> android:exported="false" />
<!-- Receive Wi-Fi connection state changes --> <!-- Receive Wi-Fi connection state changes -->
<receiver <receiver
android:name=".listener.WifiReceiver" android:name=".service.StartServiceReceiver">
android:enabled="@bool/is_pre_api_21">
<intent-filter>
<!-- This is bad for battery life, but needed on older devices to check
if WiFi is available. Let's be honest, the whole app is bad for
battery life. -->
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
</intent-filter>
</receiver>
<receiver
android:name=".service.StartServiceReceiver"
android:enabled="@bool/is_post_api_21">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:name=".service.StartupNodeOnWifiService" android:name=".service.NodeStartupService"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" /> 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 <activity
android:name=".StatusActivity" android:name=".StatusActivity"
android:label="@string/title_activity_status" android:label="@string/title_activity_status"

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.ListFragment import androidx.fragment.app.ListFragment
import android.view.View import android.view.View
import android.widget.ListView import android.widget.ListView
@ -27,7 +27,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> { abstract class AbstractItemListFragment<in L, T> : ListFragment(), ListHolder<L> {
/** /**
* The fragment's current callback object, which is notified of list item * The fragment's current callback object, which is notified of list item
* clicks. * clicks.

View File

@ -19,13 +19,14 @@ package ch.dissem.apps.abit
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.qrCode
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.wif.WifExporter import ch.dissem.bitmessage.wif.WifExporter
import com.mikepenz.community_material_typeface_library.CommunityMaterial import com.mikepenz.community_material_typeface_library.CommunityMaterial
@ -185,7 +186,7 @@ class AddressDetailFragment : Fragment() {
} }
// QR code // QR code
qr_code.setImageBitmap(Drawables.qrCode(item)) qr_code.setImageBitmap(item.qrCode())
} }
} }

View File

@ -27,9 +27,7 @@ import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.integration.android.IntentIntegrator
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
@ -45,10 +43,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = object : ArrayAdapter<BitmessageAddress>( adapter = object : ArrayAdapter<BitmessageAddress>(
activity, activity!!,
R.layout.subscription_row, R.layout.subscription_row,
R.id.name, R.id.name,
LinkedList()) { LinkedList()
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val result: View val result: View
val v: ViewHolder val v: ViewHolder
@ -72,7 +71,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
v.avatar.setImageDrawable(Identicon(item)) v.avatar.setImageDrawable(Identicon(item))
v.name.text = item.toString() v.name.text = item.toString()
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream) v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
v.subscribed.visibility = if (item.isSubscribed) View.VISIBLE else View.INVISIBLE v.subscribed.visibility =
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
} }
return result return result
} }
@ -84,10 +84,10 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onResume() super.onResume()
initFab(activity as MainActivity) initFab(activity as MainActivity)
updateList() reloadList()
} }
fun updateList() { override fun reloadList() {
adapter.clear() adapter.clear()
context?.let { context -> context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context) val addressRepo = Singleton.getAddressRepository(context)
@ -105,12 +105,13 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
val menu = FabSpeedDialMenu(activity) val menu = FabSpeedDialMenu(activity)
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code) 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) menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu) activity.initFab(R.drawable.ic_action_add_contact, menu)
.addOnMenuItemClickListener { _, _, itemId -> .addOnMenuItemClickListener { _, _, itemId ->
when (itemId) { when (itemId) {
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment) // FIXME
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) // 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
.initiateScan() // .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
// .initiateScan()
2 -> { 2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java) val intent = Intent(getActivity(), CreateAddressActivity::class.java)
startActivity(intent) startActivity(intent)
@ -121,7 +122,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_address_list, container, false) inflater.inflate(R.layout.fragment_address_list, container, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -133,7 +138,7 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
} }
} }
override fun updateList(label: Void) = updateList() override fun updateList(label: Void) = reloadList()
private data class ViewHolder( private data class ViewHolder(
val ctx: Context, val ctx: Context,

View File

@ -20,8 +20,8 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.support.v7.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
@ -98,7 +98,8 @@ class ComposeMessageActivity : AppCompatActivity() {
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals( val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
"RE:", "RE:",
ignoreCase = true ignoreCase = true
)) { )
) {
"" ""
} else { } else {
"RE: " "RE: "
@ -107,7 +108,7 @@ class ComposeMessageActivity : AppCompatActivity() {
} }
replyIntent.putExtra( replyIntent.putExtra(
EXTRA_CONTENT, EXTRA_CONTENT,
"\n\n------------------------------------------------------\n" + item.text!! "\n\n------------------------------------------------------\n${item.text ?: ""}"
) )
return replyIntent return replyIntent
} }

View File

@ -20,7 +20,7 @@ import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.view.* import android.view.*
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Toast import android.widget.Toast
@ -35,7 +35,7 @@ import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
import ch.dissem.apps.abit.adapter.ContactAdapter import ch.dissem.apps.abit.adapter.ContactAdapter
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
@ -76,7 +76,7 @@ class ComposeMessageFragment : Fragment() {
parents.addAll(draft.parents) parents.addAll(draft.parents)
} else { } else {
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && (id == null || id.privateKey == null)) { if (context != null && id?.privateKey == null) {
id = Singleton.getIdentity(context!!) id = Singleton.getIdentity(context!!)
} }
if (id?.privateKey != null) { if (id?.privateKey != null) {
@ -89,13 +89,12 @@ class ComposeMessageFragment : Fragment() {
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
} }
if (containsKey(EXTRA_SUBJECT)) { if (containsKey(EXTRA_SUBJECT)) {
subject = getString(EXTRA_SUBJECT) subject = getString(EXTRA_SUBJECT) ?: throw IllegalStateException("EXTRA_SUBJECT expected")
} }
if (containsKey(EXTRA_CONTENT)) { if (containsKey(EXTRA_CONTENT)) {
content = getString(EXTRA_CONTENT) content = getString(EXTRA_CONTENT) ?: throw IllegalStateException("EXTRA_CONTENT expected")
} }
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
Plaintext.Encoding.SIMPLE
if (containsKey(EXTRA_PARENT)) { if (containsKey(EXTRA_PARENT)) {
val parent = getSerializable(EXTRA_PARENT) as Plaintext val parent = getSerializable(EXTRA_PARENT) as Plaintext
@ -221,7 +220,7 @@ class ComposeMessageFragment : Fragment() {
} }
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
sender?.let { builder.from(it) } sender?.let { builder.from(it) }
if (!Preferences.requestAcknowledgements(ctx)) { if (!ctx.preferences.requestAcknowledgements) {
builder.preventAck() builder.preventAck()
} }
when (encoding) { when (encoding) {

View File

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

View File

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

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit
import android.app.Activity import android.app.Activity
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.util.Base64 import android.util.Base64
import android.util.Base64.URL_SAFE import android.util.Base64.URL_SAFE
import android.widget.Button import android.widget.Button

View File

@ -2,8 +2,8 @@ package ch.dissem.apps.abit
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.NavUtils import androidx.core.app.NavUtils
import android.support.v7.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem import android.view.MenuItem
import com.mikepenz.materialize.MaterializeBuilder import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.* import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*

View File

@ -18,9 +18,11 @@ package ch.dissem.apps.abit
import android.graphics.* import android.graphics.*
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import android.text.TextPaint import android.text.TextPaint
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import org.jetbrains.anko.collections.forEachWithIndex
import kotlin.math.sqrt
/** /**
* @author Christian Basler * @author Christian Basler
@ -45,8 +47,20 @@ class Identicon(input: BitmessageAddress) : Drawable() {
} }
} }
} }
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f)) private val color = Color.HSVToColor(
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f)) 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 { private val textPaint = TextPaint().apply {
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
color = 0xFF607D8B.toInt() color = 0xFF607D8B.toInt()
@ -54,20 +68,24 @@ class Identicon(input: BitmessageAddress) : Drawable() {
} }
override fun draw(canvas: Canvas) { 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 x: Float
var y: Float var y: Float
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
val cellWidth = width / SIZE.toFloat() val cellWidth = width / SIZE.toFloat()
val cellHeight = height / SIZE.toFloat() val cellHeight = height / SIZE.toFloat()
paint.color = background paint.color = background
canvas.drawCircle(width / 2, height / 2, width / 2, paint) canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
paint.color = color paint.color = color
for (row in 0 until SIZE) { for (row in 0 until SIZE) {
for (column in 0 until SIZE) { for (column in 0 until SIZE) {
if (fields[row][column]) { if (fields[row][column]) {
x = cellWidth * column x = offsetX + cellWidth * column
y = cellHeight * row y = offsetY + cellHeight * row
canvas.drawCircle( canvas.drawCircle(
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint paint
@ -77,7 +95,7 @@ class Identicon(input: BitmessageAddress) : Drawable() {
} }
if (isChan) { if (isChan) {
textPaint.textSize = 2 * cellHeight textPaint.textSize = 2 * cellHeight
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint) canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
} }
} }
@ -96,3 +114,68 @@ class Identicon(input: BitmessageAddress) : Drawable() {
private const val CENTER_COLUMN = 5 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
}

View File

@ -16,19 +16,18 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.app.Fragment
import android.os.Bundle import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button 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.adapter.AddressSelectorAdapter
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.wif.WifImporter import ch.dissem.bitmessage.wif.WifImporter
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import org.ini4j.InvalidFileFormatException import org.ini4j.InvalidFileFormatException
import org.jetbrains.anko.longToast import org.jetbrains.anko.longToast
@ -47,34 +46,31 @@ class ImportIdentitiesFragment : Fragment() {
inflater.inflate(R.layout.fragment_import_select_identities, container, false) inflater.inflate(R.layout.fragment_import_select_identities, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val ctx = activity ?: throw IllegalStateException("No activity available")
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val wifData = arguments.getString(WIF_DATA) val wifData = arguments?.getString(WIF_DATA) ?: throw IllegalStateException("No WIF data")
val bmc = Singleton.getBitmessageContext(activity) val bmc = Singleton.getBitmessageContext(ctx)
try { try {
importer = WifImporter(bmc, wifData) importer = WifImporter(bmc, wifData)
} catch (e: InvalidFileFormatException) { } catch (e: InvalidFileFormatException) {
longToast(R.string.invalid_wif_file) ctx.longToast(R.string.invalid_wif_file)
activity.finish() ctx.finish()
return return
} }
adapter = AddressSelectorAdapter(importer.getIdentities()) adapter = AddressSelectorAdapter(importer.getIdentities())
val layoutManager = LinearLayoutManager( val layoutManager = LinearLayoutManager(
activity, activity,
LinearLayoutManager.VERTICAL, RecyclerView.VERTICAL,
false false
) )
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view) val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addItemDecoration( recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.HORIZONTAL))
SimpleListDividerDecorator(
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true
)
)
view.findViewById<Button>(R.id.finish).setOnClickListener { view.findViewById<Button>(R.id.finish).setOnClickListener {
importer.importAll(adapter.selected) importer.importAll(adapter.selected)
@ -83,7 +79,7 @@ class ImportIdentitiesFragment : Fragment() {
addIdentityEntry(selected) addIdentityEntry(selected)
} }
} }
activity.finish() ctx.finish()
} }
} }

View File

@ -17,6 +17,7 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.transaction
/** /**
* @author Christian Basler * @author Christian Basler
@ -29,19 +30,20 @@ class ImportIdentityActivity : DetailActivity() {
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA) val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
if (wifData == null) { if (wifData == null) {
fragmentManager.beginTransaction() supportFragmentManager.transaction {
.replace(R.id.content, InputWifFragment()) replace(R.id.content, InputWifFragment())
.commit() }
} else { } else {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData) bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
val fragment = ImportIdentitiesFragment() val fragment = ImportIdentitiesFragment().apply {
fragment.arguments = bundle arguments = bundle
}
fragmentManager.beginTransaction() supportFragmentManager.transaction {
.replace(R.id.content, fragment) replace(R.id.content, fragment)
.commit() }
} }
} }

View File

@ -16,10 +16,11 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.app.Fragment
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.Toast 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.DialogConfigs
import com.github.angads25.filepicker.model.DialogProperties import com.github.angads25.filepicker.model.DialogProperties
import com.github.angads25.filepicker.view.FilePickerDialog import com.github.angads25.filepicker.view.FilePickerDialog
@ -42,7 +43,7 @@ class InputWifFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_import_input, container, false) inflater.inflate(R.layout.fragment_import_input, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
next.setOnClickListener { next.setOnClickListener {
val bundle = Bundle() val bundle = Bundle()
@ -52,9 +53,9 @@ class InputWifFragment : Fragment() {
arguments = bundle arguments = bundle
} }
fragmentManager.beginTransaction() fragmentManager?.transaction {
.replace(R.id.content, fragment) replace(R.id.content, fragment)
.commit() }
} }
} }

View File

@ -20,6 +20,8 @@ package ch.dissem.apps.abit
* @author Christian Basler * @author Christian Basler
*/ */
interface ListHolder<in L> { interface ListHolder<in L> {
fun reloadList()
fun updateList(label: L) fun updateList(label: L)
fun setActivateOnItemClick(activateOnItemClick: Boolean) fun setActivateOnItemClick(activateOnItemClick: Boolean)

View File

@ -17,29 +17,31 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.content.Intent import android.content.Intent
import android.graphics.Point import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View import android.view.View
import android.view.ViewGroup import androidx.annotation.DrawableRes
import android.widget.RelativeLayout 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.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE 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
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter import ch.dissem.apps.abit.util.getColor
import ch.dissem.apps.abit.util.Labels import ch.dissem.apps.abit.util.getIcon
import ch.dissem.apps.abit.util.NetworkUtils import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.github.amlcurran.showcaseview.ShowcaseView
import com.mikepenz.community_material_typeface_library.CommunityMaterial import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
@ -52,9 +54,14 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Nameable import com.mikepenz.materialdrawer.model.interfaces.Nameable
import io.github.kobakei.materialfabspeeddial.FabSpeedDial 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 kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread 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.io.Serializable
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@ -90,7 +97,10 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
var hasDetailPane: Boolean = false var hasDetailPane: Boolean = false
private set private set
private var subscription: Disposable? = null
private lateinit var bmc: BitmessageContext private lateinit var bmc: BitmessageContext
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var accountHeader: AccountHeader private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer private lateinit var drawer: Drawer
@ -103,6 +113,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = WeakReference(this) instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this) bmc = Singleton.getBitmessageContext(this)
messageRepo = Singleton.getMessageRepository(this)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
fab.hide() fab.hide()
@ -110,7 +121,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
val listFragment = MessageListFragment() val listFragment = ConversationListFragment()
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.item_list, listFragment) .replace(R.id.item_list, listFragment)
@ -140,39 +151,34 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
ComposeMessageActivity.launchReplyTo(this, item) ComposeMessageActivity.launchReplyTo(this, item)
} }
if (Preferences.useTrustedNode(this)) {
SyncAdapter.startSync(this)
} else {
SyncAdapter.stopSync(this)
}
if (drawer.isDrawerOpen) { if (drawer.isDrawerOpen) {
val lps = RelativeLayout.LayoutParams( MaterialShowcaseView.Builder(this)
ViewGroup .setMaskColour(R.color.colorPrimary)
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT .setTitleText(R.string.full_node)
).apply { .setContentText(R.string.full_node_description)
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) .setDismissOnTouch(true)
addRule(RelativeLayout.ALIGN_PARENT_LEFT) .setDismissText(R.string.got_it)
val margin = ((resources.displayMetrics.density * 12) as Number).toInt() .setShape(object : Shape {
setMargins(margin, margin, margin, margin) var w = 0
var h = 0
override fun updateTarget(target: Target) {
w = target.bounds.width()
h = target.bounds.height()
} }
ShowcaseView.Builder(this) override fun getHeight() = h
.withMaterialShowcase()
.setStyle(R.style.CustomShowcaseTheme) override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
.setContentTitle(R.string.full_node) val r = h.toFloat() / 2
.setContentText(R.string.full_node_description) canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
.setTarget {
val view = drawer.stickyFooter
val location = IntArray(2)
view.getLocationInWindow(location)
val x = location[0] + 7 * view.width / 8
val y = location[1] + view.height / 2
Point(x, y)
} }
.replaceEndButton(R.layout.showcase_button)
.hideOnTouchOutside() override fun getWidth() = w
.build() })
.setButtonPosition(lps) .setTarget(drawer.stickyFooter)
.setDelay(1000)
.show()
} }
} }
@ -252,14 +258,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
nodeSwitch = SwitchDrawerItem() nodeSwitch = SwitchDrawerItem()
.withIdentifier(ID_NODE_SWITCH) .withIdentifier(ID_NODE_SWITCH)
.withName(R.string.full_node) .withName(R.string.online)
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline) .withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
.withChecked(Preferences.isFullNodeActive(this)) .withChecked(preferences.online)
.withOnCheckedChangeListener { _, _, isChecked -> .withOnCheckedChangeListener { _, _, isChecked ->
preferences.online = isChecked
if (isChecked) { if (isChecked) {
NetworkUtils.enableNode(this@MainActivity) network.enableNode(true)
} else {
NetworkUtils.disableNode(this@MainActivity)
} }
} }
@ -296,15 +301,16 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
uiThread { uiThread {
if (intent.hasExtra(EXTRA_SHOW_LABEL)) { if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label currentLabel.onNext(intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label)
} else if (currentLabel.value == null) { } else if (currentLabel.value == null) {
currentLabel.value = labels[0] currentLabel.onNext(labels[0])
} }
for (label in labels) { for (label in labels) {
addLabelEntry(label) addLabelEntry(label)
} }
currentLabel.value?.let { currentLabel.value?.let { label ->
drawer.setSelection(it.id as Long) drawer.setSelection(label.id as Long)
} }
updateUnread() updateUnread()
} }
@ -323,27 +329,32 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val itemList = supportFragmentManager.findFragmentById(R.id.item_list) val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
val tag = item.tag val tag = item.tag
if (tag is Label) { if (tag is Label) {
currentLabel.value = tag currentLabel.onNext(tag)
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
if (itemList !is ConversationListFragment) {
changeList(ConversationListFragment())
}
} else {
if (itemList !is MessageListFragment) { if (itemList !is MessageListFragment) {
changeList(MessageListFragment()) changeList(MessageListFragment())
} }
}
return false return false
} else if (item is Nameable<*>) { } else if (item is Nameable<*>) {
when (item.name.textRes) { when (item.name.textRes) {
R.string.contacts_and_subscriptions -> { R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) { if (itemList is AddressListFragment) {
itemList.updateList() itemList.reloadList()
} else { } else {
changeList(AddressListFragment()) changeList(AddressListFragment())
} }
return false return false
} }
R.string.settings -> { R.string.settings -> {
supportFragmentManager supportFragmentManager?.transaction {
.beginTransaction() replace(R.id.item_list, SettingsFragment())
.replace(R.id.item_list, SettingsFragment()) addToBackStack(null)
.addToBackStack(null) }
.commit()
return false return false
} }
R.string.full_node -> return true R.string.full_node -> return true
@ -355,14 +366,12 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
} }
override fun onResume() { override fun onResume() {
network.enableNode(false)
updateUnread() updateUnread()
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
NetworkUtils.enableNode(this, false)
}
Singleton.getMessageListener(this).resetNotification() Singleton.getMessageListener(this).resetNotification()
currentLabel.addObserver(this) { label -> subscription = currentLabel.subscribe { label ->
if (label != null && label.id is Long) { if (label.id is Long) {
drawer.setSelection(label.id as Long) drawer.setSelection(label.id as Long, false)
} }
} }
active = true active = true
@ -370,7 +379,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
} }
override fun onPause() { override fun onPause() {
currentLabel.removeObserver(this) subscription?.dispose()
super.onPause() super.onPause()
active = false active = false
} }
@ -398,8 +407,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.withIdentifier(label.id as Long) .withIdentifier(label.id as Long)
.withName(label.toString()) .withName(label.toString())
.withTag(label) .withTag(label)
.withIcon(Labels.getIcon(label)) .withIcon(label.getIcon())
.withIconColor(Labels.getColor(label)) .withIconColor(label.getColor(0xFF000000.toInt()))
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3) drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
} }
@ -432,17 +441,19 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
if (item.tag is Label) { if (item.tag is Label) {
val label = item.tag as Label val label = item.tag as Label
if (label !== LABEL_ARCHIVE) { if (label !== LABEL_ARCHIVE) {
val unread = bmc.messages.countUnread(label) val unread = messageRepo.countUnread(label, preferences.separateIdentities)
if (unread > 0) { if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString()) (item as PrimaryDrawerItem).withBadge(unread.toString())
} else { } else {
(item as PrimaryDrawerItem).withBadge(null as String?) (item as PrimaryDrawerItem).withBadge(null as String?)
} }
runOnUiThread {
drawer.updateItem(item) drawer.updateItem(item)
} }
} }
} }
} }
}
/** /**
* Callback method from [ListSelectionListener] * Callback method from [ListSelectionListener]
@ -454,6 +465,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// adding or replacing the detail fragment using a // adding or replacing the detail fragment using a
// fragment transaction. // fragment transaction.
val fragment = when (item) { val fragment = when (item) {
is Conversation -> {
ConversationDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
}
is Plaintext -> { is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) { if (item.labels.any { it.type == Label.Type.DRAFT }) {
ComposeMessageFragment().apply { ComposeMessageFragment().apply {
@ -485,6 +503,11 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// In single-pane mode, simply start the detail activity // In single-pane mode, simply start the detail activity
// for the selected item ID. // for the selected item ID.
val detailIntent = when (item) { val detailIntent = when (item) {
is Conversation -> {
Intent(this, MessageDetailActivity::class.java).apply {
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
is Plaintext -> { is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) { if (item.labels.any { it.type == Label.Type.DRAFT }) {
Intent(this, ComposeMessageActivity::class.java).apply { Intent(this, ComposeMessageActivity::class.java).apply {
@ -518,6 +541,26 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
supportActionBar?.title = title 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 { companion object {
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage" const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel" const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
@ -531,15 +574,6 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private var instance: WeakReference<MainActivity>? = null private var instance: WeakReference<MainActivity>? = null
fun updateNodeSwitch() {
apply {
runOnUiThread {
nodeSwitch.withChecked(Preferences.isFullNodeActive(this))
drawer.updateStickyFooterItem(nodeSwitch)
}
}
}
/** /**
* Runs the given code in the main activity context, if it currently exists. Otherwise, * Runs the given code in the main activity context, if it currently exists. Otherwise,
* it's ignored. * it's ignored.

View File

@ -2,8 +2,9 @@ package ch.dissem.apps.abit
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.NavUtils import androidx.core.app.NavUtils
import android.view.MenuItem import android.view.MenuItem
import ch.dissem.bitmessage.entity.Plaintext
/** /**
@ -33,9 +34,15 @@ class MessageDetailActivity : DetailActivity() {
// Create the detail fragment and add it to the activity // Create the detail fragment and add it to the activity
// using a fragment transaction. // using a fragment transaction.
val arguments = Bundle() val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)) arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
val fragment = MessageDetailFragment() 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 fragment.arguments = arguments
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.content, fragment) .add(R.id.content, fragment)

View File

@ -19,27 +19,27 @@ package ch.dissem.apps.abit
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.annotation.IdRes import androidx.annotation.IdRes
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.support.v7.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.util.Linkify import android.text.util.Linkify
import android.text.util.Linkify.WEB_URLS import android.text.util.Linkify.WEB_URLS
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import ch.dissem.apps.abit.adapter.LabelAdapter
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN 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.Constants.BITMESSAGE_URL_SCHEMA
import ch.dissem.apps.abit.util.Drawables import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract 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.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.view.IconicsImageView
import kotlinx.android.synthetic.main.fragment_message_detail.* import kotlinx.android.synthetic.main.fragment_message_detail.*
import java.util.* import java.util.*
@ -85,8 +85,8 @@ class MessageDetailFragment : Fragment() {
// Show the dummy content as text in a TextView. // Show the dummy content as text in a TextView.
item?.let { item -> item?.let { item ->
subject.text = item.subject subject.text = item.subject
status.setImageResource(Assets.getStatusDrawable(item.status)) status.setImageResource(item.status.getDrawable())
status.contentDescription = getString(Assets.getStatusString(item.status)) status.contentDescription = getString(item.status.getString())
avatar.setImageDrawable(Identicon(item.from)) avatar.setImageDrawable(Identicon(item.from))
val senderClickListener: (View) -> Unit = { val senderClickListener: (View) -> Unit = {
MainActivity.apply { MainActivity.apply {
@ -229,7 +229,7 @@ class MessageDetailFragment : Fragment() {
val message = messages[position] val message = messages[position]
viewHolder.avatar.setImageDrawable(Identicon(message.from)) viewHolder.avatar.setImageDrawable(Identicon(message.from))
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status)) viewHolder.status.setImageResource(message.status.getDrawable())
viewHolder.sender.text = message.from.toString() viewHolder.sender.text = message.from.toString()
viewHolder.extract.text = prepareMessageExtract(message.text) viewHolder.extract.text = prepareMessageExtract(message.text)
viewHolder.item = message viewHolder.item = message
@ -259,40 +259,6 @@ class MessageDetailFragment : Fragment() {
} }
} }
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) :
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
private val labels = labels.toMutableList()
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?.color(Labels.getColor(label))
viewHolder.icon.icon?.icon(Labels.getIcon(label))
viewHolder.label.text = Labels.getText(label, ctx)
}
override fun getItemCount() = labels.size
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
var label = itemView.findViewById<TextView>(R.id.label)!!
}
}
companion object { companion object {
/** /**
* The fragment argument representing the item ID that this fragment * The fragment argument representing the item ID that this fragment

View File

@ -19,33 +19,28 @@ package ch.dissem.apps.abit
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.support.v4.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper
import android.support.v7.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.support.v7.widget.RecyclerView.OnScrollListener import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY 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.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.FabUtils import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_message_list.* import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.*
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import java.util.* import java.util.*
private const val PAGE_SIZE = 15 private const val PAGE_SIZE = 15
@ -65,22 +60,22 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
private var isLoading = false private var isLoading = false
private var isLastPage = false private var isLastPage = false
private var subscription: Disposable? = null
private var layoutManager: LinearLayoutManager? = null private var layoutManager: LinearLayoutManager? = null
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() { private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
layoutManager?.let { layoutManager -> layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) { if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5 if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - PAGE_SIZE
&& firstVisibleItemPosition >= 0) { && firstVisibleItemPosition >= 0
) {
loadMoreItems() loadMoreItems()
} }
} }
@ -89,17 +84,29 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
private var emptyTrashMenuItem: MenuItem? = null private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false private var activateOnItemClick: Boolean = false
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableMessageAdapter?.activateOnItemClick = activateOnItemClick
this.activateOnItemClick = activateOnItemClick
}
private val backStack = Stack<Label>() private val backStack = Stack<Label>()
fun loadMoreItems() { fun loadMoreItems() {
isLoading = true isLoading = true
swipeableMessageAdapter?.let { messageAdapter -> swipeableMessageAdapter?.let { messageAdapter ->
doAsync { doAsync {
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE) val label = currentLabel.value
onUiThread { val messages = messageRepo.findMessages(
label,
messageAdapter.itemCount,
PAGE_SIZE,
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
)
uiThread {
messageAdapter.addAll(messages) messageAdapter.addAll(messages)
isLoading = false isLoading = false
isLastPage = messages.size < PAGE_SIZE isLastPage = messages.size < PAGE_SIZE
@ -120,16 +127,21 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
initFab(activity) initFab(activity)
messageRepo = Singleton.getMessageRepository(activity) messageRepo = Singleton.getMessageRepository(activity)
currentLabel.addObserver(this) { new -> doUpdateList(new) } subscription = currentLabel.subscribe { new -> doUpdateList(new) }
doUpdateList(currentLabel.value) doUpdateList(currentLabel.value)
} }
override fun onPause() { override fun onPause() {
currentLabel.removeObserver(this) subscription?.dispose()
super.onPause() super.onPause()
} }
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) { 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 val mainActivity = activity as? MainActivity
swipeableMessageAdapter?.clear(label) swipeableMessageAdapter?.clear(label)
if (label == null) { if (label == null) {
@ -137,8 +149,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
swipeableMessageAdapter?.notifyDataSetChanged() swipeableMessageAdapter?.notifyDataSetChanged()
return return
} }
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH menuItem.isVisible = label.type == Label.Type.TRASH
mainActivity?.apply { MainActivity.apply {
if ("archive" == label.toString()) { if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive)) updateTitle(getString(R.string.archive))
} else { } else {
@ -148,8 +160,14 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems() loadMoreItems()
} }
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_list, container, false) inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -157,90 +175,51 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
val context = context ?: throw IllegalStateException("No context available") val context = context ?: throw IllegalStateException("No context available")
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
setInterceptVerticalScrollingWhileAnimationRunning(true)
isEnabled = true
}
// swipe manager
val swipeManager = RecyclerViewSwipeManager()
//swipeableMessageAdapter
val adapter = SwipeableMessageAdapter().apply {
setActivateOnItemClick(activateOnItemClick)
}
adapter.eventListener = object : SwipeableMessageAdapter.EventListener {
override fun onItemDeleted(item: Plaintext) {
if (MessageDetailFragment.isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
}
override fun onItemArchived(item: Plaintext) {
Singleton.labeler.archive(item)
}
override fun onItemViewClicked(v: View?) {
val position = recycler_view.getChildAdapterPosition(v)
adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) {
val item = adapter.getItem(position)
(activity as MainActivity).onItemSelected(item)
}
}
}
// wrap for swiping
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
val animator = SwipeDismissItemAnimator()
// Change animations are enabled by default since support-v7-recyclerview v22.
// Disable the change animation in order to make turning back animation of swiped item
// works properly.
animator.supportsChangeAnimations = false
recycler_view.layoutManager = layoutManager recycler_view.layoutManager = layoutManager
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableMessageAdapter swipeableMessageAdapter = SwipeableMessageAdapter(context).apply {
recycler_view.itemAnimator = animator activateOnItemClick = this@MessageListFragment.activateOnItemClick
}
recycler_view.adapter = swipeableMessageAdapter // requires *wrapped* swipeableMessageAdapter
recycler_view.addOnScrollListener(recyclerViewOnScrollListener) recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(SimpleListDividerDecorator( val dirs = when (currentLabel.value?.type) {
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true)) Label.Type.TRASH -> ItemTouchHelper.LEFT
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
}
// NOTE: val swipeHandler = SwipeToDeleteCallback(context, dirs, object : EventListener {
// The initialization order is very important! This order determines the priority of override fun onItemDeleted(position: Int) {
// touch event handling. context.toast("Deleted")
// }
// priority: TouchActionGuard > Swipe > DragAndDrop
touchActionGuardManager.attachRecyclerView(recycler_view)
swipeManager.attachRecyclerView(recycler_view)
recyclerViewTouchActionGuardManager = touchActionGuardManager override fun onItemArchived(position: Int) {
recyclerViewSwipeManager = swipeManager context.toast("Archived")
this.swipeableMessageAdapter = adapter }
Singleton.updateMessageListAdapterInListener(adapter) 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) { private fun initFab(context: MainActivity) {
val menu = FabSpeedDialMenu(context) val menu = FabSpeedDialMenu(context)
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast) menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal) menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu) context.initFab(R.drawable.ic_action_compose_message, menu)
.addOnMenuItemClickListener { _, _, itemId -> .addOnMenuItemClickListener { _, _, itemId ->
val identity = Singleton.getIdentity(context) val identity = Singleton.getIdentity(context)
if (identity == null) { if (identity == null) {
Toast.makeText(activity, R.string.no_identity_warning, Toast.makeText(
Toast.LENGTH_LONG).show() activity, R.string.no_identity_warning,
Toast.LENGTH_LONG
).show()
} else { } else {
when (itemId) { when (itemId) {
1 -> { 1 -> {
@ -262,18 +241,6 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
override fun onDestroyView() { override fun onDestroyView() {
recyclerViewSwipeManager?.release()
recyclerViewSwipeManager = null
recyclerViewTouchActionGuardManager?.release()
recyclerViewTouchActionGuardManager = null
recycler_view.itemAnimator = null
recycler_view.adapter = null
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
wrappedAdapter = null
swipeableMessageAdapter = null swipeableMessageAdapter = null
layoutManager = null layoutManager = null
@ -283,6 +250,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu) inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash) emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
// currentLabel.value?.let { doUpdateList(it) }
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
@ -292,12 +261,22 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label -> currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true if (label.type != Label.Type.TRASH) return true
doAsync { deleteAllMessages(label)
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
} }
return true
uiThread { doUpdateList(label) } }
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 return true
@ -306,19 +285,24 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
} }
override fun updateList(label: Label) { private fun deleteAllMessages(label: Label) {
currentLabel.value = label doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
} }
override fun setActivateOnItemClick(activateOnItemClick: Boolean) { uiThread { doUpdateList(label) }
swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick) }
this.activateOnItemClick = activateOnItemClick }
override fun updateList(label: Label) {
currentLabel.onNext(label)
} }
override fun showPreviousList() = if (backStack.isEmpty()) { override fun showPreviousList() = if (backStack.isEmpty()) {
false false
} else { } else {
currentLabel.value = backStack.pop() currentLabel.onNext(backStack.pop())
true true
} }
} }

View File

@ -17,42 +17,57 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.app.Activity import android.app.Activity
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.ServiceConnection
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.os.IBinder
import android.support.v4.content.FileProvider.getUriForFile
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.widget.Toast 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.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Exports import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.Preferences 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.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.indeterminateProgressDialog import org.jetbrains.anko.indeterminateProgressDialog
import org.jetbrains.anko.support.v4.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import java.util.*
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences) setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference("about")?.onPreferenceClickListener = aboutClickListener() findPreference("about")?.onPreferenceClickListener = aboutClickListener()
val cleanup = findPreference("cleanup") findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
findPreference("export")?.onPreferenceClickListener = exportClickListener() findPreference("export")?.onPreferenceClickListener = exportClickListener()
findPreference("import")?.onPreferenceClickListener = importClickListener() findPreference("import")?.onPreferenceClickListener = importClickListener()
findPreference("status").onPreferenceClickListener = statusClickListener() 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 { private fun aboutClickListener() = Preference.OnPreferenceClickListener {
@ -73,7 +88,8 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener { private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available") val ctx = activity?.applicationContext
?: throw IllegalStateException("Context not available")
cleanup.isEnabled = false cleanup.isEnabled = false
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
@ -81,7 +97,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
val bmc = Singleton.getBitmessageContext(ctx) val bmc = Singleton.getBitmessageContext(ctx)
bmc.internals.nodeRegistry.clear() bmc.internals.nodeRegistry.clear()
bmc.cleanup() bmc.cleanup()
Preferences.cleanupExportDirectory(ctx) ctx.preferences.cleanupExportDirectory()
uiThread { uiThread {
Toast.makeText( Toast.makeText(
@ -96,11 +112,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
private fun exportClickListener() = Preference.OnPreferenceClickListener { private fun exportClickListener() = Preference.OnPreferenceClickListener {
val ctx = context ?: throw IllegalStateException("No context available") val ctx = activity ?: throw IllegalStateException("No context available")
indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply { ctx.indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
doAsync { doAsync {
val exportDirectory = Preferences.getExportDirectory(ctx) val exportDirectory = ctx.preferences.exportDirectory
exportDirectory.mkdirs() exportDirectory.mkdirs()
val file = Exports.exportData(exportDirectory, ctx) val file = Exports.exportData(exportDirectory, ctx)
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file) val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
@ -131,20 +147,20 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
if (activity.hasDetailPane) { if (activity.hasDetailPane) {
activity.setDetailView(StatusFragment()) activity.setDetailView(StatusFragment())
} else { } else {
startActivity<StatusActivity>() activity.startActivity<StatusActivity>()
} }
return@OnPreferenceClickListener true return@OnPreferenceClickListener true
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val ctx = context ?: throw IllegalStateException("No context available") val ctx = activity ?: throw IllegalStateException("No context available")
when (requestCode) { when (requestCode) {
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx) WRITE_EXPORT_REQUEST_CODE -> ctx.preferences.cleanupExportDirectory()
READ_IMPORT_REQUEST_CODE -> { READ_IMPORT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK && data?.data != null) { if (resultCode == Activity.RESULT_OK && data?.data != null) {
indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply { ctx.indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
doAsync { doAsync {
Exports.importData(data.data, ctx) Exports.importData(data.data!!, ctx)
uiThread { uiThread {
dismiss() dismiss()
} }
@ -157,42 +173,91 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
override fun onAttach(ctx: Context?) { override fun onAttach(ctx: Context?) {
super.onAttach(ctx) super.onAttach(ctx)
(ctx as? MainActivity)?.floatingActionButton?.hide() ctx?.let {
PreferenceManager.getDefaultSharedPreferences(ctx) if (it is MainActivity) {
.registerOnSharedPreferenceChangeListener(this) it.floatingActionButton?.hide()
it.updateTitle(getString(R.string.settings))
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
} }
} }
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) { private val connection = object : ServiceConnection {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) override fun onServiceConnected(name: ComponentName, service: IBinder) {
val ctx = context ?: throw IllegalStateException("No context available") if (service is BatchProcessorService.BatchBinder) {
if (node != null) { val messageRepo = Singleton.getMessageRepository(service.service)
SyncAdapter.startSync(ctx) val conversationService = Singleton.getConversationService(service.service)
} else {
SyncAdapter.stopSync(ctx) 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
)
)
} }
} }
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) { override fun onServiceDisconnected(name: ComponentName) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
if (node != null) {
val ctx = context ?: throw IllegalStateException("No context available")
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
SyncAdapter.startPowSync(ctx)
} else {
SyncAdapter.stopPowSync(ctx)
} }
} }
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 { companion object {
const val WRITE_EXPORT_REQUEST_CODE = 1 const val WRITE_EXPORT_REQUEST_CODE = 1
const val READ_IMPORT_REQUEST_CODE = 2 const val READ_IMPORT_REQUEST_CODE = 2

View File

@ -17,7 +17,7 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import com.mikepenz.materialize.MaterializeBuilder import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.activity_status.* import kotlinx.android.synthetic.main.activity_status.*

View File

@ -17,7 +17,7 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View File

@ -16,7 +16,7 @@
package ch.dissem.apps.abit.adapter package ch.dissem.apps.abit.adapter
import android.support.v7.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -31,7 +31,7 @@ import java.util.*
*/ */
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() { class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
private val data = identities.map { Selectable(it) }.toMutableList() private val data = identities.asSequence().map { Selectable(it) }.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
@ -63,7 +63,7 @@ class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView
val selected: List<BitmessageAddress> val selected: List<BitmessageAddress>
get() { get() {
return data return data.asSequence()
.filter { it.selected } .filter { it.selected }
.mapTo(LinkedList()) { it.data } .mapTo(LinkedList()) { it.data }
} }

View File

@ -1,29 +0,0 @@
/*
* 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 ch.dissem.apps.abit.util.PRNGFixes
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
/**
* @author Christian Basler
*/
class AndroidCryptography : SpongyCryptography() {
init {
PRNGFixes.apply()
}
}

View File

@ -142,5 +142,4 @@ class ContactAdapter(
} }
} }
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
} }

View File

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

View File

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

View File

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

View File

@ -1,250 +0,0 @@
/*
* 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.annotation.SuppressLint
import android.graphics.Typeface
import android.support.v7.widget.RecyclerView
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 ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
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)
*/
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Plaintext>()
var eventListener: EventListener? = null
private val itemViewOnClickListener: View.OnClickListener
private val swipeableViewContainerOnClickListener: View.OnClickListener
private var label: Label? = null
private var selectedPosition = -1
private var activateOnItemClick: Boolean = false
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
}
interface EventListener {
fun onItemDeleted(item: Plaintext)
fun onItemArchived(item: Plaintext)
fun onItemViewClicked(v: View?)
}
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
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)!!
override fun getSwipeableContainerView() = container
}
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
fun add(item: Plaintext) {
data.add(item)
notifyDataSetChanged()
}
fun addFirst(item: Plaintext) {
val index = data.size
data.addFirst(item)
notifyItemInserted(index)
}
fun addAll(items: Collection<Plaintext>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: Plaintext) {
val index = data.indexOf(item)
data.removeAll { it.id == item.id }
notifyItemRemoved(index)
}
fun update(item: Plaintext) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
private fun onItemViewClick(v: View) {
eventListener?.onItemViewClicked(v)
}
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
}
fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = data[position].id as Long
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)
}
override fun onBindViewHolder(holder: ViewHolder, 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
)
}
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
itemView.setOnClickListener(itemViewOnClickListener)
// (if the item is *not pinned*, click event comes to the container)
container.setOnClickListener(swipeableViewContainerOnClickListener)
// set data
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(Assets.getStatusDrawable(item.status))
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
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
}
}
}
override fun getItemCount() = data.size
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
} else {
REACTION_CAN_SWIPE_BOTH_H
}
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
})
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
when (result) {
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
else -> null
}
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
fun setSelectedPosition(selectedPosition: Int) {
val oldPosition = this.selectedPosition
this.selectedPosition = selectedPosition
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemDeleted(item)
}
override fun onCleanUp() {
adapter = null
}
}
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemArchived(item)
}
override fun onCleanUp() {
adapter = null
}
}
}

View File

@ -1,48 +0,0 @@
/*
* 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.preference.PreferenceManager
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
/**
* Switches between two [ProofOfWorkEngine]s depending on the configuration.
*
* @author Christian Basler
*/
class SwitchingProofOfWorkEngine(
private val ctx: Context,
private val preference: String,
private val option: ProofOfWorkEngine,
private val fallback: ProofOfWorkEngine
) : ProofOfWorkEngine, InternalContext.ContextHolder {
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
if (preferences.getBoolean(preference, false)) {
option.calculateNonce(initialHash, target, callback)
} else {
fallback.calculateNonce(initialHash, target, callback)
}
}
override fun setContext(context: InternalContext) = listOf(option, fallback)
.filterIsInstance<InternalContext.ContextHolder>()
.forEach { it.setContext(context) }
}

View File

@ -19,12 +19,12 @@ package ch.dissem.apps.abit.dialog
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDialogFragment
import ch.dissem.apps.abit.ImportIdentityActivity import ch.dissem.apps.abit.ImportIdentityActivity
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
@ -33,7 +33,7 @@ import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.payload.Pubkey import ch.dissem.bitmessage.entity.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_identity.* import kotlinx.android.synthetic.main.dialog_add_identity.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
/** /**
@ -77,7 +77,7 @@ class AddIdentityDialogFragment : AppCompatDialogFragment() {
} }
} }
} }
R.id.import_identity -> startActivity<ImportIdentityActivity>() R.id.import_identity -> ctx.startActivity<ImportIdentityActivity>()
R.id.add_chan -> addChanDialog() R.id.add_chan -> addChanDialog()
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog") R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
else -> return@OnClickListener else -> return@OnClickListener

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit.dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View File

@ -17,11 +17,10 @@
package ch.dissem.apps.abit.dialog package ch.dissem.apps.abit.dialog
import android.app.Activity import android.app.Activity
import android.os.Build
import android.os.Bundle import android.os.Bundle
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.NetworkUtils import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.preferences
import kotlinx.android.synthetic.main.dialog_full_node.* import kotlinx.android.synthetic.main.dialog_full_node.*
/** /**
@ -32,14 +31,12 @@ class FullNodeDialogActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_full_node) setContentView(R.layout.dialog_full_node)
ok.setOnClickListener { ok.setOnClickListener {
Preferences.setWifiOnly(this@FullNodeDialogActivity, false) preferences.wifiOnly = false
NetworkUtils.enableNode(applicationContext) network.scheduleNodeStart()
finish() finish()
} }
dismiss.setOnClickListener { dismiss.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { network.scheduleNodeStart()
NetworkUtils.scheduleNodeStart(applicationContext)
}
finish() finish()
} }
} }

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.dialog
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -49,7 +49,7 @@ class SelectEncodingDialogFragment : AppCompatDialogFragment() {
when (encoding) { when (encoding) {
SIMPLE -> radioGroup.check(R.id.simple) SIMPLE -> radioGroup.check(R.id.simple)
EXTENDED -> radioGroup.check(R.id.extended) EXTENDED -> radioGroup.check(R.id.extended)
else -> LOG.warn("Unexpected encoding: " + encoding) else -> LOG.warn("Unexpected encoding: $encoding")
} }
ok.setOnClickListener(View.OnClickListener { ok.setOnClickListener(View.OnClickListener {
encoding = when (radioGroup.checkedRadioButtonId) { encoding = when (radioGroup.checkedRadioButtonId) {

View File

@ -10,7 +10,7 @@ import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables import ch.dissem.apps.abit.util.qrCode
import com.mikepenz.materialdrawer.AccountHeader import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.interfaces.IProfile import com.mikepenz.materialdrawer.model.interfaces.IProfile
@ -23,7 +23,7 @@ class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHe
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val imageView = ImageView(ctx) val imageView = ImageView(ctx)
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx))) imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
imageView.setOnClickListener { dialog.dismiss() } imageView.setOnClickListener { dialog.dismiss() }
dialog.addContentView( dialog.addContentView(
imageView, imageView,

View File

@ -2,23 +2,17 @@ package ch.dissem.apps.abit.drawer
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.support.v4.app.FragmentManager import androidx.fragment.app.FragmentManager
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import com.mikepenz.materialdrawer.AccountHeader import ch.dissem.apps.abit.*
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import ch.dissem.apps.abit.AddressDetailActivity
import ch.dissem.apps.abit.AddressDetailFragment
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import com.mikepenz.materialdrawer.AccountHeader
import android.widget.Toast.LENGTH_LONG import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
class ProfileSelectionListener( class ProfileSelectionListener(
private val ctx: Context, private val ctx: Context,
@ -42,6 +36,13 @@ class ProfileSelectionListener(
val tag = profile.tag val tag = profile.tag
if (tag is BitmessageAddress) { if (tag is BitmessageAddress) {
Singleton.setIdentity(tag) Singleton.setIdentity(tag)
MainActivity.apply {
updateUnread()
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
if (itemList is ListHolder<*>) {
itemList.reloadList()
}
}
} }
} }
} }

View File

@ -19,8 +19,11 @@ package ch.dissem.apps.abit.listener
import android.content.Context import android.content.Context
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.notification.NewMessageNotification import ch.dissem.apps.abit.notification.NewMessageNotification
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.ConversationService
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -33,14 +36,26 @@ import java.util.concurrent.Executors
* notifications should be combined. * notifications should be combined.
* *
*/ */
class MessageListener(ctx: Context) : BitmessageContext.Listener { class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
override fun setContext(ctx: BitmessageContext) {
messageRepo = ctx.messages
conversationService = ConversationService(messageRepo)
}
private val unacknowledged = LinkedList<Plaintext>() private val unacknowledged = LinkedList<Plaintext>()
private var numberOfUnacknowledgedMessages = 0 private var numberOfUnacknowledgedMessages = 0
private val notification = NewMessageNotification(ctx) private val notification = NewMessageNotification(ctx)
private val pool = Executors.newSingleThreadExecutor() 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) { override fun receive(plaintext: Plaintext) {
pool.submit { pool.submit {
updateConversation(plaintext)
unacknowledged.addFirst(plaintext) unacknowledged.addFirst(plaintext)
numberOfUnacknowledgedMessages++ numberOfUnacknowledgedMessages++
if (unacknowledged.size > 5) { if (unacknowledged.size > 5) {
@ -65,4 +80,17 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener {
numberOfUnacknowledgedMessages = 0 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
}
} }

View File

@ -1,36 +0,0 @@
/*
* 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
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import org.jetbrains.anko.connectivityManager
class WifiReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
val bmc = Singleton.getBitmessageContext(ctx)
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
NetworkUtils.doStartBitmessageService(ctx)
}
}
}
}

View File

@ -21,8 +21,8 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.support.annotation.ColorRes import androidx.annotation.ColorRes
import android.support.v4.content.ContextCompat import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import org.jetbrains.anko.notificationManager import org.jetbrains.anko.notificationManager

View File

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

View File

@ -17,8 +17,8 @@
package ch.dissem.apps.abit.notification package ch.dissem.apps.abit.notification
import android.content.Context import android.content.Context
import android.support.annotation.StringRes import androidx.annotation.StringRes
import android.support.v4.app.NotificationCompat import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R

View File

@ -21,11 +21,11 @@ import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.support.v4.app.NotificationCompat import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.apps.abit.service.BitmessageService import ch.dissem.apps.abit.service.NodeStartupService
import java.util.* import java.util.*
import kotlin.concurrent.fixedRateTimer import kotlin.concurrent.fixedRateTimer
@ -42,7 +42,7 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
val showAppIntent = Intent(ctx, MainActivity::class.java) val showAppIntent = Intent(ctx, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0) val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
builder builder
.setSmallIcon(R.drawable.ic_notification_full_node) .setSmallIcon(R.drawable.ic_notification_full_node_connecting)
.setContentTitle(ctx.getString(R.string.bitmessage_full_node)) .setContentTitle(ctx.getString(R.string.bitmessage_full_node))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setShowWhen(false) .setShowWhen(false)
@ -51,14 +51,17 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
@SuppressLint("StringFormatMatches") @SuppressLint("StringFormatMatches")
private fun update(): Boolean { private fun update(): Boolean {
val running = BitmessageService.isRunning val running = NodeStartupService.isRunning
builder.setOngoing(running) builder.setOngoing(running)
val connections = BitmessageService.status.getProperty("network", "connections") val connections = NodeStartupService.status.getProperty("network", "connections")
if (!running) { if (!running) {
builder.setSmallIcon(R.drawable.ic_notification_full_node_disconnected)
builder.setContentText(ctx.getString(R.string.connection_info_disconnected)) builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
} else if (connections == null || connections.properties.isEmpty()) { } 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)) builder.setContentText(ctx.getString(R.string.connection_info_pending))
} else { } else {
builder.setSmallIcon(R.drawable.ic_notification_full_node)
val info = StringBuilder() val info = StringBuilder()
for (stream in connections.properties) { for (stream in connections.properties) {
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length)) val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
@ -109,7 +112,6 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
timer = fixedRateTimer(initialDelay = 10000, period = 10000) { timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
if (!update()) { if (!update()) {
cancel() cancel()
ctx.stopService(Intent(ctx, BitmessageService::class.java))
} }
super@NetworkNotification.show() super@NetworkNotification.show()
} }

View File

@ -17,28 +17,26 @@
package ch.dissem.apps.abit.notification package ch.dissem.apps.abit.notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.support.v4.app.NotificationCompat import androidx.core.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle import androidx.core.app.NotificationCompat.BigTextStyle
import android.support.v4.app.NotificationCompat.InboxStyle import androidx.core.app.NotificationCompat.InboxStyle
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.style.StyleSpan import android.text.style.StyleSpan
import ch.dissem.apps.abit.Identicon import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.bitmessage.entity.Plaintext
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE 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.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.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
import ch.dissem.apps.abit.util.Drawables.toBitmap import ch.dissem.apps.abit.util.toBitmap
import ch.dissem.bitmessage.entity.Plaintext
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) { class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
@ -53,7 +51,7 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
} }
builder.setSmallIcon(R.drawable.ic_notification_new_message) builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setLargeIcon(toBitmap(Identicon(plaintext.from), 192)) .setLargeIcon(Identicon(plaintext.from).toBitmap(192))
.setContentTitle(plaintext.from.toString()) .setContentTitle(plaintext.from.toString())
.setContentText(plaintext.subject) .setContentText(plaintext.subject)
.setStyle(BigTextStyle().bigText(bigText)) .setStyle(BigTextStyle().bigText(bigText))

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.support.v4.app.NotificationCompat import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R

View File

@ -1,83 +0,0 @@
/*
* 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.pow
import android.content.Context
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import ch.dissem.bitmessage.utils.Singleton.cryptography
import org.slf4j.LoggerFactory
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* @author Christian Basler
*/
class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder {
private lateinit var context: InternalContext
private val pool: ExecutorService
init {
pool = Executors.newCachedThreadPool { r ->
val thread = Executors.defaultThreadFactory().newThread(r)
thread.priority = Thread.MIN_PRIORITY
thread
}
}
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) =
pool.execute {
val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW")
val request = ProofOfWorkRequest(identity, initialHash,
CALCULATE, target)
SyncAdapter.startPowSync(ctx)
try {
val cryptoMsg = CryptoCustomMessage(request)
cryptoMsg.signAndEncrypt(
identity,
cryptography().createPublicKey(identity.publicDecryptionKey)
)
val node = Preferences.getTrustedNode(ctx)
if (node == null) {
LOG.error("trusted node is not defined")
} else {
context.networkHandler.send(
node,
Preferences.getTrustedNodePort(ctx),
cryptoMsg)
}
} catch (e: Exception) {
LOG.error(e.message, e)
}
}
override fun setContext(context: InternalContext) {
this.context = context
}
companion object {
private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java)
}
}

View File

@ -20,7 +20,7 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.DatabaseUtils import android.database.DatabaseUtils
import ch.dissem.apps.abit.util.Labels import ch.dissem.apps.abit.util.getText
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractLabelRepository import ch.dissem.bitmessage.ports.AbstractLabelRepository
import ch.dissem.bitmessage.ports.MessageRepository import ch.dissem.bitmessage.ports.MessageRepository
@ -30,7 +30,8 @@ import java.util.*
/** /**
* [MessageRepository] implementation using the Android SQL API. * [MessageRepository] implementation using the Android SQL API.
*/ */
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) : AbstractLabelRepository() { class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) :
AbstractLabelRepository() {
override fun find(where: String): List<Label> { override fun find(where: String): List<Label> {
val result = LinkedList<Label>() val result = LinkedList<Label>()
@ -62,7 +63,12 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString())) db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
} else { } else {
db.transaction { db.transaction {
val exists = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "label=?", arrayOf(label.toString())) > 0 val exists = DatabaseUtils.queryNumEntries(
db,
TABLE_NAME,
"label=?",
arrayOf(label.toString())
) > 0
if (exists) { if (exists) {
val values = ContentValues() val values = ContentValues()
@ -82,7 +88,8 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
} }
} }
internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)") internal fun findLabels(msgId: Any) =
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
companion object { companion object {
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE } val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
@ -97,11 +104,12 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
internal fun getLabel(c: Cursor, context: Context): Label { internal fun getLabel(c: Cursor, context: Context): Label {
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE)) val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
val type = if (typeName == null) null else Label.Type.valueOf(typeName) val type = if (typeName == null) null else Label.Type.valueOf(typeName)
val text: String? = Labels.getText(type, null, context) val text: String? = type?.getText(null, context)
val label = Label( val label = Label(
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)), text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
type, type,
c.getInt(c.getColumnIndex(COLUMN_COLOR))) c.getInt(c.getColumnIndex(COLUMN_COLOR))
)
label.id = c.getLong(c.getColumnIndex(COLUMN_ID)) label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
return label return label
} }

View File

@ -21,6 +21,7 @@ import android.database.Cursor
import android.database.DatabaseUtils import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE 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
import ch.dissem.apps.abit.util.UuidUtils.asUuid import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
@ -38,44 +39,89 @@ import java.util.*
/** /**
* [MessageRepository] implementation using the Android SQL API. * [MessageRepository] implementation using the Android SQL API.
*/ */
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() { class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) { 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) super.findMessages(null as Label?, offset, limit)
} else { } else {
super.findMessages(label, offset, limit) super.findMessages(label, offset, limit)
} }
override fun countUnread(label: Label?) = when { 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 === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries( label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase, sql.readableDatabase,
TABLE_NAME, TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))", "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
arrayOf(Label.Type.UNREAD.name) selectIdentityQuery,
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt() ).toInt()
else -> DatabaseUtils.queryNumEntries( else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase, sql.readableDatabase,
TABLE_NAME, TABLE_NAME,
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " + "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=?))", "AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
arrayOf(label.id.toString(), Label.Type.UNREAD.name) selectIdentityQuery,
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt() ).toInt()
} }
}
override fun findConversations(label: Label?): List<UUID> { 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 projection = arrayOf(COLUMN_CONVERSATION)
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
val where = when { val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)" label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
label == null -> null label == null -> if (selectIdentityQuery.isNotBlank()) {
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})" "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>() val result = LinkedList<UUID>()
sql.readableDatabase.query( sql.readableDatabase.query(
true, true,
TABLE_NAME, projection, where, TABLE_NAME,
null, null, null, null, null projection,
where,
selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c -> ).use { c ->
while (c.moveToNext()) { while (c.moveToNext()) {
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)) val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
@ -128,16 +174,34 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
db.update(PARENTS_TABLE_NAME, values, where, null) db.update(PARENTS_TABLE_NAME, values, where, null)
} }
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> { 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 result = LinkedList<Plaintext>()
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
// Define a projection that specifies which columns from the database // Define a projection that specifies which columns from the database
// you will actually use after this query. // 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) 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( sql.readableDatabase.query(
TABLE_NAME, projection, TABLE_NAME, projection,
where, null, null, null, "$where $selectIdentityQuery", selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC", "$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit" if (limit == 0) null else "$offset, $limit"
).use { c -> ).use { c ->
@ -174,7 +238,8 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
labels = findLabels(id!!) labels = findLabels(id!!)
} }
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId) private fun findLabels(msgId: Any) =
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
override fun save(message: Plaintext) { override fun save(message: Plaintext) {
saveContactIfNecessary(message.from) saveContactIfNecessary(message.from)
@ -233,6 +298,39 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString())) 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 { companion object {
private const val TABLE_NAME = "Message" private const val TABLE_NAME = "Message"
private const val COLUMN_ID = "id" private const val COLUMN_ID = "id"
@ -257,4 +355,5 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
private const val JT_COLUMN_MESSAGE = "message_id" private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id" private const val JT_COLUMN_LABEL = "label_id"
} }
} }

View File

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

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit.service
import android.app.IntentService import android.app.IntentService
import android.content.Intent import android.content.Intent
import ch.dissem.apps.abit.util.NetworkUtils import ch.dissem.apps.abit.util.network
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
@ -44,10 +44,10 @@ class BitmessageIntentService : IntentService("BitmessageIntentService") {
Singleton.getMessageListener(this).resetNotification() Singleton.getMessageListener(this).resetNotification()
} }
if (it.hasExtra(EXTRA_STARTUP_NODE)) { if (it.hasExtra(EXTRA_STARTUP_NODE)) {
NetworkUtils.enableNode(this) network.enableNode()
} }
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) { if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
NetworkUtils.disableNode(this) network.disableNode()
} }
} }
} }

View File

@ -1,107 +0,0 @@
/*
* 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.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
class BitmessageService : Service() {
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.isConnectionAllowed(this@BitmessageService)) {
bmc.shutdown()
}
}
}
private val cleanupHandler = Handler()
private val cleanupTask: Runnable = object : Runnable {
override fun run() {
bmc.cleanup()
if (isRunning) {
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day
}
}
}
override fun onCreate() {
registerReceiver(
connectivityReceiver,
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
notification = NetworkNotification(this)
running = false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!isRunning) {
running = true
notification.connecting()
startForeground(NETWORK_NOTIFICATION_ID, notification.notification)
if (!bmc.isRunning()) {
bmc.startup()
}
notification.show()
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L)
}
return Service.START_STICKY
}
override fun onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown()
}
running = false
notification.showShutdown()
cleanupHandler.removeCallbacks(cleanupTask)
bmc.cleanup()
unregisterReceiver(connectivityReceiver)
stopSelf()
}
override fun onBind(intent: Intent) = null
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")
}
}

View File

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

View File

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

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.service
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.support.v4.content.ContextCompat import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.notification.ProofOfWorkNotification import ch.dissem.apps.abit.notification.ProofOfWorkNotification
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
import ch.dissem.apps.abit.util.PowStats import ch.dissem.apps.abit.util.PowStats

View File

@ -20,15 +20,12 @@ import android.content.Context
import android.widget.Toast import android.widget.Toast
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.adapter.AndroidCryptography
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
import ch.dissem.apps.abit.listener.MessageListener import ch.dissem.apps.abit.listener.MessageListener
import ch.dissem.apps.abit.pow.ServerPowEngine
import ch.dissem.apps.abit.repository.* import ch.dissem.apps.abit.repository.*
import ch.dissem.apps.abit.util.Constants import ch.dissem.apps.abit.util.preferences
import ch.dissem.apps.abit.util.Observable
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
@ -37,6 +34,7 @@ import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService import ch.dissem.bitmessage.utils.ConversationService
import ch.dissem.bitmessage.utils.TTL import ch.dissem.bitmessage.utils.TTL
import ch.dissem.bitmessage.utils.UnixTime.DAY import ch.dissem.bitmessage.utils.UnixTime.DAY
import io.reactivex.subjects.BehaviorSubject
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -45,7 +43,7 @@ import java.lang.ref.WeakReference
* Provides singleton objects across the application. * Provides singleton objects across the application.
*/ */
object Singleton { object Singleton {
var currentLabel = Observable<Label?>(null) var currentLabel = BehaviorSubject.create<Label>()
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
val labeler = DefaultLabeler().apply { val labeler = DefaultLabeler().apply {
@ -53,7 +51,7 @@ object Singleton {
MainActivity.apply { MainActivity.apply {
runOnUiThread { runOnUiThread {
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter -> swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
currentLabel.value?.let { label -> currentLabel.value?.let {label ->
when { when {
label.type == Label.Type.TRASH label.type == Label.Type.TRASH
&& added.all { it.type == Label.Type.TRASH } && added.all { it.type == Label.Type.TRASH }
@ -66,6 +64,10 @@ object Singleton {
// work-around for messages that are deleted from unread, which already have the unread label removed // work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message) 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) -> { added.contains(label) -> {
// in most cases, top should be the correct position, but time will show if // in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in // the message should be properly sorted in
@ -101,22 +103,19 @@ object Singleton {
TTL.pubkey = 2 * DAY TTL.pubkey = 2 * DAY
val ctx = context.applicationContext val ctx = context.applicationContext
val sqlHelper = SqlHelper(ctx) val sqlHelper = SqlHelper(ctx)
proofOfWorkEngine = SwitchingProofOfWorkEngine( proofOfWorkEngine = ServicePowEngine(ctx)
ctx, Constants.PREFERENCE_SERVER_POW, cryptography = SpongyCryptography()
ServerPowEngine(ctx),
ServicePowEngine(ctx)
)
cryptography = AndroidCryptography()
nodeRegistry = AndroidNodeRegistry(sqlHelper) nodeRegistry = AndroidNodeRegistry(sqlHelper)
inventory = AndroidInventory(sqlHelper) inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper) addressRepo = AndroidAddressRepository(sqlHelper)
labelRepo = AndroidLabelRepository(sqlHelper, ctx) labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper) messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it } proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler() networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx) listener = getMessageListener(ctx)
labeler = Singleton.labeler labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false preferences.sendPubkeyOnIdentityCreation = false
preferences.port = context.preferences.listeningPort
} }
} }
@ -132,8 +131,6 @@ object Singleton {
fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository
fun getProofOfWorkRepository(ctx: Context) = powRepo ?: getBitmessageContext(ctx).internals.proofOfWorkRepository
fun getIdentity(ctx: Context): BitmessageAddress? = fun getIdentity(ctx: Context): BitmessageAddress? =
init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc -> init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc ->
val identities = bmc.addresses.getIdentities() val identities = bmc.addresses.getIdentities()

View File

@ -3,18 +3,17 @@ package ch.dissem.apps.abit.service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import ch.dissem.apps.abit.util.NetworkUtils import android.content.Intent.ACTION_BOOT_COMPLETED
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
/** /**
* Starts the Bitmessage "full node" service if conditions allow it * Starts the Bitmessage "full node" service if conditions allow it
*/ */
class StartServiceReceiver : BroadcastReceiver() { class StartServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == "android.intent.action.BOOT_COMPLETED") { if (intent?.action == ACTION_BOOT_COMPLETED && context.preferences.online) {
if (Preferences.isFullNodeActive(context)) { context.network.enableNode(false)
NetworkUtils.enableNode(context, false)
}
} }
} }
} }

View File

@ -1,34 +0,0 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import android.os.Build
import android.support.annotation.RequiresApi
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
/**
* 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.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class StartupNodeOnWifiService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val bmc = Singleton.getBitmessageContext(this)
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
NetworkUtils.doStartBitmessageService(applicationContext)
}
return true
}
override fun onStopJob(params: JobParameters?) = if (Preferences.isWifiOnly(this)) {
// Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
// depending on Android version.
Preferences.isFullNodeActive(this)
} else {
false
}
}

View File

@ -1,62 +0,0 @@
/*
* 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.synchronization
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.NetworkErrorException
import android.content.Context
import android.os.Bundle
/**
* Implement AbstractAccountAuthenticator and stub out all
* of its methods
*/
class Authenticator(context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(r: AccountAuthenticatorResponse, s: String) =
throw UnsupportedOperationException("Editing properties is not supported")
// Don't add additional accounts
@Throws(NetworkErrorException::class)
override fun addAccount(r: AccountAuthenticatorResponse, s: String, s2: String, strings: Array<String>, bundle: Bundle) = null
// Ignore attempts to confirm credentials
@Throws(NetworkErrorException::class)
override fun confirmCredentials(r: AccountAuthenticatorResponse, account: Account, bundle: Bundle) = null
@Throws(NetworkErrorException::class)
override fun getAuthToken(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
throw UnsupportedOperationException("Getting an authentication token is not supported")
override fun getAuthTokenLabel(s: String) =
throw UnsupportedOperationException("Getting a label for the auth token is not supported")
@Throws(NetworkErrorException::class)
override fun updateCredentials(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
throw UnsupportedOperationException("Updating user credentials is not supported")
@Throws(NetworkErrorException::class)
override fun hasFeatures(r: AccountAuthenticatorResponse, account: Account, strings: Array<String>) =
throw UnsupportedOperationException("Checking features for the account is not supported")
companion object {
val ACCOUNT_SYNC = Account("Bitmessage", "ch.dissem.bitmessage")
val ACCOUNT_POW = Account("Proof of Work ", "ch.dissem.bitmessage")
}
}

View File

@ -1,42 +0,0 @@
/*
* 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.synchronization
import android.app.Service
import android.content.Intent
/**
* A bound Service that instantiates the authenticator
* when started.
*/
class AuthenticatorService : Service() {
/**
* Instance field that stores the authenticator object
*/
private var authenticator: Authenticator? = null
override fun onCreate() {
// Create a new authenticator object
authenticator = Authenticator(this)
}
/*
* When the system binds to this Service to make the RPC call
* return the authenticator's IBinder.
*/
override fun onBind(intent: Intent) = authenticator?.iBinder
}

View File

@ -1,72 +0,0 @@
/*
* 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.synchronization
import android.content.ContentProvider
import android.content.ContentValues
import android.net.Uri
/*
* Define an implementation of ContentProvider that stubs out
* all methods
*/
class StubProvider : ContentProvider() {
/**
* Always return true, indicating that the
* provider loaded correctly.
*/
override fun onCreate() = true
/**
* Return no type for MIME type
*/
override fun getType(uri: Uri) = null
/**
* query() always returns no results
*/
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?) = null
/**
* insert() always returns null (no URI)
*/
override fun insert(uri: Uri, values: ContentValues?) = null
/**
* delete() always returns "no rows affected" (0)
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
/**
* update() always returns "no rows affected" (0)
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?) = 0
companion object {
const val AUTHORITY = "ch.dissem.apps.abit.provider"
}
}

View File

@ -1,185 +0,0 @@
/*
* 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.synchronization
import android.accounts.Account
import android.accounts.AccountManager
import android.content.*
import android.os.Bundle
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.exception.DecryptionFailedException
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE
import ch.dissem.bitmessage.utils.Singleton.cryptography
import org.slf4j.LoggerFactory
import java.io.IOException
/**
* Sync Adapter to synchronize with the Bitmessage network - fetches
* new objects and then disconnects.
*/
class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) {
private val bmc = Singleton.getBitmessageContext(context)
override fun onPerformSync(
account: Account,
extras: Bundle,
authority: String,
provider: ContentProviderClient,
syncResult: SyncResult
) {
try {
if (account == ACCOUNT_SYNC) {
if (Preferences.isConnectionAllowed(context)) {
syncData()
}
} else if (account == ACCOUNT_POW) {
syncPOW()
} else {
syncResult.stats.numAuthExceptions++
}
} catch (e: IOException) {
syncResult.stats.numIoExceptions++
} catch (e: DecryptionFailedException) {
syncResult.stats.numAuthExceptions++
}
}
private fun syncData() {
// If the Bitmessage context acts as a full node, synchronization isn't necessary
if (bmc.isRunning()) {
LOG.info("Synchronization skipped, Abit is acting as a full node")
return
}
val trustedNode = Preferences.getTrustedNode(context)
if (trustedNode == null) {
LOG.info("Trusted node not available, disabling synchronization")
stopSync(context)
return
}
LOG.info("Synchronization started")
bmc.synchronize(
trustedNode,
Preferences.getTrustedNodePort(context),
Preferences.getTimeoutInSeconds(context),
true
)
LOG.info("Synchronization finished")
}
private fun syncPOW() {
val identity = Singleton.getIdentity(context)
if (identity == null) {
LOG.info("No identity available - skipping POW synchronization")
return
}
val trustedNode = Preferences.getTrustedNode(context)
if (trustedNode == null) {
LOG.info("Trusted node not available, disabling POW synchronization")
stopPowSync(context)
return
}
// If the Bitmessage context acts as a full node, synchronization isn't necessary
LOG.info("Looking for completed POW")
val privateKey = identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
val reader = ProofOfWorkRequest.Reader(identity)
val powRepo = Singleton.getProofOfWorkRepository(context)
val items = powRepo.getItems()
for (initialHash in items) {
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
val cryptoMsg = CryptoCustomMessage(
ProofOfWorkRequest(identity, initialHash, CALCULATE, target))
cryptoMsg.signAndEncrypt(identity, signingKey)
val response = bmc.send(
trustedNode,
Preferences.getTrustedNodePort(context),
cryptoMsg
)
if (response.isError) {
LOG.error("Server responded with error: ${String(response.getData())}")
} else {
val (_, _, request, data) = CryptoCustomMessage.read(response, reader).decrypt(privateKey)
if (request == COMPLETE) {
bmc.internals.proofOfWorkService.onNonceCalculated(initialHash, data)
}
}
}
if (items.isEmpty()) {
stopPowSync(context)
}
LOG.info("Synchronization finished")
}
companion object {
private val LOG = LoggerFactory.getLogger(SyncAdapter::class.java)
private const val SYNC_FREQUENCY = 15 * 60L // seconds
fun startSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_SYNC)
// Recommend a schedule for automatic synchronization. The system may modify this based
// on other scheduled syncs and network utilization.
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
}
fun stopSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_SYNC)
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
}
fun startPowSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_POW)
// Recommend a schedule for automatic synchronization. The system may modify this based
// on other scheduled syncs and network utilization.
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
}
fun stopPowSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_POW)
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
}
private fun addAccount(ctx: Context, account: Account): Account {
if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) {
// Inform the system that this account supports sync
ContentResolver.setIsSyncable(account, AUTHORITY, 1)
// Inform the system that this account is eligible for auto sync when the network is up
ContentResolver.setSyncAutomatically(account, AUTHORITY, true)
}
return account
}
}
}

View File

@ -1,50 +0,0 @@
/*
* 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.synchronization
import android.app.Service
import android.content.Intent
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
class SyncService : Service() {
/**
* Instantiate the sync adapter object.
*/
override fun onCreate() = synchronized(syncAdapterLock) {
if (syncAdapter == null) {
syncAdapter = SyncAdapter(this, true)
}
}
/**
* Return an object that allows the system to invoke
* the sync adapter.
*/
override fun onBind(intent: Intent) = syncAdapter?.syncAdapterBinder
companion object {
// Storage for an instance of the sync adapter
private var syncAdapter: SyncAdapter? = null
// Object to use as a thread-safe lock
private val syncAdapterLock = Any()
}
}

View File

@ -17,8 +17,6 @@
package ch.dissem.apps.abit.util package ch.dissem.apps.abit.util
import android.content.Context import android.content.Context
import android.support.annotation.DrawableRes
import android.support.annotation.StringRes
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import java.io.IOException import java.io.IOException
@ -43,11 +41,10 @@ object Assets {
} catch (e: IOException) { } catch (e: IOException) {
throw RuntimeException(e) throw RuntimeException(e)
} }
} }
}
@DrawableRes fun Plaintext.Status.getDrawable() = when (this) {
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> 0 Plaintext.Status.RECEIVED -> 0
Plaintext.Status.DRAFT -> R.drawable.draft Plaintext.Status.DRAFT -> R.drawable.draft
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
@ -55,10 +52,9 @@ object Assets {
Plaintext.Status.SENT -> R.drawable.sent Plaintext.Status.SENT -> R.drawable.sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
else -> 0 else -> 0
} }
@StringRes fun Plaintext.Status.getString() = when (this) {
fun getStatusString(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> R.string.status_received Plaintext.Status.RECEIVED -> R.string.status_received
Plaintext.Status.DRAFT -> R.string.status_draft Plaintext.Status.DRAFT -> R.string.status_draft
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
@ -66,5 +62,4 @@ object Assets {
Plaintext.Status.SENT -> R.string.status_sent Plaintext.Status.SENT -> R.string.status_sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
else -> 0 else -> 0
}
} }

View File

@ -22,14 +22,14 @@ import java.util.regex.Pattern
* @author Christian Basler * @author Christian Basler
*/ */
object Constants { object Constants {
const val PREFERENCE_ONLINE = "online"
const val PREFERENCE_WIFI_ONLY = "wifi_only" const val PREFERENCE_WIFI_ONLY = "wifi_only"
const val PREFERENCE_TRUSTED_NODE = "trusted_node" const val PREFERENCE_REQUIRE_CHARGING = "require_charging"
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout" const val PREFERENCE_EMULATE_CONVERSATIONS = "emulate_conversations"
const val PREFERENCE_SERVER_POW = "server_pow"
const val PREFERENCE_FULL_NODE = "full_node"
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments" const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms" const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
const val PREFERENCE_POW_COUNT = "pow_count" const val PREFERENCE_POW_COUNT = "pow_count"
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
const val BITMESSAGE_URL_SCHEMA = "bitmessage:" const val BITMESSAGE_URL_SCHEMA = "bitmessage:"

View File

@ -21,13 +21,14 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color.BLACK import android.graphics.Color.BLACK
import android.graphics.Color.WHITE import android.graphics.Color.WHITE
import android.graphics.drawable.Drawable
import android.util.Base64 import android.util.Base64
import android.util.Base64.NO_WRAP import android.util.Base64.NO_WRAP
import android.util.Base64.URL_SAFE import android.util.Base64.URL_SAFE
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.Drawables.QR_CODE_SIZE
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
@ -42,47 +43,48 @@ import java.io.ByteArrayOutputStream
* Some helper methods to work with drawables. * Some helper methods to work with drawables.
*/ */
object Drawables { object Drawables {
private val LOG = LoggerFactory.getLogger(Drawables::class.java) internal val LOG = LoggerFactory.getLogger(Drawables::class.java)
private const val QR_CODE_SIZE = 350 internal const val QR_CODE_SIZE = 350
fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem { fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
val item = menu.findItem(menuItem) val item = menu.findItem(menuItem)
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar() item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
return item return item
} }
}
fun toBitmap(identicon: Identicon, width: Int, height: Int = width): Bitmap { fun Drawable.toBitmap(width: Int, height: Int = width): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
identicon.setBounds(0, 0, canvas.width, canvas.height) setBounds(0, 0, canvas.width, canvas.height)
identicon.draw(canvas) draw(canvas)
return bitmap return bitmap
} }
fun qrCode(address: BitmessageAddress?): Bitmap? { fun BitmessageAddress.qrCode(): Bitmap? {
if (address == null) {
return null
}
val link = StringBuilder() val link = StringBuilder()
link.append(Constants.BITMESSAGE_URL_SCHEMA) link.append(Constants.BITMESSAGE_URL_SCHEMA)
link.append(address.address) link.append(address)
if (address.alias != null) { if (alias != null) {
link.append("?label=").append(address.alias) link.append("?label=").append(alias)
} }
address.pubkey?.apply { pubkey?.apply {
link.append(if (address.alias == null) '?' else '&') link.append(if (alias == null) '?' else '&')
val pubkey = ByteArrayOutputStream() val pubkey = ByteArrayOutputStream()
writer().writeUnencrypted(pubkey) writer().writeUnencrypted(pubkey)
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP)) link.append("pubkey=")
.append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
} }
val result: BitMatrix val result: BitMatrix
try { try {
result = MultiFormatWriter().encode(link.toString(), result = MultiFormatWriter().encode(
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null) link.toString(),
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
)
} catch (e: WriterException) { } catch (e: WriterException) {
LOG.error(e.message, e) Drawables.LOG.error(e.message, e)
return null return null
} }
@ -98,5 +100,4 @@ object Drawables {
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h) bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
return bitmap return bitmap
}
} }

View File

@ -1,31 +0,0 @@
package ch.dissem.apps.abit.util
import android.support.annotation.DrawableRes
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
/**
* Utilities to work with the common floating action button in the main activity
*/
object FabUtils {
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
fab.removeAllOnMenuItemClickListeners()
fab.show()
fab.closeMenu()
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)
}
}
return fab
}
}

View File

@ -1,22 +1,20 @@
package ch.dissem.apps.abit.util package ch.dissem.apps.abit.util
import android.content.Context import android.content.Context
import android.support.annotation.ColorInt 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.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.IIcon
import ch.dissem.apps.abit.R /*
import ch.dissem.bitmessage.entity.valueobject.Label * Helper methods to help with translating the default labels, getting label colors and so on.
/**
* Helper class to help with translating the default labels, getting label colors and so on.
*/ */
object Labels {
fun getText(label: Label, ctx: Context): String = getText(label.type, label.toString(), ctx)!!
fun getText(type: Label.Type?, alternative: String?, ctx: Context) = when (type) { 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.INBOX -> ctx.getString(R.string.inbox)
Label.Type.DRAFT -> ctx.getString(R.string.draft) Label.Type.DRAFT -> ctx.getString(R.string.draft)
Label.Type.OUTBOX -> ctx.getString(R.string.outbox) Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
@ -25,9 +23,9 @@ object Labels {
Label.Type.TRASH -> ctx.getString(R.string.trash) Label.Type.TRASH -> ctx.getString(R.string.trash)
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts) Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
else -> alternative else -> alternative
} }
fun getIcon(label: Label): IIcon = when (label.type) { fun Label.getIcon(): IIcon = when (type) {
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
@ -36,10 +34,9 @@ object Labels {
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
else -> CommunityMaterial.Icon.cmd_label else -> CommunityMaterial.Icon.cmd_label
}
@ColorInt
fun getColor(label: Label) = if (label.type == null) {
label.color
} else 0xFF000000.toInt()
} }
@ColorInt
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
color
} else default

View File

@ -7,58 +7,71 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
import ch.dissem.apps.abit.service.BitmessageService import ch.dissem.apps.abit.service.CleanupService
import ch.dissem.apps.abit.service.StartupNodeOnWifiService import ch.dissem.apps.abit.service.NodeStartupService
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
val Context.network get() = NetworkUtils.getInstance(this)
object NetworkUtils { class NetworkUtils internal constructor(private val ctx: Context) {
fun enableNode(ctx: Context, ask: Boolean = true) { private val jobScheduler by lazy { ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler }
Preferences.setFullNodeActive(ctx, true)
if (Preferences.isWifiOnly(ctx)) { fun enableNode(ask: Boolean = true) {
if (Preferences.isConnectionAllowed(ctx)) { if (ask && !ctx.preferences.connectionAllowed) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Ask for connection
scheduleNodeStart(ctx)
} else {
doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch()
}
} else if (ask) {
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java) val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) { if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
} }
ctx.startActivity(dialogIntent) ctx.startActivity(dialogIntent)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scheduleNodeStart(ctx)
}
} else { } else {
doStartBitmessageService(ctx) scheduleNodeStart()
MainActivity.updateNodeSwitch()
} }
} }
fun doStartBitmessageService(ctx: Context) { fun disableNode() {
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java)) jobScheduler.cancelAll()
} }
fun disableNode(ctx: Context) { fun scheduleNodeStart() {
Preferences.setFullNodeActive(ctx, false) JobInfo.Builder(0, ComponentName(ctx, NodeStartupService::class.java)).let { builder ->
ctx.stopService(Intent(ctx, BitmessageService::class.java)) when {
} ctx.preferences.wifiOnly ->
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun scheduleNodeStart(ctx: Context) {
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
val builder = JobInfo.Builder(0, serviceComponent)
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR) Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler 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()) 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
}
}
} }

View File

@ -1,37 +0,0 @@
package ch.dissem.apps.abit.util
import kotlin.properties.Delegates
/**
* A simple observable implementation that should be mostly
*/
class Observable<T>(value: T) {
private val observers = mutableMapOf<Any, (T) -> Unit>()
var value: T by Delegates.observable(value, { _, old, new ->
if (old != new) {
observers.values.forEach { it.invoke(new) }
}
})
/**
* The key will make sure the observer can easily be removed. Usually the key should be either
* the object that created the observer, or the observer itself, if it's easily available.
*
* Note that a map is used for observers, so if you define more than one observer with the same
* key, all previous ones will be removed. Also, the observers will be notified in no specific
* order.
*
* To prevent memory leaks, the observer must be removed if it isn't used anymore.
*/
fun addObserver(key: Any, observer: (T) -> Unit) {
observers[key] = observer
}
/**
* Remove the observer that was registered with the given key.
*/
fun removeObserver(key: Any) {
observers.remove(key)
}
}

View File

@ -1,344 +0,0 @@
/*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will Google be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, as long as the origin is not misrepresented.
*/
package ch.dissem.apps.abit.util;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
/**
* Fixes for the output of the default PRNG having low entropy.
* <p/>
* The fixes need to be applied via {@link #apply()} before any use of Java
* Cryptography Architecture primitives. A good place to invoke them is in the
* application's {@code onCreate}.
*
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
*/
public final class PRNGFixes {
private static final int VERSION_CODE_JELLY_BEAN = 16;
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
getBuildFingerprintAndDeviceSerial();
/**
* Hidden constructor to prevent instantiation.
*/
private PRNGFixes() {
}
/**
* Applies all fixes.
*
* @throws SecurityException if a fix is needed but could not be applied.
*/
public static void apply() {
applyOpenSSLFix();
installLinuxPRNGSecureRandom();
}
/**
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
* fix is not needed.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class)
.invoke(null, (Object) generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
int bytesRead = (Integer) Class.forName(
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) {
throw new IOException(
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
}
} catch (Exception e) {
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
}
}
/**
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
* default. Does nothing if the implementation is already the default or if
* there is not need to install the implementation.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void installLinuxPRNGSecureRandom()
throws SecurityException {
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
// No need to apply the fix
return;
}
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders =
Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
// Assert that new SecureRandom() and
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
// by the Linux PRNG-based SecureRandom implementation.
SecureRandom rng1 = new SecureRandom();
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng1.getProvider().getClass())) {
throw new SecurityException(
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
}
SecureRandom rng2;
try {
rng2 = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("SHA1PRNG not available", e);
}
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng2.getProvider().getClass())) {
throw new SecurityException(
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
}
}
/**
* {@code Provider} of {@code SecureRandom} engines which pass through
* all requests to the Linux PRNG.
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG",
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need to
// prevent them from getting the default implementation whose output
// may have low entropy.
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
}
/**
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
* ({@code /dev/urandom}).
*/
@SuppressWarnings("JavaDoc")
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
/*
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
* are passed through to the Linux PRNG (/dev/urandom). Instances of
* this class seed themselves by mixing in the current time, PID, UID,
* build fingerprint, and hardware serial number (where available) into
* Linux PRNG.
*
* Concurrency: Read requests to the underlying Linux PRNG are
* serialized (on sLock) to ensure that multiple threads do not get
* duplicated PRNG output.
*/
private static final File URANDOM_FILE = new File("/dev/urandom");
private static final Object sLock = new Object();
/**
* Input stream for reading from Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static DataInputStream sUrandomIn;
/**
* Output stream for writing to Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static OutputStream sUrandomOut;
/**
* Whether this engine instance has been seeded. This is needed because
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
private boolean mSeeded;
@Override
protected void engineSetSeed(byte[] bytes) {
try {
OutputStream out;
synchronized (sLock) {
out = getUrandomOutputStream();
}
out.write(bytes);
out.flush();
} catch (IOException e) {
// On a small fraction of devices /dev/urandom is not writable.
// Log and ignore.
Log.w(PRNGFixes.class.getSimpleName(),
"Failed to mix seed into " + URANDOM_FILE);
} finally {
mSeeded = true;
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}
try {
DataInputStream in;
synchronized (sLock) {
in = getUrandomInputStream();
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (in) {
in.readFully(bytes);
}
} catch (IOException e) {
throw new SecurityException(
"Failed to read from " + URANDOM_FILE, e);
}
}
@Override
protected byte[] engineGenerateSeed(int size) {
byte[] seed = new byte[size];
engineNextBytes(seed);
return seed;
}
private DataInputStream getUrandomInputStream() {
synchronized (sLock) {
if (sUrandomIn == null) {
// NOTE: Consider inserting a BufferedInputStream between
// DataInputStream and FileInputStream if you need higher
// PRNG output performance and can live with future PRNG
// output being pulled into this process prematurely.
try {
sUrandomIn = new DataInputStream(
new FileInputStream(URANDOM_FILE));
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for reading", e);
}
}
return sUrandomIn;
}
}
private OutputStream getUrandomOutputStream() throws IOException {
synchronized (sLock) {
if (sUrandomOut == null) {
sUrandomOut = new FileOutputStream(URANDOM_FILE);
}
return sUrandomOut;
}
}
}
/**
* Generates a device- and invocation-specific seed to be mixed into the
* Linux PRNG.
*/
private static byte[] generateSeed() {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(Process.myPid());
seedBufferOut.writeInt(Process.myUid());
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
seedBufferOut.close();
return seedBuffer.toByteArray();
} catch (IOException e) {
throw new SecurityException("Failed to generate seed", e);
}
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or {@code null} if not available.
*/
private static String getDeviceSerialNumber() {
// We're using the Reflection API because Build.SERIAL is only available
// since API Level 9 (Gingerbread, Android 2.3).
try {
return (String) Build.class.getField("SERIAL").get(null);
} catch (Exception ignored) {
return null;
}
}
private static byte[] getBuildFingerprintAndDeviceSerial() {
StringBuilder result = new StringBuilder();
String fingerprint = Build.FINGERPRINT;
if (fingerprint != null) {
result.append(fingerprint);
}
String serial = getDeviceSerialNumber();
if (serial != null) {
result.append(serial);
}
try {
return result.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported");
}
}
}

View File

@ -23,7 +23,7 @@ object PowStats {
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L) powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L)
} }
} }
return (BigInteger.valueOf(averagePowUnitTime) * BigInteger(target) / TWO_POW_64).toLong() return (averagePowUnitTime * BigInteger(target) / TWO_POW_64).toLong()
} }
fun addPow(ctx: Context, time: Long, target: ByteArray) { fun addPow(ctx: Context, time: Long, target: ByteArray) {
@ -32,7 +32,7 @@ object PowStats {
synchronized(this) { synchronized(this) {
powCount++ powCount++
averagePowUnitTime = ( averagePowUnitTime = (
(BigInteger.valueOf(averagePowUnitTime) * powCountBefore + (BigInteger.valueOf(time) * TWO_POW_64 / targetBigInt)) / BigInteger.valueOf(powCount) (averagePowUnitTime * powCountBefore + (time * TWO_POW_64 / targetBigInt)) / powCount
).toLong() ).toLong()
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
@ -42,4 +42,7 @@ object PowStats {
.apply() .apply()
} }
} }
private operator fun Long.times(other: BigInteger) = this.toBigInteger() * other
private operator fun BigInteger.div(other: Long) = this / other.toBigInteger()
} }

View File

@ -17,113 +17,67 @@
package ch.dissem.apps.abit.util package ch.dissem.apps.abit.util
import android.content.Context import android.content.Context
import android.preference.PreferenceManager import android.content.Intent
import ch.dissem.apps.abit.R import android.content.IntentFilter
import ch.dissem.apps.abit.notification.ErrorNotification import android.os.BatteryManager
import ch.dissem.apps.abit.util.Constants.PREFERENCE_FULL_NODE import android.os.Build
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS
import ch.dissem.apps.abit.util.Constants.PREFERENCE_ONLINE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE import ch.dissem.apps.abit.util.Constants.PREFERENCE_SEPARATE_IDENTITIES
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager import org.jetbrains.anko.connectivityManager
import org.jetbrains.anko.defaultSharedPreferences
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.io.IOException import java.lang.ref.WeakReference
import java.net.InetAddress
val Context.preferences get() = Preferences.getInstance(this)
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
object Preferences { class Preferences internal constructor(private val ctx: Context) {
private val LOG = LoggerFactory.getLogger(Preferences::class.java) private val LOG = LoggerFactory.getLogger(Preferences::class.java)
fun useTrustedNode(ctx: Context): Boolean { val connectionAllowed get() = isAllowedForWiFi && isAllowedForCharging
val trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return false
return trustedNode.trim { it <= ' ' }.isNotEmpty() private val isAllowedForWiFi get() = !wifiOnly || !ctx.connectivityManager.isActiveNetworkMetered
private val isAllowedForCharging get() = !requireCharging || isCharging
private val sharedPreferences = ctx.defaultSharedPreferences
private val isCharging
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ctx.batteryManager.isCharging
} else {
val intent = ctx.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
} }
/** var wifiOnly
* Warning, this method might do a network call and therefore can't be called from get() = sharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
* the UI thread. set(value) {
*/ sharedPreferences.edit()
@Throws(IOException::class) .putBoolean(PREFERENCE_WIFI_ONLY, value)
fun getTrustedNode(ctx: Context): InetAddress? { .apply()
var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return null
trustedNode = trustedNode.trim { it <= ' ' }
if (trustedNode.isEmpty()) return null
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) {
val index = trustedNode.lastIndexOf(':')
trustedNode = trustedNode.substring(0, index)
}
return InetAddress.getByName(trustedNode)
} }
fun getTrustedNodePort(ctx: Context): Int { val requireCharging get() = sharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return 8444
trustedNode = trustedNode.trim { it <= ' ' }
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) { val emulateConversations get() = sharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
val index = trustedNode.lastIndexOf(':')
val portString = trustedNode.substring(index + 1)
try {
return Integer.parseInt(portString)
} catch (e: NumberFormatException) {
ErrorNotification(ctx)
.setError(R.string.error_invalid_sync_port, portString)
.show()
}
}
return 8444
}
fun getTimeoutInSeconds(ctx: Context): Long { val exportDirectory by lazy { File(ctx.filesDir, "exports") }
val preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT) ?: return 120
return preference.toLong()
}
private fun getPreference(ctx: Context, name: String): String? { val requestAcknowledgements = sharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getString(name, null) fun cleanupExportDirectory() {
}
fun isConnectionAllowed(ctx: Context) = !isWifiOnly(ctx) || !ctx.connectivityManager.isActiveNetworkMetered
fun isWifiOnly(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
}
fun setWifiOnly(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply()
}
fun isFullNodeActive(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_FULL_NODE, false)
}
fun setFullNodeActive(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply()
}
fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports")
fun requestAcknowledgements(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
}
fun setRequestAcknowledgements(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_REQUEST_ACK, status).apply()
}
fun cleanupExportDirectory(ctx: Context) {
val exportDirectory = getExportDirectory(ctx)
if (exportDirectory.exists()) { if (exportDirectory.exists()) {
exportDirectory.listFiles().forEach { file -> exportDirectory.listFiles().forEach { file ->
try { try {
@ -136,4 +90,40 @@ object Preferences {
} }
} }
} }
var online
get() = sharedPreferences.getBoolean(PREFERENCE_ONLINE, true)
set(value) {
sharedPreferences.edit()
.putBoolean(PREFERENCE_ONLINE, value)
.apply()
if (value) {
ctx.network.enableNode(true)
} else {
ctx.network.disableNode()
}
}
val separateIdentities
get() = sharedPreferences.getBoolean(PREFERENCE_SEPARATE_IDENTITIES, false)
val currentIdentity
get() = Singleton.getIdentity(ctx)
val listeningPort
get() = sharedPreferences.getString("listening_port", null)?.toIntOrNull()
?: 8444
companion object {
private var instance: WeakReference<Preferences>? = null
internal fun getInstance(ctx: Context): Preferences {
var prefs = instance?.get()
if (prefs == null) {
prefs = Preferences(ctx.applicationContext)
instance = WeakReference(prefs)
}
return prefs
}
}
} }

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_trash"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_trash"
android:gravity="right|center_vertical"
android:right="16dp"/>
</layer-list>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_archive"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_archive"
android:gravity="left|center_vertical"
android:left="16dp"/>
</layer-list>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#ffffff">
<item
android:id="@android:id/mask"
android:drawable="@android:color/white"/>
</ripple>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_active_state"/>
</item>
</layer-list>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_state"/>
</item>
</layer-list>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#000000"/>
<corners android:radius="4dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@ -18,9 +18,8 @@
<item> <item>
<color android:color="@color/bg_swipe_item_trash"/> <color android:color="@color/bg_swipe_item_trash"/>
</item> </item>
<item android:right="16dp"> <item
<bitmap android:drawable="@drawable/ic_item_swipe_trash"
android:gravity="right|center_vertical" android:gravity="right|center_vertical"
android:src="@drawable/ic_item_swipe_trash"/> android:right="16dp"/>
</item>
</layer-list> </layer-list>

View File

@ -18,9 +18,8 @@
<item> <item>
<color android:color="@color/bg_swipe_item_archive"/> <color android:color="@color/bg_swipe_item_archive"/>
</item> </item>
<item android:left="16dp"> <item
<bitmap android:drawable="@drawable/ic_item_swipe_archive"
android:gravity="left|center_vertical" android:gravity="left|center_vertical"
android:src="@drawable/ic_item_swipe_archive"/> android:left="16dp"/>
</item>
</layer-list> </layer-list>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33V8h5.47L13,7v1h4V5.33C17,4.6 16.4,4 15.67,4z"
android:fillAlpha=".3"/>
<path
android:fillColor="#FF000000"
android:pathData="M13,12.5h2L11,20v-5.5H9L12.47,8H7v12.67C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V8h-4v4.5z"/>
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/broom.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19.36,2.72L20.78,4.14L15.06,9.85C16.13,11.39 16.28,13.24 15.38,14.44L9.06,8.12C10.26,7.22 12.11,7.37 13.65,8.44L19.36,2.72M5.93,17.57C3.92,15.56 2.69,13.16 2.35,10.92L7.23,8.83L14.67,16.27L12.58,21.15C10.34,20.81 7.94,19.58 5.93,17.57Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/check_all.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM8,14L6,14v-2h2v2zM8,11L6,11L6,9h2v2zM8,8L6,8L6,6h2v2zM15,14h-5v-2h5v2zM18,11h-8L10,9h8v2zM18,8h-8L10,6h8v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#000"
android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z"
android:fillAlpha=".3"/>
<path
android:fillColor="#FF000000"
android:pathData="M3.53,10.95l8.46,10.54 0.01,0.01 0.01,-0.01 8.46,-10.54C20.04,10.62 16.81,8 12,8c-4.81,0 -8.04,2.62 -8.47,2.95z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15.9,18.45C17.25,18.45 18.35,17.35 18.35,16C18.35,14.65 17.25,13.55 15.9,13.55C14.54,13.55 13.45,14.65 13.45,16C13.45,17.35 14.54,18.45 15.9,18.45M21.1,16.68L22.58,17.84C22.71,17.95 22.75,18.13 22.66,18.29L21.26,20.71C21.17,20.86 21,20.92 20.83,20.86L19.09,20.16C18.73,20.44 18.33,20.67 17.91,20.85L17.64,22.7C17.62,22.87 17.47,23 17.3,23H14.5C14.32,23 14.18,22.87 14.15,22.7L13.89,20.85C13.46,20.67 13.07,20.44 12.71,20.16L10.96,20.86C10.81,20.92 10.62,20.86 10.54,20.71L9.14,18.29C9.05,18.13 9.09,17.95 9.22,17.84L10.7,16.68L10.65,16L10.7,15.31L9.22,14.16C9.09,14.05 9.05,13.86 9.14,13.71L10.54,11.29C10.62,11.13 10.81,11.07 10.96,11.13L12.71,11.84C13.07,11.56 13.46,11.32 13.89,11.15L14.15,9.29C14.18,9.13 14.32,9 14.5,9H17.3C17.47,9 17.62,9.13 17.64,9.29L17.91,11.15C18.33,11.32 18.73,11.56 19.09,11.84L20.83,11.13C21,11.07 21.17,11.13 21.26,11.29L22.66,13.71C22.75,13.86 22.71,14.05 22.58,14.16L21.1,15.31L21.15,16L21.1,16.68M6.69,8.07C7.56,8.07 8.26,7.37 8.26,6.5C8.26,5.63 7.56,4.92 6.69,4.92A1.58,1.58 0 0,0 5.11,6.5C5.11,7.37 5.82,8.07 6.69,8.07M10.03,6.94L11,7.68C11.07,7.75 11.09,7.87 11.03,7.97L10.13,9.53C10.08,9.63 9.96,9.67 9.86,9.63L8.74,9.18L8,9.62L7.81,10.81C7.79,10.92 7.7,11 7.59,11H5.79C5.67,11 5.58,10.92 5.56,10.81L5.4,9.62L4.64,9.18L3.5,9.63C3.41,9.67 3.3,9.63 3.24,9.53L2.34,7.97C2.28,7.87 2.31,7.75 2.39,7.68L3.34,6.94L3.31,6.5L3.34,6.06L2.39,5.32C2.31,5.25 2.28,5.13 2.34,5.03L3.24,3.47C3.3,3.37 3.41,3.33 3.5,3.37L4.63,3.82L5.4,3.38L5.56,2.19C5.58,2.08 5.67,2 5.79,2H7.59C7.7,2 7.79,2.08 7.81,2.19L8,3.38L8.74,3.82L9.86,3.37C9.96,3.33 10.08,3.37 10.13,3.47L11.03,5.03C11.09,5.13 11.07,5.25 11,5.32L10.03,6.06L10.06,6.5L10.03,6.94Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10H6.71C7.37,7.69 9.5,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7.73,10L15.73,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10M3,5.27L5.75,8C2.56,8.15 0,10.77 0,14A6,6 0 0,0 6,20H17.73L19.73,22L21,20.73L4.27,4M19.35,10.03C18.67,6.59 15.64,4 12,4C10.5,4 9.15,4.43 8,5.17L9.45,6.63C10.21,6.23 11.08,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15C22,16.13 21.36,17.11 20.44,17.62L21.89,19.07C23.16,18.16 24,16.68 24,15C24,12.36 21.95,10.22 19.35,10.03Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.77,6.76L6.23,5.48 0.82,12l5.41,6.52 1.54,-1.28L3.42,12l4.35,-5.24zM7,13h2v-2L7,11v2zM17,11h-2v2h2v-2zM11,13h2v-2h-2v2zM17.77,5.48l-1.54,1.28L20.58,12l-4.35,5.24 1.54,1.28L23.18,12l-5.41,-6.52z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,11.24L9,7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5c0,1.56 0.79,2.93 2,3.74zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11L13,13.5v-6c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,6.67 10,7.5v10.74l-3.43,-0.72c-0.08,-0.01 -0.15,-0.03 -0.24,-0.03 -0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8 4.94,4.94c0.27,0.27 0.65,0.44 1.06,0.44h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2 0,-0.62 -0.38,-1.16 -0.91,-1.38z"/>
</vector>

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