Compare commits
51 Commits
master
...
feature/se
Author | SHA1 | Date | |
---|---|---|---|
6d7b77fd4b | |||
6b8066d473 | |||
0405d9e04f | |||
e67a4ea71b | |||
a9602368fb | |||
9f2508c1a5 | |||
6128fd32f9 | |||
ccfdff7fd8 | |||
3767d976c8 | |||
87bc01701c | |||
6878f80a54 | |||
a01f116065 | |||
c6e29c056b | |||
8b89d81970 | |||
76317a2488 | |||
85f114a33d | |||
9e7f247763 | |||
ec4615b639 | |||
2ddd78dfe2 | |||
90bb538692 | |||
9cc07f73ae | |||
0b432b6a67 | |||
725ec60fd4 | |||
60c4a4d8a0 | |||
6585876b25 | |||
b1fd9d9ef9 | |||
e05d27bfbc | |||
be7a7f1af6 | |||
61e579c0d4 | |||
eee1be873a | |||
4c213d3e9c | |||
76cb5df998 | |||
3026ae8505 | |||
412180f443 | |||
78f9621afa | |||
85562efc0d | |||
a89f80f400 | |||
1426b786e8 | |||
6a311a0346 | |||
9b75a8c2ef | |||
4e5ba4401a | |||
f374748f71 | |||
49e77199b0 | |||
46e5bb7ece | |||
8004865e01 | |||
101913a531 | |||
40f8bc87a2 | |||
0d1cfff883 | |||
d7b7b11cdf | |||
d8d5f70b37 | |||
16f1dfa6f6 |
9
.drone.yml
Normal file
9
.drone.yml
Normal 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
46
CODE_OF_CONDUCT.md
Normal 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
16
CONTRIBUTING.md
Normal 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
201
LICENSE
Normal 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.
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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.
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.apps.abit
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import android.view.*
|
||||||
|
import ch.dissem.apps.abit.adapter.ConversationAdapter
|
||||||
|
import ch.dissem.apps.abit.service.Singleton
|
||||||
|
import ch.dissem.apps.abit.util.Drawables
|
||||||
|
import ch.dissem.bitmessage.entity.Conversation
|
||||||
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
|
import kotlinx.android.synthetic.main.fragment_conversation_detail.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fragment representing a single Message detail screen.
|
||||||
|
* This fragment is either contained in a [MainActivity]
|
||||||
|
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||||
|
* on handsets.
|
||||||
|
*/
|
||||||
|
class ConversationDetailFragment : Fragment() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content this fragment is presenting.
|
||||||
|
*/
|
||||||
|
private var itemId: UUID? = null
|
||||||
|
private var item: Conversation? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
arguments?.let { arguments ->
|
||||||
|
if (arguments.containsKey(ARG_ITEM_ID)) {
|
||||||
|
// Load the dummy content specified by the fragment
|
||||||
|
// arguments. In a real-world scenario, use a Loader
|
||||||
|
// to load content from a content provider.
|
||||||
|
itemId = arguments.getSerializable(ARG_ITEM_ID) as UUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View =
|
||||||
|
inflater.inflate(R.layout.fragment_conversation_detail, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||||
|
|
||||||
|
item = itemId?.let { Singleton.getConversationService(ctx).getConversation(it) }
|
||||||
|
|
||||||
|
// Show the dummy content as text in a TextView.
|
||||||
|
item?.let { item ->
|
||||||
|
subject.text = item.subject
|
||||||
|
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||||
|
messages.adapter = ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel)
|
||||||
|
messages.layoutManager = LinearLayoutManager(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.conversation, menu)
|
||||||
|
activity?.let { activity ->
|
||||||
|
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||||
|
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||||
|
}
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
val messageRepo = Singleton.getMessageRepository(
|
||||||
|
context ?: throw IllegalStateException("No context available")
|
||||||
|
)
|
||||||
|
item?.let { item ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.delete -> {
|
||||||
|
item.messages.forEach {
|
||||||
|
Singleton.labeler.delete(it)
|
||||||
|
messageRepo.remove(it)
|
||||||
|
}
|
||||||
|
MainActivity.apply { updateUnread() }
|
||||||
|
activity?.onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.archive -> {
|
||||||
|
item.messages.forEach {
|
||||||
|
Singleton.labeler.archive(it)
|
||||||
|
messageRepo.save(it)
|
||||||
|
}
|
||||||
|
MainActivity.apply { updateUnread() }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* The fragment argument representing the item ID that this fragment
|
||||||
|
* represents.
|
||||||
|
*/
|
||||||
|
const val ARG_ITEM_ID = "item_id"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.apps.abit
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.*
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
||||||
|
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||||
|
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||||
|
import ch.dissem.apps.abit.adapter.EventListener
|
||||||
|
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
|
||||||
|
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
|
||||||
|
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||||
|
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||||
|
import ch.dissem.apps.abit.service.Singleton
|
||||||
|
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||||
|
import ch.dissem.apps.abit.util.preferences
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||||
|
import ch.dissem.bitmessage.utils.ConversationService
|
||||||
|
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||||
|
import org.jetbrains.anko.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 15
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list fragment representing a list of Messages. This fragment
|
||||||
|
* also supports tablet devices by allowing list items to be given an
|
||||||
|
* 'activated' state upon selection. This helps indicate which item is
|
||||||
|
* currently being viewed in a [MessageDetailFragment].
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||||
|
* interface.
|
||||||
|
*/
|
||||||
|
class ConversationListFragment : Fragment(), ListHolder<Label> {
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
private var isLastPage = false
|
||||||
|
|
||||||
|
private var layoutManager: LinearLayoutManager? = null
|
||||||
|
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
|
||||||
|
|
||||||
|
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
layoutManager?.let { layoutManager ->
|
||||||
|
val visibleItemCount = layoutManager.childCount
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
|
||||||
|
if (!isLoading && !isLastPage) {
|
||||||
|
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||||
|
&& firstVisibleItemPosition >= 0
|
||||||
|
) {
|
||||||
|
loadMoreItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyTrashMenuItem: MenuItem? = null
|
||||||
|
private var deleteAllMenuItem: MenuItem? = null
|
||||||
|
private lateinit var messageRepo: AndroidMessageRepository
|
||||||
|
private lateinit var conversationService: ConversationService
|
||||||
|
private var activateOnItemClick: Boolean = false
|
||||||
|
|
||||||
|
private var subscription: Disposable? = null
|
||||||
|
|
||||||
|
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||||
|
swipeableConversationAdapter?.activateOnItemClick = activateOnItemClick
|
||||||
|
this.activateOnItemClick = activateOnItemClick
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backStack = Stack<Label>()
|
||||||
|
|
||||||
|
fun loadMoreItems() {
|
||||||
|
isLoading = true
|
||||||
|
swipeableConversationAdapter?.let { messageAdapter ->
|
||||||
|
doAsync {
|
||||||
|
val conversationIds = messageRepo.findConversations(
|
||||||
|
currentLabel.value,
|
||||||
|
messageAdapter.itemCount,
|
||||||
|
PAGE_SIZE,
|
||||||
|
context?.preferences?.separateIdentities == true
|
||||||
|
)
|
||||||
|
conversationIds.forEach { conversationId ->
|
||||||
|
val conversation = conversationService.getConversation(conversationId)
|
||||||
|
uiThread {
|
||||||
|
messageAdapter.add(conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
isLastPage = conversationIds.size < PAGE_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val activity = activity as MainActivity
|
||||||
|
initFab(activity)
|
||||||
|
messageRepo = Singleton.getMessageRepository(activity)
|
||||||
|
conversationService = Singleton.getConversationService(activity)
|
||||||
|
|
||||||
|
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
|
||||||
|
doUpdateList(currentLabel.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
subscription?.dispose()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reloadList() = doUpdateList(currentLabel.value)
|
||||||
|
|
||||||
|
private fun doUpdateList(label: Label?) {
|
||||||
|
val mainActivity = activity as? MainActivity
|
||||||
|
swipeableConversationAdapter?.clear(label)
|
||||||
|
if (label == null) {
|
||||||
|
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||||
|
swipeableConversationAdapter?.notifyDataSetChanged()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||||
|
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
|
||||||
|
deleteAllMenuItem?.isVisible = false
|
||||||
|
|
||||||
|
MainActivity.apply {
|
||||||
|
if ("archive" == label.toString()) {
|
||||||
|
updateTitle(getString(R.string.archive))
|
||||||
|
} else {
|
||||||
|
updateTitle(label.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View =
|
||||||
|
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val context = context ?: throw IllegalStateException("No context available")
|
||||||
|
|
||||||
|
val listener = object : EventListener {
|
||||||
|
override fun onItemDeleted(position: Int) {
|
||||||
|
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||||
|
item.messages.forEach {
|
||||||
|
Singleton.labeler.delete(it)
|
||||||
|
messageRepo.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swipeableConversationAdapter?.removeAt(position)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemArchived(position: Int) {
|
||||||
|
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||||
|
item.messages.forEach {
|
||||||
|
Singleton.labeler.archive(it)
|
||||||
|
messageRepo.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swipeableConversationAdapter?.removeAt(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemSelected(position: Int) {
|
||||||
|
swipeableConversationAdapter?.selectedPosition = position
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||||
|
MainActivity.apply { onItemSelected(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
|
recycler_view.layoutManager = layoutManager
|
||||||
|
swipeableConversationAdapter = SwipeableConversationAdapter(context).apply {
|
||||||
|
activateOnItemClick = this@ConversationListFragment.activateOnItemClick
|
||||||
|
eventListener = listener
|
||||||
|
}
|
||||||
|
recycler_view.adapter = swipeableConversationAdapter
|
||||||
|
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||||
|
|
||||||
|
val dirs = when (currentLabel.value?.type) {
|
||||||
|
Label.Type.TRASH -> ItemTouchHelper.LEFT
|
||||||
|
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
val swipeHandler = SwipeToDeleteCallback(context, dirs, listener)
|
||||||
|
|
||||||
|
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||||
|
itemTouchHelper.attachToRecyclerView(recycler_view)
|
||||||
|
|
||||||
|
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initFab(context: MainActivity) {
|
||||||
|
val menu = FabSpeedDialMenu(context)
|
||||||
|
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||||
|
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||||
|
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||||
|
.addOnMenuItemClickListener { _, _, itemId ->
|
||||||
|
val identity = Singleton.getIdentity(context)
|
||||||
|
if (identity == null) {
|
||||||
|
Toast.makeText(
|
||||||
|
activity, R.string.no_identity_warning,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
when (itemId) {
|
||||||
|
1 -> {
|
||||||
|
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||||
|
intent.putExtra(EXTRA_BROADCAST, true)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
swipeableConversationAdapter = null
|
||||||
|
layoutManager = null
|
||||||
|
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.message_list, menu)
|
||||||
|
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||||
|
deleteAllMenuItem = menu.findItem(R.id.delete_all)
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.empty_trash -> {
|
||||||
|
currentLabel.value?.let { label ->
|
||||||
|
if (label.type != Label.Type.TRASH) return true
|
||||||
|
|
||||||
|
deleteAllMessages(label)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.delete_all -> {
|
||||||
|
currentLabel.value?.let { label ->
|
||||||
|
context?.apply {
|
||||||
|
alert(
|
||||||
|
R.string.delete_all_messages_in_list,
|
||||||
|
R.string.delete_all_messages_in_list_ask
|
||||||
|
) {
|
||||||
|
positiveButton(R.string.delete) {
|
||||||
|
deleteAllMessages(label)
|
||||||
|
}
|
||||||
|
cancelButton { }
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAllMessages(label: Label) {
|
||||||
|
doAsync {
|
||||||
|
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
|
||||||
|
messageRepo.remove(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiThread { doUpdateList(label) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateList(label: Label) {
|
||||||
|
currentLabel.onNext(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
currentLabel.onNext(backStack.pop())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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.*
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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.*
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -142,5 +142,4 @@ class ContactAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,162 @@
|
|||||||
|
package ch.dissem.apps.abit.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.util.Linkify
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ch.dissem.apps.abit.*
|
||||||
|
import ch.dissem.apps.abit.service.Singleton
|
||||||
|
import ch.dissem.apps.abit.util.Constants
|
||||||
|
import ch.dissem.apps.abit.util.getDrawable
|
||||||
|
import ch.dissem.bitmessage.entity.Conversation
|
||||||
|
import ch.dissem.bitmessage.entity.Plaintext
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||||
|
import ch.dissem.bitmessage.ports.MessageRepository
|
||||||
|
import io.reactivex.subjects.BehaviorSubject
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationAdapter internal constructor(
|
||||||
|
ctx: Context,
|
||||||
|
private val parent: Fragment,
|
||||||
|
conversation: Conversation,
|
||||||
|
label: BehaviorSubject<Label>
|
||||||
|
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val messageRepo = Singleton.getMessageRepository(ctx)
|
||||||
|
|
||||||
|
private var filteredMessages = label.value?.let { l -> conversation.messages.filter { m -> m.labels.any { it == l } } } ?: emptyList()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): ConversationAdapter.ViewHolder {
|
||||||
|
val context = parent.context
|
||||||
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
// Inflate the custom layout
|
||||||
|
val messageView = inflater.inflate(R.layout.item_message_detail, parent, false)
|
||||||
|
|
||||||
|
// Return a new holder instance
|
||||||
|
return ViewHolder(messageView, this.parent, messageRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Involves populating data into the item through holder
|
||||||
|
override fun onBindViewHolder(viewHolder: ConversationAdapter.ViewHolder, position: Int) {
|
||||||
|
// Get the data model based on position
|
||||||
|
val message = filteredMessages[position]
|
||||||
|
|
||||||
|
viewHolder.apply {
|
||||||
|
item = message
|
||||||
|
avatar.setImageDrawable(Identicon(message.from))
|
||||||
|
sender.text = message.from.toString()
|
||||||
|
val senderClickListener: (View) -> Unit = {
|
||||||
|
MainActivity.apply {
|
||||||
|
onItemSelected(message.from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avatar.setOnClickListener(senderClickListener)
|
||||||
|
sender.setOnClickListener(senderClickListener)
|
||||||
|
|
||||||
|
recipient.text = message.to.toString()
|
||||||
|
status.setImageResource(message.status.getDrawable())
|
||||||
|
text.text = message.text
|
||||||
|
|
||||||
|
Linkify.addLinks(text, Linkify.WEB_URLS)
|
||||||
|
Linkify.addLinks(text,
|
||||||
|
Constants.BITMESSAGE_ADDRESS_PATTERN,
|
||||||
|
Constants.BITMESSAGE_URL_SCHEMA, null,
|
||||||
|
Linkify.TransformFilter { match, _ -> match.group() }
|
||||||
|
)
|
||||||
|
|
||||||
|
labelAdapter.labels = message.labels.toList()
|
||||||
|
|
||||||
|
// FIXME: I think that's not quite correct
|
||||||
|
if (message.isUnread()) {
|
||||||
|
Singleton.labeler.markAsRead(message)
|
||||||
|
messageRepo.save(message)
|
||||||
|
MainActivity.apply { updateUnread() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = filteredMessages.size
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
itemView: View,
|
||||||
|
parent: Fragment,
|
||||||
|
messageRepo: MessageRepository
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var item: Plaintext? = null
|
||||||
|
val avatar = itemView.findViewById<ImageView>(R.id.avatar)!!
|
||||||
|
val sender = itemView.findViewById<TextView>(R.id.sender)!!
|
||||||
|
val recipient = itemView.findViewById<TextView>(R.id.recipient)!!
|
||||||
|
val status = itemView.findViewById<ImageView>(R.id.status)!!
|
||||||
|
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.apply {
|
||||||
|
setOnClickListener { view ->
|
||||||
|
PopupMenu(itemView.context, view).apply {
|
||||||
|
|
||||||
|
menuInflater.inflate(R.menu.message, menu)
|
||||||
|
setOnMenuItemClickListener { menuItem ->
|
||||||
|
item?.let { item ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.reply -> {
|
||||||
|
ComposeMessageActivity.launchReplyTo(parent, item)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.delete -> {
|
||||||
|
if (MessageDetailFragment.isInTrash(item)) {
|
||||||
|
Singleton.labeler.delete(item)
|
||||||
|
messageRepo.remove(item)
|
||||||
|
} else {
|
||||||
|
Singleton.labeler.delete(item)
|
||||||
|
messageRepo.save(item)
|
||||||
|
}
|
||||||
|
filteredMessages.indexOf(item).let { i ->
|
||||||
|
filteredMessages -= item
|
||||||
|
notifyItemRemoved(i)
|
||||||
|
}
|
||||||
|
MainActivity.apply {
|
||||||
|
updateUnread()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.mark_unread -> {
|
||||||
|
Singleton.labeler.markAsUnread(item)
|
||||||
|
messageRepo.save(item)
|
||||||
|
MainActivity.apply { updateUnread() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.archive -> {
|
||||||
|
Singleton.labeler.archive(item)
|
||||||
|
messageRepo.save(item)
|
||||||
|
MainActivity.apply { updateUnread() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
|
||||||
|
linksClickable = true
|
||||||
|
setTextIsSelectable(true)
|
||||||
|
}
|
||||||
|
val labelAdapter = LabelAdapter(itemView.context, emptySet())
|
||||||
|
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
|
||||||
|
adapter = labelAdapter
|
||||||
|
layoutManager = GridLayoutManager(itemView.context, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package ch.dissem.apps.abit.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import ch.dissem.apps.abit.R
|
||||||
|
import ch.dissem.apps.abit.util.getColor
|
||||||
|
import ch.dissem.apps.abit.util.getIcon
|
||||||
|
import ch.dissem.apps.abit.util.getText
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||||
|
import com.mikepenz.iconics.view.IconicsImageView
|
||||||
|
|
||||||
|
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
|
||||||
|
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
var labels = labels.toList()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||||
|
val context = parent.context
|
||||||
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
// Inflate the custom layout
|
||||||
|
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||||
|
|
||||||
|
// Return a new holder instance
|
||||||
|
return ViewHolder(contactView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Involves populating data into the item through holder
|
||||||
|
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||||
|
// Get the data model based on position
|
||||||
|
val label = labels[position]
|
||||||
|
|
||||||
|
viewHolder.icon.icon?.icon(label.getIcon())
|
||||||
|
viewHolder.label.text = label.getText(ctx)
|
||||||
|
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = labels.size
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||||
|
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||||
|
|
||||||
|
fun setBackground(@ColorInt color: Int) {
|
||||||
|
itemView.backgroundTintList = ColorStateList.valueOf(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,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)
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) }
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
package ch.dissem.apps.abit.service
|
||||||
|
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobService
|
||||||
|
import org.jetbrains.anko.doAsync
|
||||||
|
|
||||||
|
class CleanupService : JobService() {
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters?): Boolean {
|
||||||
|
doAsync {
|
||||||
|
Singleton.getBitmessageContext(this@CleanupService).cleanup()
|
||||||
|
jobFinished(params, false)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopJob(params: JobParameters?) = false
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package ch.dissem.apps.abit.service
|
||||||
|
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobService
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import ch.dissem.apps.abit.notification.NetworkNotification
|
||||||
|
import ch.dissem.apps.abit.util.network
|
||||||
|
import ch.dissem.apps.abit.util.preferences
|
||||||
|
import ch.dissem.bitmessage.BitmessageContext
|
||||||
|
import ch.dissem.bitmessage.utils.Property
|
||||||
|
import org.jetbrains.anko.doAsync
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the full node if
|
||||||
|
* * it is active
|
||||||
|
* * it is not already running
|
||||||
|
*
|
||||||
|
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
|
||||||
|
*/
|
||||||
|
class NodeStartupService : JobService() {
|
||||||
|
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
|
||||||
|
|
||||||
|
private lateinit var notification: NetworkNotification
|
||||||
|
|
||||||
|
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (bmc.isRunning() && !preferences.connectionAllowed) {
|
||||||
|
bmc.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notification = NetworkNotification(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters?): Boolean {
|
||||||
|
if (preferences.online) {
|
||||||
|
registerReceiver(
|
||||||
|
connectivityReceiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||||
|
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
startForeground(0, notification.notification)
|
||||||
|
NodeStartupService.running = false
|
||||||
|
|
||||||
|
if (!isRunning) {
|
||||||
|
running = true
|
||||||
|
notification.connecting()
|
||||||
|
if (!bmc.isRunning()) {
|
||||||
|
bmc.startup()
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (bmc.isRunning()) {
|
||||||
|
bmc.shutdown()
|
||||||
|
}
|
||||||
|
running = false
|
||||||
|
notification.showShutdown()
|
||||||
|
doAsync {
|
||||||
|
bmc.cleanup()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(connectivityReceiver)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
// For some reason, onStartJob wasn't called so the receiver isn't registered.
|
||||||
|
// Let's just ignore this.
|
||||||
|
}
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
|
||||||
|
* depending on Android version.
|
||||||
|
*/
|
||||||
|
override fun onStopJob(params: JobParameters?): Boolean {
|
||||||
|
network.scheduleNodeStart()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var running = false
|
||||||
|
|
||||||
|
val isRunning: Boolean
|
||||||
|
get() = running && Singleton.bitmessageContext?.isRunning() == true
|
||||||
|
|
||||||
|
val status: Property
|
||||||
|
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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 {
|
||||||
@ -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()
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
@ -57,8 +54,7 @@ object Assets {
|
|||||||
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
|
||||||
@ -67,4 +63,3 @@ object Assets {
|
|||||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -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:"
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,4 +101,3 @@ object Drawables {
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
@ -27,7 +25,7 @@ object Labels {
|
|||||||
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
|
||||||
@ -39,7 +37,6 @@ object Labels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun getColor(label: Label) = if (label.type == null) {
|
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
|
||||||
label.color
|
color
|
||||||
} else 0xFF000000.toInt()
|
} else default
|
||||||
}
|
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
6
app/src/main/res/drawable/bg_label.xml
Normal file
6
app/src/main/res/drawable/bg_label.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
13
app/src/main/res/drawable/ic_battery_charging.xml
Normal file
13
app/src/main/res/drawable/ic_battery_charging.xml
Normal 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>
|
8
app/src/main/res/drawable/ic_broom.xml
Normal file
8
app/src/main/res/drawable/ic_broom.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_bug_report.xml
Normal file
9
app/src/main/res/drawable/ic_bug_report.xml
Normal 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>
|
8
app/src/main/res/drawable/ic_check_all.xml
Normal file
8
app/src/main/res/drawable/ic_check_all.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_emulate_conversations.xml
Normal file
9
app/src/main/res/drawable/ic_emulate_conversations.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_export.xml
Normal file
9
app/src/main/res/drawable/ic_export.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_import.xml
Normal file
9
app/src/main/res/drawable/ic_import.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_info.xml
Normal file
9
app/src/main/res/drawable/ic_info.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_menu.xml
Normal file
9
app/src/main/res/drawable/ic_menu.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_network_wifi.xml
Normal file
13
app/src/main/res/drawable/ic_network_wifi.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_notification_batch.xml
Normal file
10
app/src/main/res/drawable/ic_notification_batch.xml
Normal 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>
|
@ -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>
|
@ -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>
|
9
app/src/main/res/drawable/ic_port.xml
Normal file
9
app/src/main/res/drawable/ic_port.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_separate_identities.xml
Normal file
9
app/src/main/res/drawable/ic_separate_identities.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_support_app.xml
Normal file
9
app/src/main/res/drawable/ic_support_app.xml
Normal 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
Loading…
Reference in New Issue
Block a user