Compare commits
147 Commits
1.0-beta12
...
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 | |||
06e99ea0cd | |||
ad3af929d7 | |||
16f1dfa6f6 | |||
2bddd0f256 | |||
ab70e6df12 | |||
fb72356467 | |||
e3c7c4d557 | |||
938bfc206e | |||
b4b1d25f99 | |||
3509082d30 | |||
9e59187ae0 | |||
c1af65732a | |||
61e0a12a50 | |||
c254c1bacd | |||
4f1ef4407c | |||
46bbd59712 | |||
8d876719c4 | |||
ec645d70ce | |||
7f0d8828d1 | |||
d98b800249 | |||
c750e2004a | |||
9eefbad7d6 | |||
21abdbd720 | |||
39ad5e8baf | |||
1da0674857 | |||
f481914a65 | |||
99b2d1903e | |||
d611bd13bc | |||
66c8536a84 | |||
72213a53a5 | |||
8a668f4af2 | |||
6829614da0 | |||
1906a2e13c | |||
708529fc0a | |||
b5dbbeb46a | |||
b368c8251d | |||
6986d9a2df | |||
35249a0145 | |||
2a1aa736cc | |||
9f26ade617 | |||
396f1a23a6 | |||
db939dab9c | |||
30d9d72133 | |||
df581f4c51 | |||
4c89bfe1cf | |||
d88d3c900e | |||
f6ebd62c8d | |||
760e423b9b | |||
f45f6c3919 | |||
dd22caaa50 | |||
49d87c3c75 | |||
c7dbe660b9 | |||
b825b33250 | |||
33e932e630 | |||
072f732924 | |||
f58a22dadb | |||
1329aecde4 | |||
f27f438998 | |||
c1d74e4781 | |||
34e2e0673b | |||
f4b6bcb8d9 | |||
1e8b71e43b | |||
9dd1b457e3 | |||
287de9deb5 | |||
696cd6c0a6 | |||
a23ae14f1d | |||
415107a6c8 | |||
cc18f34161 | |||
852e38b97d | |||
858651e808 | |||
625848bd9d | |||
4622ad68f0 | |||
d05f1f98e5 | |||
973e4a0dca | |||
3a98cc115a | |||
e2aa0e8b1d | |||
ec3009a257 | |||
e79bfdb244 | |||
898c49802b | |||
e064012551 | |||
faa6752b10 | |||
593a390b40 | |||
ccfeb5b479 | |||
8057980f6c | |||
8af8419b7c | |||
433c757107 | |||
1c284eba26 | |||
bf52d2f3de | |||
a67560c28b | |||
bf070da20a | |||
73944b5883 | |||
263fc8893f | |||
6540df4fc9 | |||
422c7ac803 | |||
5bc1bc2a47 | |||
c7200d06bc | |||
3bdf1bd6bf |
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.
|
122
app/build.gradle
122
app/build.gradle
@ -1,32 +1,39 @@
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'idea'
|
||||
|
||||
ext {
|
||||
appName = "Abit"
|
||||
}
|
||||
if (project.hasProperty("project.configs")
|
||||
&& new File(project.property("project.configs") + appName + ".gradle").exists()) {
|
||||
apply from: project.property("project.configs") + appName + ".gradle";
|
||||
apply from: project.property("project.configs") + appName + ".gradle"
|
||||
}
|
||||
|
||||
//noinspection GroovyMissingReturnStatement
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "ch.dissem.apps." + appName.toLowerCase()
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 25
|
||||
versionCode 12
|
||||
versionName "1.0-beta12"
|
||||
jackOptions.enabled = false
|
||||
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 23
|
||||
versionName "1.0-rc1"
|
||||
multiDexEnabled true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
@ -35,60 +42,83 @@ android {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/core.kotlin_module'
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ext.jabitVersion = '2.0.4'
|
||||
ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT'
|
||||
ext.supportVersion = '25.3.1'
|
||||
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
|
||||
ext.supportVersion = '27.1.1'
|
||||
dependencies {
|
||||
compile 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-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
||||
|
||||
compile "com.android.support:appcompat-v7:$supportVersion"
|
||||
compile "com.android.support:support-v4:$supportVersion"
|
||||
compile "com.android.support:design:$supportVersion"
|
||||
compile "com.android.support:multidex:1.0.1"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.0.0'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.0.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.0.0'
|
||||
|
||||
compile "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
testImplementation "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
|
||||
|
||||
compile 'org.slf4j:slf4j-android:1.7.25'
|
||||
implementation 'org.slf4j:slf4j-android:1.7.25'
|
||||
|
||||
compile 'com.mikepenz:materialize:1.0.1@aar'
|
||||
compile('com.mikepenz:materialdrawer:5.9.0@aar') {
|
||||
implementation 'com.mikepenz:materialize:1.2.0-rc01@aar'
|
||||
implementation('com.mikepenz:materialdrawer:6.1.0-rc01.2@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile('com.mikepenz:aboutlibraries:5.9.5@aar') {
|
||||
implementation('com.mikepenz:aboutlibraries:6.2.0-rc01@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile "com.mikepenz:iconics-core:2.8.3@aar"
|
||||
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
|
||||
compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar'
|
||||
implementation "com.mikepenz:iconics-core:3.1.0-rc01@aar"
|
||||
implementation "com.mikepenz:iconics-views:3.1.0-rc01@aar"
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
||||
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
|
||||
|
||||
compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
|
||||
compile 'com.google.zxing:core:3.3.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
||||
compile 'io.github.yavski:fab-speed-dial:1.0.6'
|
||||
compile 'com.github.amlcurran.showcaseview:library:5.4.3'
|
||||
compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile 'com.github.angads25:filepicker:1.1.0'
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
|
||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
|
||||
implementation 'com.github.angads25:filepicker:1.1.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:2.7.22'
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
|
||||
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.19.0'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:1.3'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation 'org.robolectric:robolectric:3.7.1'
|
||||
testImplementation "org.robolectric:shadows-multidex:3.7.1"
|
||||
|
||||
androidTestImplementation "androidx.multidex:multidex:2.0.0"
|
||||
}
|
||||
|
||||
idea.module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
|
||||
android {
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="ch.dissem.apps.abit"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="ch.dissem.apps.abit">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
tools:replace="android:allowBackup">
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@ -37,7 +36,7 @@
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AddressDetailActivity"
|
||||
@ -46,52 +45,42 @@
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".dialog.FullNodeDialogActivity"
|
||||
android:label="@string/full_node"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog"/>
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog" />
|
||||
<activity
|
||||
android:name=".ComposeMessageActivity"
|
||||
android:label="@string/compose_message"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
|
||||
<data android:scheme="bitmessage"/>
|
||||
<data android:scheme="bitmsg"/>
|
||||
<data android:scheme="bm"/>
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@ -99,14 +88,14 @@
|
||||
android:label="@string/title_activity_open_bitmessage_link"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="bitmessage"/>
|
||||
<data android:scheme="bitmsg"/>
|
||||
<data android:scheme="bm"/>
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@ -115,77 +104,69 @@
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data
|
||||
android:host="*"
|
||||
android:mimeType="*/*"
|
||||
android:pathPattern=".*\\.dat"
|
||||
android:scheme="file"/>
|
||||
android:scheme="file" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.BitmessageService"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".service.ProofOfWorkService"
|
||||
android:exported="false"/>
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Synchronization -->
|
||||
<!-- Exports -->
|
||||
<provider
|
||||
android:name=".synchronization.StubProvider"
|
||||
android:authorities="ch.dissem.apps.abit.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="ch.dissem.apps.abit.fileprovider"
|
||||
android:exported="false"
|
||||
android:syncable="true"/>
|
||||
|
||||
<service
|
||||
android:name=".synchronization.AuthenticatorService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
|
||||
android:grantUriPermissions="true">
|
||||
<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>
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.BitmessageIntentService"
|
||||
android:exported="false"/>
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Receive Wi-Fi connection state changes -->
|
||||
<receiver android:name=".listener.WifiReceiver">
|
||||
<receiver
|
||||
android:name=".service.StartServiceReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".service.NodeStartupService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.CleanupService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.BatchProcessorService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
android:parentActivityName=".SettingsActivity">
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".SettingsActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder {
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
*/
|
||||
private static final String STATE_ACTIVATED_POSITION = "activated_position";
|
||||
/**
|
||||
* A dummy implementation of the {@link ListSelectionListener} interface that does
|
||||
* nothing. Used only when this fragment is not attached to an activity.
|
||||
*/
|
||||
private static final ListSelectionListener<Object> dummyCallbacks =
|
||||
new ListSelectionListener<Object>() {
|
||||
@Override
|
||||
public void onItemSelected(Object item) {
|
||||
// NO OP
|
||||
}
|
||||
};
|
||||
/**
|
||||
* The fragment's current callback object, which is notified of list item
|
||||
* clicks.
|
||||
*/
|
||||
private ListSelectionListener<? super T> callbacks = dummyCallbacks;
|
||||
/**
|
||||
* The current activated item position. Only used on tablets.
|
||||
*/
|
||||
private int activatedPosition = ListView.INVALID_POSITION;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
? ListView.CHOICE_MODE_SINGLE
|
||||
: ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (context instanceof ListSelectionListener) {
|
||||
//noinspection unchecked
|
||||
callbacks = (ListSelectionListener) context;
|
||||
} else {
|
||||
throw new IllegalStateException("Activity must implement fragment's callbacks.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
|
||||
// Reset the active callbacks interface to the dummy implementation.
|
||||
callbacks = dummyCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView listView, View view, int position, long id) {
|
||||
super.onListItemClick(listView, view, position, id);
|
||||
|
||||
// Notify the active callbacks interface (the activity, if the
|
||||
// fragment is attached to one) that an item has been selected.
|
||||
//noinspection unchecked
|
||||
callbacks.onItemSelected((T) listView.getItemAtPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (activatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
|
||||
if (isVisible()) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
? ListView.CHOICE_MODE_SINGLE
|
||||
: ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setActivatedPosition(int position) {
|
||||
if (position == ListView.INVALID_POSITION) {
|
||||
getListView().setItemChecked(activatedPosition, false);
|
||||
} else {
|
||||
getListView().setItemChecked(position, true);
|
||||
}
|
||||
|
||||
activatedPosition = position;
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.ListFragment
|
||||
import android.view.View
|
||||
import android.widget.ListView
|
||||
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class AbstractItemListFragment<in L, T> : ListFragment(), ListHolder<L> {
|
||||
/**
|
||||
* The fragment's current callback object, which is notified of list item
|
||||
* clicks.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T>
|
||||
/**
|
||||
* The current activated item position. Only used on tablets.
|
||||
*/
|
||||
private var activatedPosition = ListView.INVALID_POSITION
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (context is ListSelectionListener<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = context as ListSelectionListener<T>
|
||||
} else {
|
||||
throw IllegalStateException("Activity must implement fragment's callbacks.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
// Reset the active callbacks interface to the dummy implementation.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = DummyCallback as ListSelectionListener<T>
|
||||
}
|
||||
|
||||
override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) {
|
||||
super.onListItemClick(listView, view, position, id)
|
||||
|
||||
// Notify the active callbacks interface (the activity, if the
|
||||
// fragment is attached to one) that an item has been selected.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(listView.getItemAtPosition(position) as? T)?.let {
|
||||
callbacks.onItemSelected(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (activatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
|
||||
if (isVisible) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActivatedPosition(position: Int) {
|
||||
if (position == ListView.INVALID_POSITION) {
|
||||
listView.setItemChecked(activatedPosition, false)
|
||||
} else {
|
||||
listView.setItemChecked(position, true)
|
||||
}
|
||||
|
||||
activatedPosition = position
|
||||
}
|
||||
|
||||
override fun showPreviousList() = false
|
||||
|
||||
/**
|
||||
* A dummy implementation of the [ListSelectionListener] interface that does
|
||||
* nothing. Used only when this fragment is not attached to an activity.
|
||||
*/
|
||||
internal object DummyCallback : ListSelectionListener<Any> {
|
||||
override fun onItemSelected(item: Any) = Unit // NO OP
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
*/
|
||||
internal const val STATE_ACTIVATED_POSITION = "activated_position"
|
||||
}
|
||||
}
|
@ -14,25 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Bundle
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Subscription detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a {@link MainActivity}.
|
||||
* <p/>
|
||||
* in a [MainActivity].
|
||||
*
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a {@link AddressDetailFragment}.
|
||||
* more than a [AddressDetailFragment].
|
||||
*/
|
||||
public class AddressDetailActivity extends DetailActivity {
|
||||
class AddressDetailActivity : DetailActivity() {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
@ -42,18 +42,18 @@ public class AddressDetailActivity extends DetailActivity {
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
|
||||
getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM));
|
||||
AddressDetailFragment fragment = new AddressDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM))
|
||||
val fragment = AddressDetailFragment()
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit();
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Drawables;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.wif.WifExporter;
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a {@link MainActivity}
|
||||
* in two-pane mode (on tablets) or a {@link MessageDetailActivity}
|
||||
* on handsets.
|
||||
*/
|
||||
public class AddressDetailFragment extends Fragment {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM = "item";
|
||||
public static final String EXPORT_POSTFIX = ".keys.dat";
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private BitmessageAddress item;
|
||||
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public AddressDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments().containsKey(ARG_ITEM)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
item = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.address, menu);
|
||||
|
||||
FragmentActivity activity = getActivity();
|
||||
Drawables.addIcon(activity, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail);
|
||||
Drawables.addIcon(activity, menu, R.id.share, GoogleMaterial.Icon.gmd_share);
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete);
|
||||
Drawables.addIcon(activity, menu, R.id.export,
|
||||
CommunityMaterial.Icon.cmd_export)
|
||||
.setVisible(item != null && item.getPrivateKey() != null);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
final Activity ctx = getActivity();
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.write_message: {
|
||||
BitmessageAddress identity = Singleton.getIdentity(ctx);
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Intent intent = new Intent(ctx, ComposeMessageActivity.class);
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity);
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item);
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.delete: {
|
||||
int warning;
|
||||
if (item.getPrivateKey() != null)
|
||||
warning = R.string.delete_identity_warning;
|
||||
else
|
||||
warning = R.string.delete_contact_warning;
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setMessage(warning)
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Singleton.getAddressRepository(ctx).remove(item);
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (item.getPrivateKey() != null && mainActivity != null) {
|
||||
mainActivity.removeIdentityEntry(item);
|
||||
}
|
||||
item = null;
|
||||
ctx.onBackPressed();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
case R.id.export: {
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setMessage(R.string.confirm_export)
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TITLE, item +
|
||||
EXPORT_POSTFIX);
|
||||
WifExporter exporter = new WifExporter(Singleton
|
||||
.getBitmessageContext(ctx));
|
||||
exporter.addIdentity(item);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString
|
||||
());
|
||||
startActivity(Intent.createChooser(shareIntent, null));
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
case R.id.share: {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress());
|
||||
startActivity(Intent.createChooser(shareIntent, null));
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_address_detail, container, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
if (item != null) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (item.isChan()) {
|
||||
activity.setTitle(R.string.title_chan_detail);
|
||||
} else if (item.getPrivateKey() != null) {
|
||||
activity.setTitle(R.string.title_identity_detail);
|
||||
} else if (item.isSubscribed()) {
|
||||
activity.setTitle(R.string.title_subscription_detail);
|
||||
} else {
|
||||
activity.setTitle(R.string.title_contact_detail);
|
||||
}
|
||||
|
||||
((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item));
|
||||
TextView name = (TextView) rootView.findViewById(R.id.name);
|
||||
name.setText(item.toString());
|
||||
name.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
item.setAlias(s.toString());
|
||||
}
|
||||
});
|
||||
TextView address = (TextView) rootView.findViewById(R.id.address);
|
||||
address.setText(item.getAddress());
|
||||
address.setSelected(true);
|
||||
((TextView) rootView.findViewById(R.id.stream_number)).setText(
|
||||
getString(R.string.stream_number, item.getStream()));
|
||||
if (item.getPrivateKey() == null) {
|
||||
Switch active = (Switch) rootView.findViewById(R.id.active);
|
||||
active.setChecked(item.isSubscribed());
|
||||
active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton button, boolean checked) {
|
||||
item.setSubscribed(checked);
|
||||
}
|
||||
});
|
||||
|
||||
ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id
|
||||
.pubkey_available);
|
||||
|
||||
if (item.getPubkey() == null) {
|
||||
pubkeyAvailableImg.setAlpha(0.3f);
|
||||
TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id
|
||||
.pubkey_available_desc);
|
||||
pubkeyAvailableDesc.setText(R.string.pubkey_not_available);
|
||||
}
|
||||
} else {
|
||||
rootView.findViewById(R.id.active).setVisibility(View.GONE);
|
||||
rootView.findViewById(R.id.pubkey_available).setVisibility(View.GONE);
|
||||
rootView.findViewById(R.id.pubkey_available_desc).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// QR code
|
||||
ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code);
|
||||
qrCode.setImageBitmap(Drawables.qrCode(item));
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (item != null) {
|
||||
Singleton.getAddressRepository(getContext()).save(item);
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null && item.getPrivateKey() != null) {
|
||||
mainActivity.updateIdentityEntry(item);
|
||||
}
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
}
|
211
app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt
Normal file
211
app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.wif.WifExporter
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_address_detail.*
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class AddressDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: BitmessageAddress? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.address, menu)
|
||||
|
||||
val ctx = activity!!
|
||||
Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail)
|
||||
Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share)
|
||||
Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val item = item ?: return false
|
||||
val ctx = activity ?: return false
|
||||
when (menuItem.itemId) {
|
||||
R.id.write_message -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
val intent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
val warning = if (item.privateKey != null)
|
||||
R.string.delete_identity_warning
|
||||
else
|
||||
R.string.delete_contact_warning
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(warning)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
Singleton.getAddressRepository(ctx).remove(item)
|
||||
MainActivity.apply {
|
||||
if (item.privateKey != null) {
|
||||
removeIdentityEntry(item)
|
||||
}
|
||||
}
|
||||
this.item = null
|
||||
ctx.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.export -> {
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(R.string.confirm_export)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"$item$EXPORT_POSTFIX"
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
WifExporter(Singleton.getBitmessageContext(ctx)).apply {
|
||||
addIdentity(item)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.share -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, item.address)
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
|
||||
= inflater.inflate(R.layout.fragment_address_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
activity?.let { activity ->
|
||||
when {
|
||||
item.isChan -> activity.setTitle(R.string.title_chan_detail)
|
||||
item.privateKey != null -> activity.setTitle(R.string.title_identity_detail)
|
||||
item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail)
|
||||
else -> activity.setTitle(R.string.title_contact_detail)
|
||||
}
|
||||
}
|
||||
|
||||
avatar.setImageDrawable(Identicon(item))
|
||||
name.setText(item.toString())
|
||||
name.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
item.alias = s.toString()
|
||||
}
|
||||
})
|
||||
address.text = item.address
|
||||
address.isSelected = true
|
||||
stream_number.text = getString(R.string.stream_number, item.stream)
|
||||
if (item.privateKey == null) {
|
||||
active.isChecked = item.isSubscribed
|
||||
active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked }
|
||||
|
||||
if (item.pubkey == null) {
|
||||
pubkey_available.alpha = 0.3f
|
||||
pubkey_available_desc.setText(R.string.pubkey_not_available)
|
||||
}
|
||||
} else {
|
||||
active.visibility = View.GONE
|
||||
pubkey_available.visibility = View.GONE
|
||||
pubkey_available_desc.visibility = View.GONE
|
||||
}
|
||||
|
||||
// QR code
|
||||
qr_code.setImageBitmap(item.qrCode())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
item?.let { item ->
|
||||
Singleton.getAddressRepository(context!!).save(item)
|
||||
if (item.privateKey != null) {
|
||||
MainActivity.apply { updateIdentityEntry(item) }
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
const val EXPORT_POSTFIX = ".keys.dat"
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import io.github.yavski.fabspeeddial.FabSpeedDial;
|
||||
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
|
||||
|
||||
/**
|
||||
* Fragment that shows a list of all contacts, the ones we subscribed to first.
|
||||
*/
|
||||
public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
updateList();
|
||||
}
|
||||
|
||||
public void updateList() {
|
||||
List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext())
|
||||
.getContacts();
|
||||
Collections.sort(addresses, new Comparator<BitmessageAddress>() {
|
||||
@Override
|
||||
public int compare(BitmessageAddress lhs, BitmessageAddress rhs) {
|
||||
// Yields the following order:
|
||||
// * Subscribed addresses come first
|
||||
// * Addresses with Aliases (alphabetically)
|
||||
// * Addresses (alphabetically)
|
||||
if (lhs.isSubscribed() == rhs.isSubscribed()) {
|
||||
if (lhs.getAlias() != null) {
|
||||
if (rhs.getAlias() != null) {
|
||||
return lhs.getAlias().compareTo(rhs.getAlias());
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (rhs.getAlias() != null) {
|
||||
return 1;
|
||||
} else {
|
||||
return lhs.getAddress().compareTo(rhs.getAddress());
|
||||
}
|
||||
}
|
||||
if (lhs.isSubscribed()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
setListAdapter(new ArrayAdapter<BitmessageAddress>(
|
||||
getActivity(),
|
||||
android.R.layout.simple_list_item_activated_1,
|
||||
android.R.id.text1,
|
||||
addresses) {
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
convertView = inflater.inflate(R.layout.subscription_row, parent, false);
|
||||
}
|
||||
BitmessageAddress item = getItem(position);
|
||||
assert item != null;
|
||||
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new
|
||||
Identicon(item));
|
||||
TextView name = (TextView) convertView.findViewById(R.id.name);
|
||||
name.setText(item.toString());
|
||||
TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number);
|
||||
streamNumber.setText(getContext().getString(R.string.stream_number,
|
||||
item.getStream()));
|
||||
convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ?
|
||||
View.VISIBLE : View.INVISIBLE);
|
||||
return convertView;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context ctx) {
|
||||
super.onAttach(ctx);
|
||||
if (ctx instanceof ActionBarListener) {
|
||||
((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_address_list, container, false);
|
||||
|
||||
FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact);
|
||||
fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() {
|
||||
@Override
|
||||
public boolean onMenuItemSelected(MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_read_qr_code:
|
||||
IntentIntegrator.forSupportFragment(AddressListFragment.this)
|
||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
|
||||
.initiateScan();
|
||||
return true;
|
||||
case R.id.action_create_contact:
|
||||
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && data.hasExtra("SCAN_RESULT")) {
|
||||
Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT"));
|
||||
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateList(Label label) {
|
||||
updateList();
|
||||
}
|
||||
}
|
150
app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt
Normal file
150
app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Fragment that shows a list of all contacts, the ones we subscribed to first.
|
||||
*/
|
||||
class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() {
|
||||
private lateinit var adapter: ArrayAdapter<BitmessageAddress>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = object : ArrayAdapter<BitmessageAddress>(
|
||||
activity!!,
|
||||
R.layout.subscription_row,
|
||||
R.id.name,
|
||||
LinkedList()
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val result: View
|
||||
val v: ViewHolder
|
||||
if (convertView == null) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.subscription_row, parent, false)
|
||||
v = ViewHolder(
|
||||
ctx = context,
|
||||
avatar = view.findViewById(R.id.avatar),
|
||||
name = view.findViewById(R.id.name),
|
||||
streamNumber = view.findViewById(R.id.stream_number),
|
||||
subscribed = view.findViewById(R.id.subscribed)
|
||||
)
|
||||
view.tag = v
|
||||
result = view
|
||||
} else {
|
||||
v = convertView.tag as ViewHolder
|
||||
result = convertView
|
||||
}
|
||||
getItem(position)?.let { item ->
|
||||
v.avatar.setImageDrawable(Identicon(item))
|
||||
v.name.text = item.toString()
|
||||
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
|
||||
v.subscribed.visibility =
|
||||
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
listAdapter = adapter
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
initFab(activity as MainActivity)
|
||||
reloadList()
|
||||
}
|
||||
|
||||
override fun reloadList() {
|
||||
adapter.clear()
|
||||
context?.let { context ->
|
||||
val addressRepo = Singleton.getAddressRepository(context)
|
||||
doAsync {
|
||||
addressRepo.getContactIds()
|
||||
.map { addressRepo.getAddress(it) }
|
||||
.forEach { address -> uiThread { adapter.add(address) } }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFab(activity: MainActivity) {
|
||||
activity.updateTitle(getString(R.string.contacts_and_subscriptions))
|
||||
val menu = FabSpeedDialMenu(activity)
|
||||
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
|
||||
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
|
||||
activity.initFab(R.drawable.ic_action_add_contact, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
when (itemId) {
|
||||
// FIXME
|
||||
// 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
|
||||
// .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
// .initiateScan()
|
||||
2 -> {
|
||||
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_address_list, container, false)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (data != null && data.hasExtra("SCAN_RESULT")) {
|
||||
val uri = Uri.parse(data.getStringExtra("SCAN_RESULT"))
|
||||
val intent = Intent(activity, CreateAddressActivity::class.java)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Void) = reloadList()
|
||||
|
||||
private data class ViewHolder(
|
||||
val ctx: Context,
|
||||
val avatar: ImageView,
|
||||
val name: TextView,
|
||||
val streamNumber: TextView,
|
||||
val subscribed: View
|
||||
)
|
||||
}
|
@ -1,105 +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;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
public class ComposeMessageActivity extends AppCompatActivity {
|
||||
public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER";
|
||||
public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT";
|
||||
public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT";
|
||||
public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT";
|
||||
public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST";
|
||||
public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING";
|
||||
public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.toolbar_layout);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(false);
|
||||
|
||||
// Display the fragment as the main content.
|
||||
ComposeMessageFragment fragment = new ComposeMessageFragment();
|
||||
fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static void launchReplyTo(Fragment fragment, Plaintext item) {
|
||||
fragment.startActivity(getReplyIntent(fragment.getActivity(), item));
|
||||
}
|
||||
|
||||
public static void launchReplyTo(Activity activity, Plaintext item) {
|
||||
activity.startActivity(getReplyIntent(activity, item));
|
||||
}
|
||||
|
||||
private static Intent getReplyIntent(Context ctx, Plaintext item) {
|
||||
Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class);
|
||||
BitmessageAddress receivingIdentity = item.getTo();
|
||||
if (receivingIdentity.isChan()) {
|
||||
// reply to chan, not to the sender of the message
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity);
|
||||
// I hate when people send as chan, so it won't be the default behaviour.
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx));
|
||||
} else {
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom());
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity);
|
||||
}
|
||||
// if the original message was sent using extended encoding, use it as well
|
||||
// so features like threading can be supported
|
||||
if (item.getEncoding() == EXTENDED) {
|
||||
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED);
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_PARENT, item);
|
||||
String prefix;
|
||||
if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3)
|
||||
.equalsIgnoreCase("RE:")) {
|
||||
prefix = "";
|
||||
} else {
|
||||
prefix = "RE: ";
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject());
|
||||
replyIntent.putExtra(EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n"
|
||||
+ item.getText());
|
||||
return replyIntent;
|
||||
}
|
||||
}
|
116
app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt
Normal file
116
app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setHomeAsUpIndicator(R.drawable.ic_action_close)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.content) == null) {
|
||||
// Display the fragment as the main content.
|
||||
val fragment = ComposeMessageFragment()
|
||||
fragment.arguments = intent.extras
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DRAFT = "ch.dissem.abit.Message.DRAFT"
|
||||
const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"
|
||||
const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"
|
||||
const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"
|
||||
const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"
|
||||
const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"
|
||||
const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"
|
||||
const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"
|
||||
|
||||
fun launchReplyTo(fragment: Fragment, item: Plaintext) =
|
||||
fragment.startActivity(
|
||||
getReplyIntent(
|
||||
ctx = fragment.activity
|
||||
?: throw IllegalStateException("Fragment not attached to an activity"),
|
||||
item = item
|
||||
)
|
||||
)
|
||||
|
||||
fun launchReplyTo(activity: Activity, item: Plaintext) =
|
||||
activity.startActivity(getReplyIntent(activity, item))
|
||||
|
||||
private fun getReplyIntent(ctx: Context, item: Plaintext): Intent {
|
||||
val replyIntent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
val receivingIdentity = item.to
|
||||
if (receivingIdentity?.isChan == true) {
|
||||
// reply to chan, not to the sender of the message
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity)
|
||||
// I hate when people send as chan, so it won't be the default behaviour.
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx))
|
||||
} else {
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, item.from)
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity)
|
||||
}
|
||||
// if the original message was sent using extended encoding, use it as well
|
||||
// so features like threading can be supported
|
||||
if (item.encoding == EXTENDED) {
|
||||
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED)
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_PARENT, item)
|
||||
item.subject?.let { subject ->
|
||||
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
|
||||
"RE:",
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
""
|
||||
} else {
|
||||
"RE: "
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject)
|
||||
}
|
||||
replyIntent.putExtra(
|
||||
EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n${item.text ?: ""}"
|
||||
)
|
||||
return replyIntent
|
||||
}
|
||||
}
|
||||
}
|
@ -1,266 +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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.ContactAdapter;
|
||||
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_PARENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
public class ComposeMessageFragment extends Fragment {
|
||||
private BitmessageAddress identity;
|
||||
private BitmessageAddress recipient;
|
||||
private String subject;
|
||||
private String content;
|
||||
private AutoCompleteTextView recipientInput;
|
||||
private EditText subjectInput;
|
||||
private EditText bodyInput;
|
||||
private boolean broadcast;
|
||||
private Plaintext.Encoding encoding;
|
||||
private Plaintext parent;
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public ComposeMessageFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
if (getArguments().containsKey(EXTRA_IDENTITY)) {
|
||||
identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY);
|
||||
if (getActivity() != null) {
|
||||
if (identity == null || identity.getPrivateKey() == null) {
|
||||
identity = Singleton.getIdentity(getActivity());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("No identity set for ComposeMessageFragment");
|
||||
}
|
||||
broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false);
|
||||
if (getArguments().containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getArguments().getString(EXTRA_SUBJECT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_CONTENT)) {
|
||||
content = getArguments().getString(EXTRA_CONTENT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_ENCODING)) {
|
||||
encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING);
|
||||
} else {
|
||||
encoding = Plaintext.Encoding.SIMPLE;
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_PARENT)) {
|
||||
parent = (Plaintext) getArguments().getSerializable(EXTRA_PARENT);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("No identity set for ComposeMessageFragment");
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false);
|
||||
recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient);
|
||||
if (broadcast) {
|
||||
recipientInput.setVisibility(View.GONE);
|
||||
} else {
|
||||
final ContactAdapter adapter = new ContactAdapter(getContext());
|
||||
recipientInput.setAdapter(adapter);
|
||||
recipientInput.setOnItemClickListener(
|
||||
new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
|
||||
adapter.getItem(pos);
|
||||
}
|
||||
}
|
||||
);
|
||||
recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long
|
||||
id) {
|
||||
recipient = adapter.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
if (recipient != null) {
|
||||
recipientInput.setText(recipient.toString());
|
||||
}
|
||||
}
|
||||
subjectInput = (EditText) rootView.findViewById(R.id.subject);
|
||||
subjectInput.setText(subject);
|
||||
bodyInput = (EditText) rootView.findViewById(R.id.body);
|
||||
bodyInput.setText(content);
|
||||
|
||||
if (recipient == null) {
|
||||
recipientInput.requestFocus();
|
||||
} else if (subject == null || subject.isEmpty()) {
|
||||
subjectInput.requestFocus();
|
||||
} else {
|
||||
bodyInput.requestFocus();
|
||||
bodyInput.setSelection(0);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (identity == null || identity.getPrivateKey() == null) {
|
||||
identity = Singleton.getIdentity(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.compose, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.send:
|
||||
send();
|
||||
return true;
|
||||
case R.id.select_encoding:
|
||||
SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(EXTRA_ENCODING, encoding);
|
||||
encodingDialog.setArguments(args);
|
||||
encodingDialog.setTargetFragment(this, 0);
|
||||
encodingDialog.show(getFragmentManager(), "select encoding dialog");
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 0 && resultCode == RESULT_OK) {
|
||||
encoding = (Plaintext.Encoding) data.getSerializableExtra(EXTRA_ENCODING);
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void send() {
|
||||
Plaintext.Builder builder;
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(getContext());
|
||||
if (broadcast) {
|
||||
builder = new Plaintext.Builder(BROADCAST)
|
||||
.from(identity);
|
||||
} else {
|
||||
String inputString = recipientInput.getText().toString();
|
||||
if (recipient == null || !recipient.toString().equals(inputString)) {
|
||||
try {
|
||||
recipient = new BitmessageAddress(inputString);
|
||||
} catch (Exception e) {
|
||||
List<BitmessageAddress> contacts = Singleton.getAddressRepository
|
||||
(getContext()).getContacts();
|
||||
for (BitmessageAddress contact : contacts) {
|
||||
if (inputString.equalsIgnoreCase(contact.getAlias())) {
|
||||
recipient = contact;
|
||||
if (inputString.equals(contact.getAlias()))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
builder = new Plaintext.Builder(MSG)
|
||||
.from(identity)
|
||||
.to(recipient);
|
||||
}
|
||||
switch (encoding) {
|
||||
case SIMPLE:
|
||||
builder.message(
|
||||
subjectInput.getText().toString(),
|
||||
bodyInput.getText().toString()
|
||||
);
|
||||
break;
|
||||
case EXTENDED:
|
||||
builder.message(
|
||||
new Message.Builder()
|
||||
.subject(subjectInput.getText().toString())
|
||||
.body(bodyInput.getText().toString())
|
||||
.addParent(parent)
|
||||
.build()
|
||||
);
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(
|
||||
getContext(),
|
||||
getContext().getString(R.string.error_unsupported_encoding, encoding),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
builder.message(
|
||||
subjectInput.getText().toString(),
|
||||
bodyInput.getText().toString()
|
||||
);
|
||||
break;
|
||||
}
|
||||
bmc.send(builder.build());
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
|
292
app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt
Normal file
292
app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt
Normal file
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_DRAFT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
|
||||
import ch.dissem.apps.abit.adapter.ContactAdapter
|
||||
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message
|
||||
import kotlinx.android.synthetic.main.fragment_compose_message.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageFragment : Fragment() {
|
||||
private lateinit var identity: BitmessageAddress
|
||||
private var recipient: BitmessageAddress? = null
|
||||
private var subject: String = ""
|
||||
private var content: String = ""
|
||||
|
||||
private var broadcast: Boolean = false
|
||||
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
|
||||
private val parents = mutableListOf<InventoryVector>()
|
||||
|
||||
private var draft: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
arguments?.apply {
|
||||
val draft = getSerializable(EXTRA_DRAFT) as Plaintext?
|
||||
if (draft != null) {
|
||||
this@ComposeMessageFragment.draft = draft
|
||||
identity = draft.from
|
||||
recipient = draft.to
|
||||
subject = draft.subject ?: ""
|
||||
content = draft.text ?: ""
|
||||
encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE
|
||||
parents.addAll(draft.parents)
|
||||
} else {
|
||||
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
|
||||
if (context != null && id?.privateKey == null) {
|
||||
id = Singleton.getIdentity(context!!)
|
||||
}
|
||||
if (id?.privateKey != null) {
|
||||
identity = id
|
||||
} else {
|
||||
throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
}
|
||||
broadcast = getBoolean(EXTRA_BROADCAST, false)
|
||||
if (containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
|
||||
}
|
||||
if (containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getString(EXTRA_SUBJECT) ?: throw IllegalStateException("EXTRA_SUBJECT expected")
|
||||
}
|
||||
if (containsKey(EXTRA_CONTENT)) {
|
||||
content = getString(EXTRA_CONTENT) ?: throw IllegalStateException("EXTRA_CONTENT expected")
|
||||
}
|
||||
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
|
||||
|
||||
if (containsKey(EXTRA_PARENT)) {
|
||||
val parent = getSerializable(EXTRA_PARENT) as Plaintext
|
||||
parent.inventoryVector?.let { parents.add(it) }
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(R.layout.fragment_compose_message, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { ctx ->
|
||||
val identities = Singleton.getAddressRepository(ctx).getIdentities()
|
||||
sender_input.adapter = ContactAdapter(ctx, identities, true)
|
||||
val index = identities.indexOf(Singleton.getIdentity(ctx))
|
||||
if (index >= 0) {
|
||||
sender_input.setSelection(index)
|
||||
}
|
||||
|
||||
if (broadcast) {
|
||||
recipient_input.visibility = View.GONE
|
||||
} else {
|
||||
val adapter = ContactAdapter(
|
||||
ctx,
|
||||
Singleton.getAddressRepository(ctx).getContacts()
|
||||
)
|
||||
recipient_input.setAdapter(adapter)
|
||||
recipient_input.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _, _, pos, _ -> recipient = adapter.getItem(pos) }
|
||||
|
||||
recipient_input.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
recipient = adapter.getItem(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) =
|
||||
Unit // leave current selection
|
||||
}
|
||||
recipient?.let { recipient_input.setText(it.toString()) }
|
||||
}
|
||||
subject_input.setText(subject)
|
||||
body_input.setText(content)
|
||||
|
||||
when {
|
||||
recipient == null -> recipient_input.requestFocus()
|
||||
subject.isEmpty() -> subject_input.requestFocus()
|
||||
else -> {
|
||||
body_input.requestFocus()
|
||||
body_input.setSelection(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.compose, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.send -> {
|
||||
send()
|
||||
return true
|
||||
}
|
||||
R.id.select_encoding -> {
|
||||
val encodingDialog = SelectEncodingDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(EXTRA_ENCODING, encoding)
|
||||
encodingDialog.arguments = args
|
||||
encodingDialog.setTargetFragment(this, 0)
|
||||
encodingDialog.show(fragmentManager, "select encoding dialog")
|
||||
return true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) =
|
||||
if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
|
||||
encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun build(ctx: Context): Plaintext {
|
||||
val builder: Plaintext.Builder
|
||||
if (broadcast) {
|
||||
builder = Plaintext.Builder(BROADCAST)
|
||||
} else {
|
||||
val inputString = recipient_input.text.toString()
|
||||
if (recipient == null || recipient?.toString() != inputString) {
|
||||
try {
|
||||
recipient = BitmessageAddress(inputString)
|
||||
} catch (e: Exception) {
|
||||
val contacts = Singleton.getAddressRepository(ctx).getContacts()
|
||||
for (contact in contacts) {
|
||||
if (inputString.equals(contact.alias, ignoreCase = true)) {
|
||||
recipient = contact
|
||||
if (inputString == contact.alias)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
builder = Plaintext.Builder(MSG)
|
||||
.to(recipient)
|
||||
}
|
||||
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
sender?.let { builder.from(it) }
|
||||
if (!ctx.preferences.requestAcknowledgements) {
|
||||
builder.preventAck()
|
||||
}
|
||||
when (encoding) {
|
||||
Plaintext.Encoding.SIMPLE -> builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
Plaintext.Encoding.EXTENDED -> builder.message(
|
||||
ExtendedEncoding(
|
||||
Message(
|
||||
subject = subject_input.text.toString(),
|
||||
body = body_input.text.toString(),
|
||||
parents = parents,
|
||||
files = emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
ctx.getString(R.string.error_unsupported_encoding, encoding),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
draft?.id?.let { builder.id(it) }
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (draft?.labels?.any { it.type == Label.Type.DRAFT } != false) {
|
||||
context?.let { ctx ->
|
||||
draft = build(ctx).also { msg ->
|
||||
Singleton.labeler.markAsDraft(msg)
|
||||
Singleton.getMessageRepository(ctx).save(msg)
|
||||
}
|
||||
Toast.makeText(ctx, "Message saved as draft", Toast.LENGTH_LONG).show()
|
||||
} ?: throw IllegalStateException("Context is not available")
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
identity = sender_input.selectedItem as BitmessageAddress
|
||||
// recipient is set when one is selected
|
||||
subject = subject_input.text?.toString() ?: ""
|
||||
content = body_input.text?.toString() ?: ""
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
if (recipient == null) {
|
||||
Toast.makeText(ctx, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
build(ctx).let { message ->
|
||||
draft = message
|
||||
Singleton.getBitmessageContext(ctx).send(message)
|
||||
}
|
||||
ctx.finish()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import ch.dissem.apps.abit.adapter.ConversationAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class ConversationDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var itemId: UUID? = null
|
||||
private var item: Conversation? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM_ID)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
itemId = arguments.getSerializable(ARG_ITEM_ID) as UUID
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_conversation_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
item = itemId?.let { Singleton.getConversationService(ctx).getConversation(it) }
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||
messages.adapter = ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel)
|
||||
messages.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.conversation, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.delete -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.remove(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.archive(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM_ID = "item_id"
|
||||
}
|
||||
}
|
@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.adapter.EventListener
|
||||
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
|
||||
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||
import org.jetbrains.anko.*
|
||||
import java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class ConversationListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
|
||||
|
||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private var deleteAllMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableConversationAdapter?.activateOnItemClick = activateOnItemClick
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableConversationAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val conversationIds = messageRepo.findConversations(
|
||||
currentLabel.value,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE,
|
||||
context?.preferences?.separateIdentities == true
|
||||
)
|
||||
conversationIds.forEach { conversationId ->
|
||||
val conversation = conversationService.getConversation(conversationId)
|
||||
uiThread {
|
||||
messageAdapter.add(conversation)
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
isLastPage = conversationIds.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
conversationService = Singleton.getConversationService(activity)
|
||||
|
||||
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun reloadList() = doUpdateList(currentLabel.value)
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableConversationAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableConversationAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
|
||||
deleteAllMenuItem?.isVisible = false
|
||||
|
||||
MainActivity.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val context = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
val listener = object : EventListener {
|
||||
override fun onItemDeleted(position: Int) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
swipeableConversationAdapter?.removeAt(position)
|
||||
|
||||
}
|
||||
|
||||
override fun onItemArchived(position: Int) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.archive(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
swipeableConversationAdapter?.removeAt(position)
|
||||
}
|
||||
|
||||
override fun onItemSelected(position: Int) {
|
||||
swipeableConversationAdapter?.selectedPosition = position
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
MainActivity.apply { onItemSelected(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recycler_view.layoutManager = layoutManager
|
||||
swipeableConversationAdapter = SwipeableConversationAdapter(context).apply {
|
||||
activateOnItemClick = this@ConversationListFragment.activateOnItemClick
|
||||
eventListener = listener
|
||||
}
|
||||
recycler_view.adapter = swipeableConversationAdapter
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
val dirs = when (currentLabel.value?.type) {
|
||||
Label.Type.TRASH -> ItemTouchHelper.LEFT
|
||||
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
|
||||
}
|
||||
|
||||
val swipeHandler = SwipeToDeleteCallback(context, dirs, listener)
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||
itemTouchHelper.attachToRecyclerView(recycler_view)
|
||||
|
||||
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
swipeableConversationAdapter = null
|
||||
layoutManager = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
deleteAllMenuItem = menu.findItem(R.id.delete_all)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete_all -> {
|
||||
currentLabel.value?.let { label ->
|
||||
context?.apply {
|
||||
alert(
|
||||
R.string.delete_all_messages_in_list,
|
||||
R.string.delete_all_messages_in_list_ask
|
||||
) {
|
||||
positiveButton(R.string.delete) {
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
cancelButton { }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAllMessages(label: Label) {
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.onNext(label)
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.onNext(backStack.pop())
|
||||
true
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Base64;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V2Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
|
||||
|
||||
import static android.util.Base64.URL_SAFE;
|
||||
|
||||
public class CreateAddressActivity extends AppCompatActivity {
|
||||
private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$");
|
||||
private byte[] pubkeyBytes;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri != null)
|
||||
setContentView(R.layout.activity_open_bitmessage_link);
|
||||
else
|
||||
setContentView(R.layout.activity_create_bitmessage_address);
|
||||
|
||||
final TextView address = (TextView) findViewById(R.id.address);
|
||||
final EditText label = (EditText) findViewById(R.id.label);
|
||||
final Switch subscribe = (Switch) findViewById(R.id.subscribe);
|
||||
|
||||
if (uri != null) {
|
||||
String addressText = getAddress(uri);
|
||||
String[] parameters = getParameters(uri);
|
||||
for (String parameter : parameters) {
|
||||
Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter);
|
||||
if (matcher.find()) {
|
||||
String key = matcher.group(1).toLowerCase();
|
||||
String value = matcher.group(2);
|
||||
switch (key) {
|
||||
case "label":
|
||||
label.setText(value.trim());
|
||||
break;
|
||||
case "action":
|
||||
subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe"));
|
||||
break;
|
||||
case "pubkey":
|
||||
pubkeyBytes = Base64.decode(value, URL_SAFE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address.setText(addressText);
|
||||
}
|
||||
|
||||
final Button cancel = (Button) findViewById(R.id.cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
final Button ok = (Button) findViewById(R.id.do_import);
|
||||
ok.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String addressText = String.valueOf(address.getText()).trim();
|
||||
try {
|
||||
BitmessageAddress bmAddress = new BitmessageAddress(addressText);
|
||||
bmAddress.setAlias(label.getText().toString());
|
||||
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext
|
||||
(CreateAddressActivity.this);
|
||||
bmc.addContact(bmAddress);
|
||||
if (subscribe.isChecked()) {
|
||||
bmc.addSubscribtion(bmAddress);
|
||||
}
|
||||
if (pubkeyBytes != null) {
|
||||
try {
|
||||
final Pubkey pubkey;
|
||||
InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes);
|
||||
long stream = bmAddress.getStream();
|
||||
switch ((int) bmAddress.getVersion()) {
|
||||
case 2:
|
||||
pubkey = V2Pubkey.read(pubkeyStream, stream);
|
||||
break;
|
||||
case 3:
|
||||
pubkey = V3Pubkey.read(pubkeyStream, stream);
|
||||
break;
|
||||
case 4:
|
||||
pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream));
|
||||
break;
|
||||
default:
|
||||
pubkey = null;
|
||||
}
|
||||
if (pubkey != null) {
|
||||
bmAddress.setPubkey(pubkey);
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
} catch (RuntimeException e) {
|
||||
address.setError(getString(R.string.error_illegal_address));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getAddress(Uri uri) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String schemeSpecificPart = uri.getSchemeSpecificPart();
|
||||
if (!schemeSpecificPart.startsWith("BM-")) {
|
||||
result.append("BM-");
|
||||
}
|
||||
if (schemeSpecificPart.contains("?")) {
|
||||
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')));
|
||||
} else if (schemeSpecificPart.contains("#")) {
|
||||
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')));
|
||||
} else {
|
||||
result.append(schemeSpecificPart);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String[] getParameters(Uri uri) {
|
||||
int index = uri.getSchemeSpecificPart().indexOf('?');
|
||||
if (index >= 0) {
|
||||
String parameterPart = uri.getSchemeSpecificPart().substring(index + 1);
|
||||
return parameterPart.split("&");
|
||||
} else {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
}
|
146
app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt
Normal file
146
app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Base64
|
||||
import android.util.Base64.URL_SAFE
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V2Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class CreateAddressActivity : AppCompatActivity() {
|
||||
private var pubkeyBytes: ByteArray? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val uri = intent.data
|
||||
if (uri != null)
|
||||
setContentView(R.layout.activity_open_bitmessage_link)
|
||||
else
|
||||
setContentView(R.layout.activity_create_bitmessage_address)
|
||||
|
||||
val address = findViewById<TextView>(R.id.address)
|
||||
val label = findViewById<EditText>(R.id.label)
|
||||
val subscribe = findViewById<Switch>(R.id.subscribe)
|
||||
|
||||
if (uri != null) {
|
||||
val addressText = getAddress(uri)
|
||||
val parameters = getParameters(uri)
|
||||
for (parameter in parameters) {
|
||||
val matcher = KEY_VALUE_PATTERN.matcher(parameter)
|
||||
if (matcher.find()) {
|
||||
val key = matcher.group(1).toLowerCase()
|
||||
val value = matcher.group(2)
|
||||
when (key) {
|
||||
"label" -> label.setText(value.trim { it <= ' ' })
|
||||
"action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true)
|
||||
"pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE)
|
||||
else -> LOG.debug("Unknown attribute: $key=$value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address.text = addressText
|
||||
}
|
||||
|
||||
val cancel = findViewById<Button>(R.id.cancel)
|
||||
cancel.setOnClickListener {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) }
|
||||
}
|
||||
|
||||
|
||||
private fun onOK(address: TextView, label: EditText, subscribe: Switch) {
|
||||
val addressText = address.text.toString().trim { it <= ' ' }
|
||||
try {
|
||||
val bmAddress = BitmessageAddress(addressText)
|
||||
bmAddress.alias = label.text.toString()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(applicationContext)
|
||||
bmc.addContact(bmAddress)
|
||||
if (subscribe.isChecked) {
|
||||
bmc.addSubscribtion(bmAddress)
|
||||
}
|
||||
pubkeyBytes?.let { pubkeyBytes ->
|
||||
try {
|
||||
val pubkeyStream = ByteArrayInputStream(pubkeyBytes)
|
||||
val stream = bmAddress.stream
|
||||
when (bmAddress.version.toInt()) {
|
||||
2 -> V2Pubkey.read(pubkeyStream, stream)
|
||||
3 -> V3Pubkey.read(pubkeyStream, stream)
|
||||
4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream))
|
||||
else -> null
|
||||
}?.let { bmAddress.pubkey = it }
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} catch (e: RuntimeException) {
|
||||
address.error = getString(R.string.error_illegal_address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAddress(uri: Uri): String {
|
||||
val result = StringBuilder()
|
||||
val schemeSpecificPart = uri.schemeSpecificPart
|
||||
if (!schemeSpecificPart.startsWith("BM-")) {
|
||||
result.append("BM-")
|
||||
}
|
||||
when {
|
||||
schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')))
|
||||
schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')))
|
||||
else -> result.append(schemeSpecificPart)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun getParameters(uri: Uri): Array<String> {
|
||||
val index = uri.schemeSpecificPart.indexOf('?')
|
||||
return if (index >= 0) {
|
||||
uri.schemeSpecificPart
|
||||
.substring(index + 1)
|
||||
.split("&".toRegex())
|
||||
.dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
} else {
|
||||
emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java)
|
||||
|
||||
private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$")
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.mikepenz.materialize.MaterializeBuilder;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public abstract class DetailActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scrolling_toolbar_layout);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
// Show the Up button in the action bar.
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
new MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
48
app/src/main/java/ch/dissem/apps/abit/DetailActivity.kt
Normal file
48
app/src/main/java/ch/dissem/apps/abit/DetailActivity.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.MenuItem
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class DetailActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.scrolling_toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
// Show the Up button in the action bar.
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.graphics.*;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextPaint;
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class Identicon extends Drawable {
|
||||
private static final int SIZE = 9;
|
||||
private static final int CENTER_COLUMN = 5;
|
||||
|
||||
private final Paint paint;
|
||||
private final int color;
|
||||
private final int background;
|
||||
private final boolean[][] fields;
|
||||
private final boolean chan;
|
||||
private final TextPaint textPaint;
|
||||
|
||||
public Identicon(@NonNull BitmessageAddress input) {
|
||||
paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setAntiAlias(true);
|
||||
textPaint = new TextPaint();
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setColor(0xFF607D8B);
|
||||
textPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
|
||||
|
||||
chan = input.isChan();
|
||||
|
||||
byte[] hash = input.getRipe();
|
||||
|
||||
fields = new boolean[SIZE][SIZE];
|
||||
color = Color.HSVToColor(new float[]{
|
||||
Math.abs(hash[0] * hash[1] + hash[2]) % 360,
|
||||
0.8f,
|
||||
1.0f
|
||||
});
|
||||
background = Color.HSVToColor(new float[]{
|
||||
Math.abs(hash[1] * hash[2] + hash[0]) % 360,
|
||||
0.8f,
|
||||
1.0f
|
||||
});
|
||||
|
||||
for (int row = 0; row < SIZE; row++) {
|
||||
if (!chan || row < 5 || row > 6) {
|
||||
for (int column = 0; column <= CENTER_COLUMN; column++) {
|
||||
if (
|
||||
(row - SIZE / 2) * (row - SIZE / 2)
|
||||
+ (column - SIZE / 2) * (column - SIZE / 2)
|
||||
< SIZE / 2 * SIZE / 2
|
||||
) {
|
||||
fields[row][column] = hash[(row * CENTER_COLUMN + column) % hash.length]
|
||||
>= 0;
|
||||
fields[row][SIZE - column - 1] = fields[row][column];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
float x, y;
|
||||
float width = canvas.getWidth();
|
||||
float height = canvas.getHeight();
|
||||
float cellWidth = width / (float) SIZE;
|
||||
float cellHeight = height / (float) SIZE;
|
||||
paint.setColor(background);
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint);
|
||||
paint.setColor(color);
|
||||
for (int row = 0; row < SIZE; row++) {
|
||||
for (int column = 0; column < SIZE; column++) {
|
||||
if (fields[row][column]) {
|
||||
x = cellWidth * column;
|
||||
y = cellHeight * row;
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chan) {
|
||||
textPaint.setTextSize(2 * cellHeight);
|
||||
canvas.drawText("[chan]", width / 2, 6.7f * cellHeight, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
paint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSPARENT;
|
||||
}
|
||||
}
|
181
app/src/main/java/ch/dissem/apps/abit/Identicon.kt
Normal file
181
app/src/main/java/ch/dissem/apps/abit/Identicon.kt
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import android.text.TextPaint
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import org.jetbrains.anko.collections.forEachWithIndex
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val hash = input.ripe
|
||||
private val isChan = input.isChan
|
||||
private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply {
|
||||
for (row in 0 until SIZE) {
|
||||
if (!isChan || row < 5 || row > 6) {
|
||||
for (column in 0..CENTER_COLUMN) {
|
||||
if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) {
|
||||
this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0
|
||||
this[row][SIZE - column - 1] = this[row][column]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val color = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val background = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val textPaint = TextPaint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = 0xFF607D8B.toInt()
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
draw(canvas, 0f, 0f, width, height)
|
||||
}
|
||||
|
||||
internal fun draw(canvas: Canvas, offsetX: Float, offsetY: Float, width: Float, height: Float) {
|
||||
var x: Float
|
||||
var y: Float
|
||||
val cellWidth = width / SIZE.toFloat()
|
||||
val cellHeight = height / SIZE.toFloat()
|
||||
paint.color = background
|
||||
canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
|
||||
paint.color = color
|
||||
for (row in 0 until SIZE) {
|
||||
for (column in 0 until SIZE) {
|
||||
if (fields[row][column]) {
|
||||
x = offsetX + cellWidth * column
|
||||
y = offsetY + cellHeight * row
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isChan) {
|
||||
textPaint.textSize = 2 * cellHeight
|
||||
canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
paint.colorFilter = cf
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
|
||||
companion object {
|
||||
private const val SIZE = 9
|
||||
private const val CENTER_COLUMN = 5
|
||||
}
|
||||
}
|
||||
|
||||
class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt private val backgroundColor: Int = 0xFFAEC2CC.toInt()) :
|
||||
Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = backgroundColor
|
||||
}
|
||||
|
||||
private val identicons = input.asSequence().sortedBy { it.isChan }.map { Identicon(it) }.take(4).toList()
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
|
||||
when (identicons.size) {
|
||||
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
1 -> identicons.first().draw(canvas, 0f, 0f, width, height)
|
||||
2 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val w = width / 2
|
||||
val h = height / 2
|
||||
var x = 0f
|
||||
val y = height / 4
|
||||
identicons.forEach {
|
||||
it.draw(canvas, x, y, w, h)
|
||||
x += w
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
val scale = 2f / (1f + 2f * sqrt(3f))
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
|
||||
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
|
||||
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
|
||||
}
|
||||
4 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val scale = 1f / (1f + sqrt(2f))
|
||||
val borderScale = 0.5f - scale
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
val x = width * borderScale
|
||||
val y = height * borderScale
|
||||
identicons.forEachWithIndex { i, identicon ->
|
||||
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
identicons.forEach { it.alpha = alpha }
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
identicons.forEach { it.colorFilter = colorFilter }
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
}
|
@ -1,85 +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;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
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.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.wif.WifImporter;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class ImportIdentitiesFragment extends Fragment {
|
||||
public static final String WIF_DATA = "wif_data";
|
||||
private AddressSelectorAdapter adapter;
|
||||
private WifImporter importer;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
String wifData = getArguments().getString(WIF_DATA);
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(getActivity());
|
||||
View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false);
|
||||
try {
|
||||
importer = new WifImporter(bmc, wifData);
|
||||
adapter = new AddressSelectorAdapter(importer.getIdentities());
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(),
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false);
|
||||
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
recyclerView.addItemDecoration(new SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(getActivity(), R.drawable.list_divider_h), true));
|
||||
} catch (IOException e) {
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
importer.importAll(adapter.getSelected());
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
for (BitmessageAddress selected : adapter.getSelected()) {
|
||||
mainActivity.addIdentityEntry(selected);
|
||||
}
|
||||
}
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.wif.WifImporter
|
||||
import org.ini4j.InvalidFileFormatException
|
||||
import org.jetbrains.anko.longToast
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentitiesFragment : Fragment() {
|
||||
private lateinit var adapter: AddressSelectorAdapter
|
||||
private lateinit var importer: WifImporter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val ctx = activity ?: throw IllegalStateException("No activity available")
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val wifData = arguments?.getString(WIF_DATA) ?: throw IllegalStateException("No WIF data")
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
|
||||
try {
|
||||
importer = WifImporter(bmc, wifData)
|
||||
} catch (e: InvalidFileFormatException) {
|
||||
ctx.longToast(R.string.invalid_wif_file)
|
||||
ctx.finish()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = AddressSelectorAdapter(importer.getIdentities())
|
||||
val layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
RecyclerView.VERTICAL,
|
||||
false
|
||||
)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.HORIZONTAL))
|
||||
|
||||
view.findViewById<Button>(R.id.finish).setOnClickListener {
|
||||
importer.importAll(adapter.selected)
|
||||
MainActivity.apply {
|
||||
for (selected in adapter.selected) {
|
||||
addIdentityEntry(selected)
|
||||
}
|
||||
}
|
||||
ctx.finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WIF_DATA = "wif_data"
|
||||
}
|
||||
}
|
@ -1,56 +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;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class ImportIdentityActivity extends DetailActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String wifData;
|
||||
if (savedInstanceState == null) {
|
||||
wifData = null;
|
||||
} else {
|
||||
wifData = savedInstanceState.getString(WIF_DATA);
|
||||
}
|
||||
if (wifData == null) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, new InputWifFragment())
|
||||
.commit();
|
||||
} else {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(WIF_DATA, wifData);
|
||||
|
||||
ImportIdentitiesFragment fragment = new ImportIdentitiesFragment();
|
||||
fragment.setArguments(bundle);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.transaction
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentityActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
|
||||
|
||||
if (wifData == null) {
|
||||
supportFragmentManager.transaction {
|
||||
replace(R.id.content, InputWifFragment())
|
||||
}
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
|
||||
|
||||
val fragment = ImportIdentitiesFragment().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
supportFragmentManager.transaction {
|
||||
replace(R.id.content, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,122 +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;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.angads25.filepicker.controller.DialogSelectionListener;
|
||||
import com.github.angads25.filepicker.model.DialogConfigs;
|
||||
import com.github.angads25.filepicker.model.DialogProperties;
|
||||
import com.github.angads25.filepicker.view.FilePickerDialog;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class InputWifFragment extends Fragment {
|
||||
private TextView wifData;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_import_input, container, false);
|
||||
wifData = (TextView) view.findViewById(R.id.wif_input);
|
||||
|
||||
view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(WIF_DATA, wifData.getText().toString());
|
||||
|
||||
ImportIdentitiesFragment fragment = new ImportIdentitiesFragment();
|
||||
fragment.setArguments(bundle);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.import_input_data, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
DialogProperties properties = new DialogProperties();
|
||||
properties.selection_mode = DialogConfigs.SINGLE_MODE;
|
||||
properties.selection_type = DialogConfigs.FILE_SELECT;
|
||||
properties.root = new File(DialogConfigs.DEFAULT_DIR);
|
||||
properties.error_dir = new File(DialogConfigs.DEFAULT_DIR);
|
||||
properties.extensions = null;
|
||||
FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties);
|
||||
dialog.setTitle(getString(R.string.select_file_title));
|
||||
dialog.setDialogSelectionListener(new DialogSelectionListener() {
|
||||
@Override
|
||||
public void onSelectedFilePaths(String[] files) {
|
||||
if (files.length > 0) {
|
||||
try (InputStream in = new FileInputStream(files[0])) {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
//noinspection ConstantConditions
|
||||
while ((length = in.read(buffer)) != -1) {
|
||||
data.write(buffer, 0, length);
|
||||
}
|
||||
wifData.setText(data.toString("UTF-8"));
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(
|
||||
getActivity(),
|
||||
R.string.error_loading_data,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
}
|
102
app/src/main/java/ch/dissem/apps/abit/InputWifFragment.kt
Normal file
102
app/src/main/java/ch/dissem/apps/abit/InputWifFragment.kt
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.transaction
|
||||
import com.github.angads25.filepicker.model.DialogConfigs
|
||||
import com.github.angads25.filepicker.model.DialogProperties
|
||||
import com.github.angads25.filepicker.view.FilePickerDialog
|
||||
import kotlinx.android.synthetic.main.fragment_import_input.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class InputWifFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_import_input, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
next.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString())
|
||||
|
||||
val fragment = ImportIdentitiesFragment().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
fragmentManager?.transaction {
|
||||
replace(R.id.content, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
|
||||
inflater.inflate(R.menu.import_input_data, menu)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val properties = DialogProperties()
|
||||
properties.selection_mode = DialogConfigs.SINGLE_MODE
|
||||
properties.selection_type = DialogConfigs.FILE_SELECT
|
||||
properties.root = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.error_dir = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.extensions = null
|
||||
val dialog = FilePickerDialog(activity, properties)
|
||||
dialog.setTitle(getString(R.string.select_file_title))
|
||||
dialog.setDialogSelectionListener { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
try {
|
||||
FileInputStream(files[0]).use { inputStream ->
|
||||
val data = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
|
||||
var length: Int = inputStream.read(buffer)
|
||||
|
||||
while (length != -1) {
|
||||
data.write(buffer, 0, length)
|
||||
length = inputStream.read(buffer)
|
||||
}
|
||||
wif_input.setText(data.toByteArray().toString())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.error_loading_data,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
}
|
@ -14,15 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public interface ListHolder {
|
||||
void updateList(Label label);
|
||||
interface ListHolder<in L> {
|
||||
fun reloadList()
|
||||
|
||||
void setActivateOnItemClick(boolean activateOnItemClick);
|
||||
fun updateList(label: L)
|
||||
|
||||
fun setActivateOnItemClick(activateOnItemClick: Boolean)
|
||||
|
||||
fun showPreviousList(): Boolean
|
||||
}
|
@ -1,622 +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;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
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.Display;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.amlcurran.showcaseview.ShowcaseView;
|
||||
import com.github.amlcurran.showcaseview.targets.Target;
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
import com.mikepenz.iconics.IconicsDrawable;
|
||||
import com.mikepenz.materialdrawer.AccountHeader;
|
||||
import com.mikepenz.materialdrawer.AccountHeaderBuilder;
|
||||
import com.mikepenz.materialdrawer.Drawer;
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder;
|
||||
import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener;
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.SwitchDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Nameable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment;
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity;
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository;
|
||||
import ch.dissem.apps.abit.service.BitmessageService;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter;
|
||||
import ch.dissem.apps.abit.util.Drawables;
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo;
|
||||
import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE;
|
||||
import static ch.dissem.apps.abit.service.BitmessageService.isRunning;
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a list of Messages. This activity
|
||||
* has different presentations for handset and tablet-size devices. On
|
||||
* handsets, the activity presents a list of items, which when touched,
|
||||
* lead to a {@link MessageDetailActivity} representing
|
||||
* item details. On tablets, the activity presents the list of items and
|
||||
* item details side-by-side using two vertical panes.
|
||||
* <p>
|
||||
* The activity makes heavy use of fragments. The list of items is a
|
||||
* {@link MessageListFragment} and the item details
|
||||
* (if present) is a {@link MessageDetailFragment}.
|
||||
* </p><p>
|
||||
* This activity also implements the required
|
||||
* {@link ListSelectionListener} interface
|
||||
* to listen for item selections.
|
||||
* </p>
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements ListSelectionListener<Serializable>, ActionBarListener {
|
||||
public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage";
|
||||
public static final String EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel";
|
||||
public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage";
|
||||
public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox";
|
||||
|
||||
private static final int ADD_IDENTITY = 1;
|
||||
private static final int MANAGE_IDENTITY = 2;
|
||||
|
||||
private static final long ID_NODE_SWITCH = 1;
|
||||
|
||||
private static WeakReference<MainActivity> instance;
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
*/
|
||||
private boolean twoPane;
|
||||
|
||||
private Label selectedLabel;
|
||||
|
||||
private BitmessageContext bmc;
|
||||
private AccountHeader accountHeader;
|
||||
|
||||
private Drawer drawer;
|
||||
private SwitchDrawerItem nodeSwitch;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = new WeakReference<>(this);
|
||||
bmc = Singleton.getBitmessageContext(this);
|
||||
|
||||
setContentView(R.layout.activity_message_list);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
MessageListFragment listFragment = new MessageListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.commit();
|
||||
|
||||
if (findViewById(R.id.message_detail_container) != null) {
|
||||
// The detail container view will be present only in the
|
||||
// large-screen layouts (res/values-large and
|
||||
// res/values-sw600dp). If this view is present, then the
|
||||
// activity should be in two-pane mode.
|
||||
twoPane = true;
|
||||
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true);
|
||||
}
|
||||
|
||||
createDrawer(toolbar);
|
||||
|
||||
// handle intents
|
||||
Intent intent = getIntent();
|
||||
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE));
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
|
||||
Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE);
|
||||
launchReplyTo(this, item);
|
||||
}
|
||||
|
||||
if (Preferences.useTrustedNode(this)) {
|
||||
SyncAdapter.startSync(this);
|
||||
} else {
|
||||
SyncAdapter.stopSync(this);
|
||||
}
|
||||
if (drawer.isDrawerOpen()) {
|
||||
RelativeLayout.LayoutParams lps = new RelativeLayout.LayoutParams(ViewGroup
|
||||
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue();
|
||||
lps.setMargins(margin, margin, margin, margin);
|
||||
|
||||
new ShowcaseView.Builder(this)
|
||||
.withMaterialShowcase()
|
||||
.setStyle(R.style.CustomShowcaseTheme)
|
||||
.setContentTitle(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setTarget(new Target() {
|
||||
@Override
|
||||
public Point getPoint() {
|
||||
View view = drawer.getStickyFooter();
|
||||
int[] location = new int[2];
|
||||
view.getLocationInWindow(location);
|
||||
int x = location[0] + 7 * view.getWidth() / 8;
|
||||
int y = location[1] + view.getHeight() / 2;
|
||||
return new Point(x, y);
|
||||
}
|
||||
})
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps);
|
||||
}
|
||||
}
|
||||
|
||||
private <F extends Fragment & ListHolder> void changeList(F listFragment) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
||||
if (twoPane) {
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void createDrawer(Toolbar toolbar) {
|
||||
final ArrayList<IProfile> profiles = new ArrayList<>();
|
||||
profiles.add(new ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons))
|
||||
.withIdentifier(ADD_IDENTITY)
|
||||
);
|
||||
profiles.add(new ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY)
|
||||
);
|
||||
// Create the AccountHeader
|
||||
accountHeader = new AccountHeaderBuilder()
|
||||
.withActivity(this)
|
||||
.withHeaderBackground(R.drawable.header)
|
||||
.withProfiles(profiles)
|
||||
.withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
|
||||
@Override
|
||||
public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
|
||||
if (current) {
|
||||
// Show QR code in modal dialog
|
||||
final Dialog dialog = new Dialog(MainActivity.this);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
ImageView imageView = new ImageView(MainActivity.this);
|
||||
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this)));
|
||||
imageView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog.addContentView(imageView, new RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
Display display = window.getWindowManager().getDefaultDisplay();
|
||||
Point size = new Point();
|
||||
display.getSize(size);
|
||||
int dim = size.x < size.y ? size.x : size.y;
|
||||
|
||||
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
|
||||
lp.copyFrom(window.getAttributes());
|
||||
lp.width = dim;
|
||||
lp.height = dim;
|
||||
|
||||
window.setAttributes(lp);
|
||||
}
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() {
|
||||
@Override
|
||||
public boolean onProfileChanged(View view, IProfile profile, boolean current) {
|
||||
switch ((int) profile.getIdentifier()) {
|
||||
case ADD_IDENTITY:
|
||||
addIdentityDialog();
|
||||
break;
|
||||
case MANAGE_IDENTITY:
|
||||
BitmessageAddress identity = Singleton.getIdentity(MainActivity.this);
|
||||
if (identity == null) {
|
||||
Toast.makeText(MainActivity.this,
|
||||
R.string.no_identity_warning, LENGTH_LONG).show();
|
||||
} else {
|
||||
Intent show = new Intent(MainActivity.this,
|
||||
AddressDetailActivity.class);
|
||||
show.putExtra(AddressDetailFragment.ARG_ITEM, identity);
|
||||
startActivity(show);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
Object tag = ((ProfileDrawerItem) profile).getTag();
|
||||
if (tag instanceof BitmessageAddress) {
|
||||
Singleton.setIdentity((BitmessageAddress) tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// false if it should close the drawer
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.build();
|
||||
if (profiles.size() > 2) { // There's always the add and manage identity items
|
||||
accountHeader.setActiveProfile(profiles.get(0), true);
|
||||
}
|
||||
|
||||
final ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.archive)
|
||||
.withTag(LABEL_ARCHIVE)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_archive)
|
||||
);
|
||||
drawerItems.add(new DividerDrawerItem());
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts));
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings));
|
||||
|
||||
nodeSwitch = new SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
.withName(R.string.full_node)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
||||
.withChecked(isRunning())
|
||||
.withOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView,
|
||||
boolean isChecked) {
|
||||
if (isChecked) {
|
||||
checkAndStartNode();
|
||||
} else {
|
||||
stopService(new Intent(MainActivity.this, BitmessageService.class));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawer = new DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(accountHeader)
|
||||
.withDrawerItems(drawerItems)
|
||||
.addStickyDrawerItems(nodeSwitch)
|
||||
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
|
||||
@Override
|
||||
public boolean onItemClick(View view, int position, IDrawerItem item) {
|
||||
if (item.getTag() instanceof Label) {
|
||||
selectedLabel = (Label) item.getTag();
|
||||
if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof
|
||||
MessageListFragment) {
|
||||
((MessageListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.item_list)).updateList(selectedLabel);
|
||||
} else {
|
||||
MessageListFragment listFragment = new MessageListFragment();
|
||||
changeList(listFragment);
|
||||
listFragment.updateList(selectedLabel);
|
||||
}
|
||||
return false;
|
||||
} else if (item instanceof Nameable<?>) {
|
||||
Nameable<?> ni = (Nameable<?>) item;
|
||||
switch (ni.getName().getTextRes()) {
|
||||
case R.string.contacts_and_subscriptions:
|
||||
if (!(getSupportFragmentManager().findFragmentById(R.id
|
||||
.item_list) instanceof AddressListFragment)) {
|
||||
changeList(new AddressListFragment());
|
||||
} else {
|
||||
((AddressListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.item_list)).updateList();
|
||||
}
|
||||
break;
|
||||
case R.string.settings:
|
||||
startActivity(new Intent(MainActivity.this, SettingsActivity
|
||||
.class));
|
||||
break;
|
||||
case R.string.full_node:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withShowDrawerOnFirstLaunch(true)
|
||||
.build();
|
||||
|
||||
new AsyncTask<Void, Void, List<BitmessageAddress>>() {
|
||||
@Override
|
||||
protected List<BitmessageAddress> doInBackground(Void... params) {
|
||||
List<BitmessageAddress> identities = bmc.addresses().getIdentities();
|
||||
if (identities.isEmpty()) {
|
||||
// Create an initial identity
|
||||
Singleton.getIdentity(MainActivity.this);
|
||||
}
|
||||
return identities;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<BitmessageAddress> identities) {
|
||||
for (BitmessageAddress identity : identities) {
|
||||
addIdentityEntry(identity);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
|
||||
new AsyncTask<Void, Void, List<Label>>() {
|
||||
@Override
|
||||
protected List<Label> doInBackground(Void... params) {
|
||||
return bmc.messages().getLabels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Label> labels) {
|
||||
if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) {
|
||||
selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL);
|
||||
} else if (selectedLabel == null) {
|
||||
selectedLabel = labels.get(0);
|
||||
}
|
||||
for (Label label : labels) {
|
||||
addLabelEntry(label);
|
||||
}
|
||||
IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel);
|
||||
if (selectedDrawerItem != null) {
|
||||
drawer.setSelection(selectedDrawerItem);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
savedInstanceState.putSerializable("selectedLabel", selectedLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel");
|
||||
|
||||
IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel);
|
||||
if (selectedItem != null) {
|
||||
drawer.setSelection(selectedItem);
|
||||
}
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
private void addIdentityDialog() {
|
||||
AddIdentityDialogFragment dialog = new AddIdentityDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
updateUnread();
|
||||
updateNodeSwitch();
|
||||
Singleton.getMessageListener(this).resetNotification();
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
public void addIdentityEntry(BitmessageAddress identity) {
|
||||
IProfile newProfile = new ProfileDrawerItem()
|
||||
.withIcon(new Identicon(identity))
|
||||
.withName(identity.toString())
|
||||
.withNameShown(true)
|
||||
.withEmail(identity.getAddress())
|
||||
.withTag(identity);
|
||||
if (accountHeader.getProfiles() != null) {
|
||||
// we know that there are 2 setting elements.
|
||||
// Set the new profile above them ;)
|
||||
accountHeader.addProfile(
|
||||
newProfile, accountHeader.getProfiles().size() - 2);
|
||||
} else {
|
||||
accountHeader.addProfiles(newProfile);
|
||||
}
|
||||
}
|
||||
|
||||
public void addLabelEntry(Label label) {
|
||||
PrimaryDrawerItem item = new PrimaryDrawerItem()
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(Labels.getIcon(label))
|
||||
.withIconColor(Labels.getColor(label));
|
||||
drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3);
|
||||
}
|
||||
|
||||
public void updateIdentityEntry(BitmessageAddress identity) {
|
||||
for (IProfile profile : accountHeader.getProfiles()) {
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
if (identity.equals(((ProfileDrawerItem) profile).getTag())) {
|
||||
((ProfileDrawerItem) profile)
|
||||
.withName(identity.toString())
|
||||
.withTag(identity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeIdentityEntry(BitmessageAddress identity) {
|
||||
for (IProfile profile : accountHeader.getProfiles()) {
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
if (identity.equals(((ProfileDrawerItem) profile).getTag())) {
|
||||
accountHeader.removeProfile(profile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndStartNode() {
|
||||
if (Preferences.isConnectionAllowed(MainActivity.this)) {
|
||||
startService(new Intent(this, BitmessageService.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, FullNodeDialogActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUnread() {
|
||||
for (IDrawerItem item : drawer.getDrawerItems()) {
|
||||
if (item.getTag() instanceof Label) {
|
||||
Label label = (Label) item.getTag();
|
||||
if (label != LABEL_ARCHIVE) {
|
||||
int unread = bmc.messages().countUnread(label);
|
||||
if (unread > 0) {
|
||||
((PrimaryDrawerItem) item).withBadge(String.valueOf(unread));
|
||||
} else {
|
||||
((PrimaryDrawerItem) item).withBadge((String) null);
|
||||
}
|
||||
drawer.updateItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateNodeSwitch() {
|
||||
final MainActivity i = getInstance();
|
||||
if (i != null) {
|
||||
i.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
i.nodeSwitch.withChecked(i.bmc.isRunning());
|
||||
i.drawer.updateStickyFooterItem(i.nodeSwitch);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from {@link ListSelectionListener}
|
||||
* indicating that the item with the given ID was selected.
|
||||
*/
|
||||
@Override
|
||||
public void onItemSelected(Serializable item) {
|
||||
if (twoPane) {
|
||||
// In two-pane mode, show the detail view in this activity by
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item);
|
||||
Fragment fragment;
|
||||
if (item instanceof Plaintext)
|
||||
fragment = new MessageDetailFragment();
|
||||
else if (item instanceof BitmessageAddress)
|
||||
fragment = new AddressDetailFragment();
|
||||
else
|
||||
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
|
||||
"was "
|
||||
+ item.getClass().getSimpleName());
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit();
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
Intent detailIntent;
|
||||
if (item instanceof Plaintext) {
|
||||
detailIntent = new Intent(this, MessageDetailActivity.class);
|
||||
detailIntent.putExtra(EXTRA_SHOW_LABEL, selectedLabel);
|
||||
} else if (item instanceof BitmessageAddress) {
|
||||
detailIntent = new Intent(this, AddressDetailActivity.class);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
|
||||
"was "
|
||||
+ item.getClass().getSimpleName());
|
||||
}
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
|
||||
startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTitle(CharSequence title) {
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
public Label getSelectedLabel() {
|
||||
return selectedLabel;
|
||||
}
|
||||
|
||||
public static MainActivity getInstance() {
|
||||
if (instance == null) return null;
|
||||
return instance.get();
|
||||
}
|
||||
}
|
585
app/src/main/java/ch/dissem/apps/abit/MainActivity.kt
Normal file
585
app/src/main/java/ch/dissem/apps/abit/MainActivity.kt
Normal file
@ -0,0 +1,585 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.transaction
|
||||
import ch.dissem.apps.abit.drawer.ProfileImageListener
|
||||
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.getColor
|
||||
import ch.dissem.apps.abit.util.getIcon
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.AccountHeaderBuilder
|
||||
import com.mikepenz.materialdrawer.Drawer
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Nameable
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView
|
||||
import uk.co.deanwild.materialshowcaseview.shape.Shape
|
||||
import uk.co.deanwild.materialshowcaseview.target.Target
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a list of Messages. This activity
|
||||
* has different presentations for handset and tablet-size devices. On
|
||||
* handsets, the activity presents a list of items, which when touched,
|
||||
* lead to a [MessageDetailActivity] representing
|
||||
* item details. On tablets, the activity presents the list of items and
|
||||
* item details side-by-side using two vertical panes.
|
||||
*
|
||||
*
|
||||
* The activity makes heavy use of fragments. The list of items is a
|
||||
* [MessageListFragment] and the item details
|
||||
* (if present) is a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* This activity also implements the required
|
||||
* [ListSelectionListener] interface
|
||||
* to listen for item selections.
|
||||
*
|
||||
*/
|
||||
class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
|
||||
private var active: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
*/
|
||||
var hasDetailPane: Boolean = false
|
||||
private set
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private lateinit var accountHeader: AccountHeader
|
||||
|
||||
private lateinit var drawer: Drawer
|
||||
private lateinit var nodeSwitch: SwitchDrawerItem
|
||||
|
||||
val floatingActionButton: FabSpeedDial?
|
||||
get() = findViewById(R.id.fab)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
instance = WeakReference(this)
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
messageRepo = Singleton.getMessageRepository(this)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
fab.hide()
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val listFragment = ConversationListFragment()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.commit()
|
||||
|
||||
if (findViewById<View>(R.id.message_detail_container) != null) {
|
||||
// The detail container view will be present only in the
|
||||
// large-screen layouts (res/values-large and
|
||||
// res/values-sw600dp). If this view is present, then the
|
||||
// activity should be in two-pane mode.
|
||||
hasDetailPane = true
|
||||
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
|
||||
createDrawer(toolbar)
|
||||
|
||||
// handle intents
|
||||
val intent = intent
|
||||
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE))
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
|
||||
val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
}
|
||||
|
||||
if (drawer.isDrawerOpen) {
|
||||
MaterialShowcaseView.Builder(this)
|
||||
.setMaskColour(R.color.colorPrimary)
|
||||
.setTitleText(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setDismissOnTouch(true)
|
||||
.setDismissText(R.string.got_it)
|
||||
.setShape(object : Shape {
|
||||
var w = 0
|
||||
var h = 0
|
||||
|
||||
override fun updateTarget(target: Target) {
|
||||
w = target.bounds.width()
|
||||
h = target.bounds.height()
|
||||
}
|
||||
|
||||
override fun getHeight() = h
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
|
||||
val r = h.toFloat() / 2
|
||||
canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
|
||||
}
|
||||
|
||||
override fun getWidth() = w
|
||||
})
|
||||
.setTarget(drawer.stickyFooter)
|
||||
.setDelay(1000)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
|
||||
if (active) {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.item_list, listFragment)
|
||||
supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let {
|
||||
transaction.remove(it)
|
||||
}
|
||||
transaction.addToBackStack(null).commit()
|
||||
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDrawer(toolbar: Toolbar) {
|
||||
val profiles = ArrayList<IProfile<*>>()
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(
|
||||
IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons)
|
||||
)
|
||||
.withIdentifier(ADD_IDENTITY.toLong())
|
||||
)
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY.toLong())
|
||||
)
|
||||
// Create the AccountHeader
|
||||
accountHeader = AccountHeaderBuilder()
|
||||
.withActivity(this)
|
||||
.withHeaderBackground(R.drawable.header)
|
||||
.withProfiles(profiles)
|
||||
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
|
||||
.withOnAccountHeaderListener(
|
||||
ProfileSelectionListener(
|
||||
this@MainActivity,
|
||||
supportFragmentManager
|
||||
)
|
||||
)
|
||||
.build()
|
||||
if (profiles.size > 2) { // There's always the add and manage identity items
|
||||
accountHeader.setActiveProfile(profiles[0], true)
|
||||
}
|
||||
|
||||
val drawerItems = ArrayList<IDrawerItem<*, *>>()
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withIdentifier(LABEL_ARCHIVE.id as Long)
|
||||
.withName(R.string.archive)
|
||||
.withTag(LABEL_ARCHIVE)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_archive)
|
||||
)
|
||||
drawerItems.add(DividerDrawerItem())
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts)
|
||||
)
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
)
|
||||
|
||||
nodeSwitch = SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
.withName(R.string.online)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
||||
.withChecked(preferences.online)
|
||||
.withOnCheckedChangeListener { _, _, isChecked ->
|
||||
preferences.online = isChecked
|
||||
if (isChecked) {
|
||||
network.enableNode(true)
|
||||
}
|
||||
}
|
||||
|
||||
drawer = DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(accountHeader)
|
||||
.withDrawerItems(drawerItems)
|
||||
.addStickyDrawerItems(nodeSwitch)
|
||||
.withOnDrawerItemClickListener(DrawerItemClickListener())
|
||||
.withShowDrawerOnFirstLaunch(true)
|
||||
.build()
|
||||
|
||||
loadDrawerItemsAsynchronously()
|
||||
}
|
||||
|
||||
private fun loadDrawerItemsAsynchronously() {
|
||||
doAsync {
|
||||
val identities = bmc.addresses.getIdentities()
|
||||
if (identities.isEmpty()) {
|
||||
// Create an initial identity
|
||||
Singleton.getIdentity(this@MainActivity)
|
||||
}
|
||||
|
||||
uiThread {
|
||||
for (identity in identities) {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAsync {
|
||||
val labels = bmc.labels.getLabels()
|
||||
|
||||
uiThread {
|
||||
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
|
||||
currentLabel.onNext(intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label)
|
||||
} else if (currentLabel.value == null) {
|
||||
currentLabel.onNext(labels[0])
|
||||
}
|
||||
|
||||
for (label in labels) {
|
||||
addLabelEntry(label)
|
||||
}
|
||||
currentLabel.value?.let { label ->
|
||||
drawer.setSelection(label.id as Long)
|
||||
}
|
||||
updateUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
|
||||
override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean {
|
||||
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
val tag = item.tag
|
||||
if (tag is Label) {
|
||||
currentLabel.onNext(tag)
|
||||
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
|
||||
if (itemList !is ConversationListFragment) {
|
||||
changeList(ConversationListFragment())
|
||||
}
|
||||
} else {
|
||||
if (itemList !is MessageListFragment) {
|
||||
changeList(MessageListFragment())
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (item is Nameable<*>) {
|
||||
when (item.name.textRes) {
|
||||
R.string.contacts_and_subscriptions -> {
|
||||
if (itemList is AddressListFragment) {
|
||||
itemList.reloadList()
|
||||
} else {
|
||||
changeList(AddressListFragment())
|
||||
}
|
||||
return false
|
||||
}
|
||||
R.string.settings -> {
|
||||
supportFragmentManager?.transaction {
|
||||
replace(R.id.item_list, SettingsFragment())
|
||||
addToBackStack(null)
|
||||
}
|
||||
return false
|
||||
}
|
||||
R.string.full_node -> return true
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
network.enableNode(false)
|
||||
updateUnread()
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
subscription = currentLabel.subscribe { label ->
|
||||
if (label.id is Long) {
|
||||
drawer.setSelection(label.id as Long, false)
|
||||
}
|
||||
}
|
||||
active = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
active = false
|
||||
}
|
||||
|
||||
fun addIdentityEntry(identity: BitmessageAddress) {
|
||||
val newProfile = ProfileDrawerItem()
|
||||
.withIcon(Identicon(identity))
|
||||
.withName(identity.toString())
|
||||
.withNameShown(true)
|
||||
.withEmail(identity.address)
|
||||
.withTag(identity)
|
||||
if (accountHeader.profiles != null) {
|
||||
// we know that there are 2 setting elements.
|
||||
// Set the new profile above them ;)
|
||||
accountHeader.addProfile(
|
||||
newProfile, accountHeader.profiles.size - 2
|
||||
)
|
||||
} else {
|
||||
accountHeader.addProfiles(newProfile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLabelEntry(label: Label) {
|
||||
val item = PrimaryDrawerItem()
|
||||
.withIdentifier(label.id as Long)
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(label.getIcon())
|
||||
.withIconColor(label.getColor(0xFF000000.toInt()))
|
||||
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
|
||||
}
|
||||
|
||||
fun updateIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
profile
|
||||
.withName(identity.toString())
|
||||
.withTag(identity)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
accountHeader.removeProfile(profile)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnread() {
|
||||
for (item in drawer.drawerItems) {
|
||||
if (item.tag is Label) {
|
||||
val label = item.tag as Label
|
||||
if (label !== LABEL_ARCHIVE) {
|
||||
val unread = messageRepo.countUnread(label, preferences.separateIdentities)
|
||||
if (unread > 0) {
|
||||
(item as PrimaryDrawerItem).withBadge(unread.toString())
|
||||
} else {
|
||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
||||
}
|
||||
runOnUiThread {
|
||||
drawer.updateItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from [ListSelectionListener]
|
||||
* indicating that the item with the given ID was selected.
|
||||
*/
|
||||
override fun onItemSelected(item: Serializable) {
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, show the detail view in this activity by
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
val fragment = when (item) {
|
||||
is Conversation -> {
|
||||
ConversationDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
ComposeMessageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessageDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> {
|
||||
AddressDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
val detailIntent = when (item) {
|
||||
is Conversation -> {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
Intent(this, ComposeMessageActivity::class.java).apply {
|
||||
putExtra(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
} else {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java).apply {
|
||||
putExtra(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDetailView(fragment: Fragment) {
|
||||
if (hasDetailPane) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(title: CharSequence) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
|
||||
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.hide()
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
return fab
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
|
||||
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
|
||||
const val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
|
||||
const val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
|
||||
|
||||
const val ADD_IDENTITY = 1
|
||||
const val MANAGE_IDENTITY = 2
|
||||
|
||||
private const val ID_NODE_SWITCH: Long = 1
|
||||
|
||||
private var instance: WeakReference<MainActivity>? = null
|
||||
|
||||
/**
|
||||
* Runs the given code in the main activity context, if it currently exists. Otherwise,
|
||||
* it's ignored.
|
||||
*/
|
||||
fun apply(run: MainActivity.() -> Unit) {
|
||||
instance?.get()?.let { run.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Message detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a {@link MainActivity}.
|
||||
* <p/>
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a {@link MessageDetailFragment}.
|
||||
*/
|
||||
public class MessageDetailActivity extends DetailActivity {
|
||||
private Label label;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
if (savedInstanceState == null) {
|
||||
label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL);
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
|
||||
getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM));
|
||||
MessageDetailFragment fragment = new MessageDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
Intent parentIntent = new Intent(this, MainActivity.class);
|
||||
parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label);
|
||||
NavUtils.navigateUpTo(this, parentIntent);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NavUtils
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Message detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a [MainActivity].
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a [MessageDetailFragment].
|
||||
*/
|
||||
class MessageDetailActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
val itemId = intent.getSerializableExtra(ConversationDetailFragment.ARG_ITEM_ID)
|
||||
arguments.putSerializable(ConversationDetailFragment.ARG_ITEM_ID, itemId)
|
||||
val fragment = if (item is Plaintext) {
|
||||
MessageDetailFragment()
|
||||
} else {
|
||||
ConversationDetailFragment()
|
||||
}
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
@ -1,367 +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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
import com.mikepenz.iconics.view.IconicsImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
import ch.dissem.apps.abit.util.Drawables;
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||
|
||||
import static android.text.util.Linkify.WEB_URLS;
|
||||
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN;
|
||||
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA;
|
||||
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a {@link MainActivity}
|
||||
* in two-pane mode (on tablets) or a {@link MessageDetailActivity}
|
||||
* on handsets.
|
||||
*/
|
||||
public class MessageDetailFragment extends Fragment {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM = "item";
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private Plaintext item;
|
||||
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public MessageDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments().containsKey(ARG_ITEM)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
item = (Plaintext) getArguments().getSerializable(ARG_ITEM);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_message_detail, container, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
if (item != null) {
|
||||
((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject());
|
||||
ImageView status = (ImageView) rootView.findViewById(R.id.status);
|
||||
status.setImageResource(Assets.getStatusDrawable(item.getStatus()));
|
||||
status.setContentDescription(getString(Assets.getStatusString(item.getStatus())));
|
||||
BitmessageAddress sender = item.getFrom();
|
||||
((ImageView) rootView.findViewById(R.id.avatar))
|
||||
.setImageDrawable(new Identicon(sender));
|
||||
((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString());
|
||||
if (item.getTo() != null) {
|
||||
((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString());
|
||||
} else if (item.getType() == Plaintext.Type.BROADCAST) {
|
||||
((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast);
|
||||
}
|
||||
RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels);
|
||||
LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels());
|
||||
labelView.setAdapter(labelAdapter);
|
||||
labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
|
||||
|
||||
TextView messageBody = (TextView) rootView.findViewById(R.id.text);
|
||||
messageBody.setText(item.getText());
|
||||
|
||||
Linkify.addLinks(messageBody, WEB_URLS);
|
||||
Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
|
||||
new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(Matcher match, String url) {
|
||||
return match.group();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
messageBody.setLinksClickable(true);
|
||||
messageBody.setTextIsSelectable(true);
|
||||
|
||||
boolean removed = false;
|
||||
Iterator<Label> labels = item.getLabels().iterator();
|
||||
while (labels.hasNext()) {
|
||||
if (labels.next().getType() == Label.Type.UNREAD) {
|
||||
labels.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext());
|
||||
if (removed) {
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
messageRepo.save(item);
|
||||
}
|
||||
List<Plaintext> parents = new ArrayList<>(item.getParents().size());
|
||||
for (InventoryVector parentIV : item.getParents()) {
|
||||
Plaintext parent = messageRepo.getMessage(parentIV);
|
||||
if (parent != null) {
|
||||
parents.add(parent);
|
||||
}
|
||||
}
|
||||
showRelatedMessages(rootView, R.id.parents, parents);
|
||||
showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item));
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) {
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id);
|
||||
RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.message, menu);
|
||||
|
||||
Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon
|
||||
.gmd_markunread);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
MessageRepository messageRepo = Singleton.getMessageRepository(getContext());
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.reply:
|
||||
ComposeMessageActivity.launchReplyTo(this, item);
|
||||
return true;
|
||||
case R.id.delete:
|
||||
if (isInTrash(item)) {
|
||||
messageRepo.remove(item);
|
||||
} else {
|
||||
item.getLabels().clear();
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
|
||||
messageRepo.save(item);
|
||||
}
|
||||
getActivity().onBackPressed();
|
||||
return true;
|
||||
case R.id.mark_unread:
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.UNREAD));
|
||||
messageRepo.save(item);
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
return true;
|
||||
case R.id.archive:
|
||||
if (item.isUnread() && getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
item.getLabels().clear();
|
||||
messageRepo.save(item);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInTrash(Plaintext item) {
|
||||
for (Label label : item.getLabels()) {
|
||||
if (label.getType() == Label.Type.TRASH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> {
|
||||
private final List<Plaintext> messages;
|
||||
private final Context ctx;
|
||||
|
||||
private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) {
|
||||
this.messages = messages;
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
// Inflate the custom layout
|
||||
View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false);
|
||||
|
||||
// Return a new holder instance
|
||||
return new RelatedMessageAdapter.ViewHolder(contactView);
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
@Override
|
||||
public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) {
|
||||
// Get the data model based on position
|
||||
Plaintext message = messages.get(position);
|
||||
|
||||
viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom()));
|
||||
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus()));
|
||||
viewHolder.sender.setText(message.getFrom().toString());
|
||||
viewHolder.extract.setText(normalizeWhitespaces(message.getText()));
|
||||
viewHolder.item = message;
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ImageView avatar;
|
||||
private final ImageView status;
|
||||
private final TextView sender;
|
||||
private final TextView extract;
|
||||
private Plaintext item;
|
||||
|
||||
ViewHolder(final View itemView) {
|
||||
super(itemView);
|
||||
avatar = (ImageView) itemView.findViewById(R.id.avatar);
|
||||
status = (ImageView) itemView.findViewById(R.id.status);
|
||||
sender = (TextView) itemView.findViewById(R.id.sender);
|
||||
extract = (TextView) itemView.findViewById(R.id.text);
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (ctx instanceof MainActivity) {
|
||||
((MainActivity) ctx).onItemSelected(item);
|
||||
} else {
|
||||
Intent detailIntent;
|
||||
detailIntent = new Intent(ctx, MessageDetailActivity.class);
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
|
||||
ctx.startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelAdapter extends
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder> {
|
||||
|
||||
private final List<Label> labels;
|
||||
private final Context ctx;
|
||||
|
||||
private LabelAdapter(Context ctx, Set<Label> labels) {
|
||||
this.labels = new ArrayList<>(labels);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
// Inflate the custom layout
|
||||
View contactView = inflater.inflate(R.layout.item_label, parent, false);
|
||||
|
||||
// Return a new holder instance
|
||||
return new ViewHolder(contactView);
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
@Override
|
||||
public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) {
|
||||
// Get the data model based on position
|
||||
Label label = labels.get(position);
|
||||
|
||||
viewHolder.icon.setColor(Labels.getColor(label));
|
||||
viewHolder.icon.setIcon(Labels.getIcon(label));
|
||||
viewHolder.label.setText(Labels.getText(label, ctx));
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return labels.size();
|
||||
}
|
||||
|
||||
// Provide a direct reference to each of the views within a data item
|
||||
// Used to cache the views within the item layout for fast access
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
// Your holder should contain a member variable
|
||||
// for any view that will be set as you render a row
|
||||
public IconicsImageView icon;
|
||||
public TextView label;
|
||||
|
||||
// We also create a constructor that accepts the entire item row
|
||||
// and does the view lookups to find each subview
|
||||
ViewHolder(View itemView) {
|
||||
// Stores the itemView in a public final member variable that can be used
|
||||
// to access the context from any ViewHolder instance.
|
||||
super(itemView);
|
||||
|
||||
icon = (IconicsImageView) itemView.findViewById(R.id.icon);
|
||||
label = (TextView) itemView.findViewById(R.id.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
271
app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.kt
Normal file
271
app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.kt
Normal file
@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.util.Linkify
|
||||
import android.text.util.Linkify.WEB_URLS
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.adapter.LabelAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.apps.abit.util.getString
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_message_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class MessageDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
item = arguments.getSerializable(ARG_ITEM) as Plaintext
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
status.setImageResource(item.status.getDrawable())
|
||||
status.contentDescription = getString(item.status.getString())
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(item.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
sender.text = item.from.toString()
|
||||
item.to?.let { to ->
|
||||
recipient.text = to.toString()
|
||||
} ?: {
|
||||
if (item.type == Plaintext.Type.BROADCAST) {
|
||||
recipient.setText(R.string.broadcast)
|
||||
}
|
||||
}.invoke()
|
||||
val labelAdapter = LabelAdapter(ctx, item.labels)
|
||||
labels.adapter = labelAdapter
|
||||
labels.layoutManager = GridLayoutManager(activity, 2)
|
||||
|
||||
text.text = item.text
|
||||
|
||||
Linkify.addLinks(text, WEB_URLS)
|
||||
Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
text.linksClickable = true
|
||||
text.setTextIsSelectable(true)
|
||||
|
||||
val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
if (item.isUnread()) {
|
||||
Singleton.labeler.markAsRead(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
messageRepo.save(item)
|
||||
}
|
||||
val parents = ArrayList<Plaintext>(item.parents.size)
|
||||
for (parentIV in item.parents) {
|
||||
val parent = messageRepo.getMessage(parentIV)
|
||||
if (parent != null) {
|
||||
parents.add(parent)
|
||||
}
|
||||
}
|
||||
showRelatedMessages(ctx, view, R.id.parents, parents)
|
||||
showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRelatedMessages(
|
||||
ctx: Context,
|
||||
rootView: View, @IdRes id: Int,
|
||||
messages: List<Plaintext>
|
||||
) {
|
||||
val recyclerView = rootView.findViewById<RecyclerView>(id)
|
||||
val adapter = RelatedMessageAdapter(ctx, messages)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply)
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(
|
||||
activity, menu, R.id.mark_unread, GoogleMaterial.Icon
|
||||
.gmd_markunread
|
||||
)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
if (item.isUnread() && activity is MainActivity) {
|
||||
(activity as MainActivity).updateUnread()
|
||||
}
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private class RelatedMessageAdapter internal constructor(
|
||||
private val ctx: Context,
|
||||
private val messages: List<Plaintext>
|
||||
) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RelatedMessageAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = messages[position]
|
||||
|
||||
viewHolder.avatar.setImageDrawable(Identicon(message.from))
|
||||
viewHolder.status.setImageResource(message.status.getDrawable())
|
||||
viewHolder.sender.text = message.from.toString()
|
||||
viewHolder.extract.text = prepareMessageExtract(message.text)
|
||||
viewHolder.item = message
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
override fun getItemCount() = messages.size
|
||||
|
||||
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
internal val avatar = itemView.findViewById<ImageView>(R.id.avatar)
|
||||
internal val status = itemView.findViewById<ImageView>(R.id.status)
|
||||
internal val sender = itemView.findViewById<TextView>(R.id.sender)
|
||||
internal val extract = itemView.findViewById<TextView>(R.id.text)
|
||||
internal var item: Plaintext? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (ctx is MainActivity) {
|
||||
item?.let { ctx.onItemSelected(it) }
|
||||
} else {
|
||||
val detailIntent = Intent(ctx, MessageDetailActivity::class.java)
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
ctx.startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
|
||||
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
|
||||
}
|
||||
}
|
@ -1,336 +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;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
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.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
|
||||
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 java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter;
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||
import io.github.yavski.fabspeeddial.FabSpeedDial;
|
||||
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
|
||||
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY;
|
||||
import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash;
|
||||
|
||||
/**
|
||||
* 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 {@link MessageDetailFragment}.
|
||||
* <p/>
|
||||
* Activities containing this fragment MUST implement the {@link ListSelectionListener}
|
||||
* interface.
|
||||
*/
|
||||
public class MessageListFragment extends Fragment implements ListHolder {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerView.LayoutManager layoutManager;
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private RecyclerView.Adapter wrappedAdapter;
|
||||
private RecyclerViewSwipeManager recyclerViewSwipeManager;
|
||||
private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager;
|
||||
|
||||
private Label currentLabel;
|
||||
private MenuItem emptyTrashMenuItem;
|
||||
private MessageRepository messageRepo;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public MessageListFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
messageRepo = Singleton.getMessageRepository(activity);
|
||||
|
||||
doUpdateList(activity.getSelectedLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateList(Label label) {
|
||||
if (!isResumed()) {
|
||||
currentLabel = label;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Objects.equals(currentLabel, label)) {
|
||||
adapter.setData(label, Collections.<Plaintext>emptyList());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
doUpdateList(label);
|
||||
}
|
||||
|
||||
private void doUpdateList(final Label label) {
|
||||
if (label == null) {
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name));
|
||||
}
|
||||
adapter.setData(null, Collections.<Plaintext>emptyList());
|
||||
adapter.notifyDataSetChanged();
|
||||
return;
|
||||
}
|
||||
currentLabel = label;
|
||||
if (emptyTrashMenuItem != null) {
|
||||
emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH);
|
||||
}
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
ActionBarListener actionBarListener = (ActionBarListener) getActivity();
|
||||
if ("archive".equals(label.toString())) {
|
||||
actionBarListener.updateTitle(getString(R.string.archive));
|
||||
} else {
|
||||
actionBarListener.updateTitle(label.toString());
|
||||
}
|
||||
}
|
||||
new AsyncTask<Void, Void, List<Plaintext>>() {
|
||||
|
||||
@Override
|
||||
protected List<Plaintext> doInBackground(Void... params) {
|
||||
return messageRepo.findMessages(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Plaintext> messages) {
|
||||
if (adapter != null) {
|
||||
adapter.setData(label, messages);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_message_list, container, false);
|
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id
|
||||
.fab_compose_message);
|
||||
fab.setMenuListener(new SimpleMenuListenerAdapter() {
|
||||
@Override
|
||||
public boolean onMenuItemSelected(MenuItem menuItem) {
|
||||
BitmessageAddress identity = Singleton.getIdentity(getActivity());
|
||||
if (identity == null) {
|
||||
Toast.makeText(getActivity(), R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
} else {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_compose_message: {
|
||||
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
|
||||
intent.putExtra(EXTRA_IDENTITY, identity);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_compose_broadcast: {
|
||||
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
|
||||
intent.putExtra(EXTRA_IDENTITY, identity);
|
||||
intent.putExtra(EXTRA_BROADCAST, true);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
|
||||
// animation is running)
|
||||
recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager();
|
||||
recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning
|
||||
(true);
|
||||
recyclerViewTouchActionGuardManager.setEnabled(true);
|
||||
|
||||
// swipe manager
|
||||
recyclerViewSwipeManager = new RecyclerViewSwipeManager();
|
||||
|
||||
//adapter
|
||||
adapter = new SwipeableMessageAdapter();
|
||||
adapter.setActivateOnItemClick(activateOnItemClick);
|
||||
adapter.setEventListener(new SwipeableMessageAdapter.EventListener() {
|
||||
@Override
|
||||
public void onItemDeleted(Plaintext item) {
|
||||
if (isInTrash(item)) {
|
||||
messageRepo.remove(item);
|
||||
} else {
|
||||
item.getLabels().clear();
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
|
||||
messageRepo.save(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemArchived(Plaintext item) {
|
||||
item.getLabels().clear();
|
||||
messageRepo.save(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemViewClicked(View v) {
|
||||
int position = recyclerView.getChildAdapterPosition(v);
|
||||
adapter.setSelectedPosition(position);
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
Plaintext item = adapter.getItem(position);
|
||||
((MainActivity) getActivity()).onItemSelected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// wrap for swiping
|
||||
wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter);
|
||||
|
||||
final GeneralItemAnimator animator = new 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.setSupportsChangeAnimations(false);
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(wrappedAdapter); // requires *wrapped* adapter
|
||||
recyclerView.setItemAnimator(animator);
|
||||
|
||||
recyclerView.addItemDecoration(new SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(getContext(), R.drawable.list_divider_h), true));
|
||||
|
||||
// NOTE:
|
||||
// The initialization order is very important! This order determines the priority of
|
||||
// touch event handling.
|
||||
//
|
||||
// priority: TouchActionGuard > Swipe > DragAndDrop
|
||||
recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView);
|
||||
recyclerViewSwipeManager.attachRecyclerView(recyclerView);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (recyclerViewSwipeManager != null) {
|
||||
recyclerViewSwipeManager.release();
|
||||
recyclerViewSwipeManager = null;
|
||||
}
|
||||
|
||||
if (recyclerViewTouchActionGuardManager != null) {
|
||||
recyclerViewTouchActionGuardManager.release();
|
||||
recyclerViewTouchActionGuardManager = null;
|
||||
}
|
||||
|
||||
if (recyclerView != null) {
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setAdapter(null);
|
||||
recyclerView = null;
|
||||
}
|
||||
|
||||
if (wrappedAdapter != null) {
|
||||
WrapperAdapterUtils.releaseAll(wrappedAdapter);
|
||||
wrappedAdapter = null;
|
||||
}
|
||||
adapter = null;
|
||||
layoutManager = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.message_list, menu);
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.empty_trash:
|
||||
if (currentLabel.getType() != Label.Type.TRASH) return true;
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
for (Plaintext message : messageRepo.findMessages(currentLabel)) {
|
||||
messageRepo.remove(message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
updateList(currentLabel);
|
||||
}
|
||||
}.execute();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
if (adapter != null) {
|
||||
adapter.setActivateOnItemClick(activateOnItemClick);
|
||||
}
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
}
|
||||
}
|
308
app/src/main/java/ch/dissem/apps/abit/MessageListFragment.kt
Normal file
308
app/src/main/java/ch/dissem/apps/abit/MessageListFragment.kt
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.adapter.EventListener
|
||||
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||
import org.jetbrains.anko.*
|
||||
import java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
|
||||
|
||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - PAGE_SIZE
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private var deleteAllMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableMessageAdapter?.activateOnItemClick = activateOnItemClick
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableMessageAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val label = currentLabel.value
|
||||
val messages = messageRepo.findMessages(
|
||||
label,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE,
|
||||
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
|
||||
)
|
||||
uiThread {
|
||||
messageAdapter.addAll(messages)
|
||||
isLoading = false
|
||||
isLastPage = messages.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
|
||||
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun reloadList() = doUpdateList(currentLabel.value)
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
// If the menu item isn't available yet, we should wait - the method will be called again once it's
|
||||
// initialized.
|
||||
emptyTrashMenuItem?.let { menuItem ->
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableMessageAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableMessageAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
menuItem.isVisible = label.type == Label.Type.TRASH
|
||||
MainActivity.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val context = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recycler_view.layoutManager = layoutManager
|
||||
swipeableMessageAdapter = SwipeableMessageAdapter(context).apply {
|
||||
activateOnItemClick = this@MessageListFragment.activateOnItemClick
|
||||
}
|
||||
recycler_view.adapter = swipeableMessageAdapter // requires *wrapped* swipeableMessageAdapter
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
val dirs = when (currentLabel.value?.type) {
|
||||
Label.Type.TRASH -> ItemTouchHelper.LEFT
|
||||
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
|
||||
}
|
||||
|
||||
val swipeHandler = SwipeToDeleteCallback(context, dirs, object : EventListener {
|
||||
override fun onItemDeleted(position: Int) {
|
||||
context.toast("Deleted")
|
||||
}
|
||||
|
||||
override fun onItemArchived(position: Int) {
|
||||
context.toast("Archived")
|
||||
}
|
||||
|
||||
override fun onItemSelected(position: Int) {
|
||||
context.toast("Selected")
|
||||
}
|
||||
})
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||
itemTouchHelper.attachToRecyclerView(recycler_view)
|
||||
|
||||
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
swipeableMessageAdapter = null
|
||||
layoutManager = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
deleteAllMenuItem = menu.findItem(R.id.delete_all)
|
||||
// currentLabel.value?.let { doUpdateList(it) }
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete_all -> {
|
||||
currentLabel.value?.let { label ->
|
||||
context?.apply {
|
||||
alert(
|
||||
R.string.delete_all_messages_in_list,
|
||||
R.string.delete_all_messages_in_list_ask
|
||||
) {
|
||||
positiveButton(R.string.delete) {
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
cancelButton { }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAllMessages(label: Label) {
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.onNext(label)
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.onNext(backStack.pop())
|
||||
true
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SettingsActivity extends DetailActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Display the fragment as the main content.
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
@ -1,140 +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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mikepenz.aboutlibraries.Libs;
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder;
|
||||
|
||||
import ch.dissem.apps.abit.repository.AndroidNodeRegistry;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW;
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SettingsFragment
|
||||
extends PreferenceFragment
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
Preference about = findPreference("about");
|
||||
about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new LibsBuilder()
|
||||
.withActivityTitle(getActivity().getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutDescription(getString(R.string.about_app))
|
||||
.start(getActivity());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
final Preference cleanup = findPreference("cleanup");
|
||||
cleanup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
private Context ctx = getActivity().getApplicationContext();
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
cleanup.setEnabled(false);
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast
|
||||
.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
|
||||
bmc.cleanup();
|
||||
bmc.internals().getNodeRegistry().clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
R.string.cleanup_notification_end,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
cleanup.setEnabled(true);
|
||||
}
|
||||
}.execute();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Preference status = findPreference("status");
|
||||
status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivity(new Intent(getActivity(), StatusActivity.class));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context ctx) {
|
||||
super.onAttach(ctx);
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
switch (key) {
|
||||
case PREFERENCE_TRUSTED_NODE:
|
||||
String node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null);
|
||||
if (node != null) {
|
||||
SyncAdapter.startSync(getActivity());
|
||||
} else {
|
||||
SyncAdapter.stopSync(getActivity());
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_SERVER_POW:
|
||||
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
|
||||
SyncAdapter.startPowSync(getActivity());
|
||||
} else {
|
||||
SyncAdapter.stopPowSync(getActivity());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
265
app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt
Normal file
265
app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt
Normal file
@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider.getUriForFile
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import ch.dissem.apps.abit.service.BatchProcessorService
|
||||
import ch.dissem.apps.abit.service.SimpleJob
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Exports
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.indeterminateProgressDialog
|
||||
import org.jetbrains.anko.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
|
||||
findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
|
||||
findPreference("export")?.onPreferenceClickListener = exportClickListener()
|
||||
findPreference("import")?.onPreferenceClickListener = importClickListener()
|
||||
findPreference("status")?.onPreferenceClickListener = statusClickListener()
|
||||
|
||||
connectivityChangeListener().let {
|
||||
findPreference("wifi_only")?.onPreferenceChangeListener = it
|
||||
findPreference("require_charging")?.onPreferenceChangeListener = it
|
||||
}
|
||||
|
||||
val emulateConversations = findPreference("emulate_conversations") as? SwitchPreferenceCompat
|
||||
val conversationInit = findPreference("emulate_conversations_initialize")
|
||||
|
||||
emulateConversations?.onPreferenceChangeListener = emulateConversationChangeListener(conversationInit)
|
||||
conversationInit?.onPreferenceClickListener = conversationInitClickListener()
|
||||
conversationInit?.isEnabled = emulateConversations?.isChecked ?: false
|
||||
}
|
||||
|
||||
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
|
||||
(activity as? MainActivity)?.let { activity ->
|
||||
val libsBuilder = LibsBuilder()
|
||||
.withActivityTitle(activity.getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutDescription(getString(R.string.about_app))
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(libsBuilder.supportFragment())
|
||||
} else {
|
||||
libsBuilder.start(activity)
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
cleanup.isEnabled = false
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
|
||||
|
||||
doAsync {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
bmc.internals.nodeRegistry.clear()
|
||||
bmc.cleanup()
|
||||
ctx.preferences.cleanupExportDirectory()
|
||||
|
||||
uiThread {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
R.string.cleanup_notification_end,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
cleanup.isEnabled = true
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun exportClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity ?: throw IllegalStateException("No context available")
|
||||
|
||||
ctx.indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
|
||||
doAsync {
|
||||
val exportDirectory = ctx.preferences.exportDirectory
|
||||
exportDirectory.mkdirs()
|
||||
val file = Exports.exportData(exportDirectory, ctx)
|
||||
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
|
||||
val intent = Intent(android.content.Intent.ACTION_SEND)
|
||||
intent.type = "application/zip"
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun importClickListener() = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "application/zip"
|
||||
|
||||
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun statusClickListener() = Preference.OnPreferenceClickListener {
|
||||
val activity = activity as MainActivity
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(StatusFragment())
|
||||
} else {
|
||||
activity.startActivity<StatusActivity>()
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val ctx = activity ?: throw IllegalStateException("No context available")
|
||||
when (requestCode) {
|
||||
WRITE_EXPORT_REQUEST_CODE -> ctx.preferences.cleanupExportDirectory()
|
||||
READ_IMPORT_REQUEST_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||
ctx.indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
|
||||
doAsync {
|
||||
Exports.importData(data.data!!, ctx)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(ctx: Context?) {
|
||||
super.onAttach(ctx)
|
||||
ctx?.let {
|
||||
if (it is MainActivity) {
|
||||
it.floatingActionButton?.hide()
|
||||
it.updateTitle(getString(R.string.settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
if (service is BatchProcessorService.BatchBinder) {
|
||||
val messageRepo = Singleton.getMessageRepository(service.service)
|
||||
val conversationService = Singleton.getConversationService(service.service)
|
||||
|
||||
service.process(
|
||||
SimpleJob<Plaintext>(
|
||||
messageRepo.count(),
|
||||
{ messageRepo.findNextLegacyMessages(it) },
|
||||
{ msg ->
|
||||
if (msg.encoding == Plaintext.Encoding.SIMPLE) {
|
||||
conversationService.getSubject(listOf(msg))?.let { subject ->
|
||||
msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(msg)
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
},
|
||||
R.drawable.ic_notification_batch,
|
||||
R.string.emulate_conversations_batch
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun conversationInitClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
ctx.bindService(Intent(ctx, BatchProcessorService::class.java), connection, Context.BIND_AUTO_CREATE)
|
||||
true
|
||||
}
|
||||
|
||||
private fun emulateConversationChangeListener(conversationInit: Preference?) =
|
||||
Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
conversationInit?.isEnabled = newValue as Boolean
|
||||
true
|
||||
}
|
||||
|
||||
private fun connectivityChangeListener() =
|
||||
Preference.OnPreferenceChangeListener { _, _ ->
|
||||
activity?.network?.scheduleNodeStart()
|
||||
true
|
||||
}
|
||||
|
||||
// The why-is-it-so-damn-hard-to-group-preferences section
|
||||
// FIXME: maybe this is once again necessary, maybe not. Test!
|
||||
// override fun getCallbackFragment(): Fragment = this
|
||||
//
|
||||
// override fun onPreferenceStartScreen(
|
||||
// preferenceFragmentCompat: PreferenceFragment,
|
||||
// preferenceScreen: PreferenceScreen
|
||||
// ): Boolean {
|
||||
// fragmentManager?.beginTransaction()?.let { ft ->
|
||||
// val fragment = SettingsFragment()
|
||||
// fragment.arguments = Bundle().apply {
|
||||
// putString(PreferenceFragment.ARG_PREFERENCE_ROOT, preferenceScreen.key)
|
||||
// }
|
||||
// ft.add(R.id.item_list, fragment, preferenceScreen.key)
|
||||
// ft.addToBackStack(preferenceScreen.key)
|
||||
// ft.commit()
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated(view, savedInstanceState)
|
||||
// context?.let { ctx -> view.setBackgroundColor(ContextCompat.getColor(ctx, R.color.contentBackground)) }
|
||||
// }
|
||||
// End of the why-is-it-so-damn-hard-to-group-preferences section
|
||||
// Afterthought: here it looks so simple: https://developer.android.com/guide/topics/ui/settings.html
|
||||
// Remind me, why do we need to use PreferenceFragmentCompat?
|
||||
|
||||
companion object {
|
||||
const val WRITE_EXPORT_REQUEST_CODE = 1
|
||||
const val READ_IMPORT_REQUEST_CODE = 2
|
||||
}
|
||||
}
|
@ -1,61 +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;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.materialize.MaterializeBuilder;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
public class StatusActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_status);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(false);
|
||||
|
||||
new MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build();
|
||||
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(this);
|
||||
StringBuilder status = new StringBuilder();
|
||||
for (BitmessageAddress address : bmc.addresses().getIdentities()) {
|
||||
status.append(address.getAddress()).append('\n');
|
||||
}
|
||||
status.append('\n');
|
||||
status.append(bmc.status());
|
||||
((TextView) findViewById(R.id.content)).setText(status);
|
||||
}
|
||||
|
||||
}
|
54
app/src/main/java/ch/dissem/apps/abit/StatusActivity.kt
Normal file
54
app/src/main/java/ch/dissem/apps/abit/StatusActivity.kt
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.activity_status.*
|
||||
|
||||
class StatusActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_status)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(this)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
content.text = status
|
||||
}
|
||||
|
||||
}
|
48
app/src/main/java/ch/dissem/apps/abit/StatusFragment.kt
Normal file
48
app/src/main/java/ch/dissem/apps/abit/StatusFragment.kt
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
|
||||
class StatusFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_status, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
view.findViewById<TextView>(R.id.content).text = status
|
||||
}
|
||||
|
||||
}
|
@ -1,99 +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.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AddressSelectorAdapter
|
||||
extends RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder> {
|
||||
|
||||
private final List<Selectable<BitmessageAddress>> data;
|
||||
|
||||
public AddressSelectorAdapter(List<BitmessageAddress> identities) {
|
||||
data = new ArrayList<>(identities.size());
|
||||
for (BitmessageAddress identity : identities) {
|
||||
data.add(new Selectable<>(identity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View v = inflater.inflate(R.layout.select_identity_row, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
Selectable<BitmessageAddress> selectable = data.get(position);
|
||||
holder.data = selectable;
|
||||
holder.checkbox.setChecked(selectable.selected);
|
||||
holder.checkbox.setText(selectable.data.toString());
|
||||
holder.address.setText(selectable.data.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public Selectable<BitmessageAddress> data;
|
||||
public final CheckBox checkbox;
|
||||
public final TextView address;
|
||||
|
||||
private ViewHolder(View v) {
|
||||
super(v);
|
||||
checkbox = (CheckBox) v.findViewById(R.id.checkbox);
|
||||
address = (TextView) v.findViewById(R.id.address);
|
||||
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
|
||||
if (data != null) {
|
||||
data.selected = isChecked;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public List<BitmessageAddress> getSelected() {
|
||||
List<BitmessageAddress> result = new LinkedList<>();
|
||||
for (Selectable<BitmessageAddress> selectable : data) {
|
||||
if (selectable.selected) {
|
||||
result.add(selectable.data);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
|
||||
|
||||
private val data = identities.asSequence().map { Selectable(it) }.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.select_identity_row, parent, false)
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val selectable = data[position]
|
||||
holder.data = selectable
|
||||
holder.checkbox.isChecked = selectable.selected
|
||||
holder.checkbox.text = selectable.data.toString()
|
||||
holder.address.text = selectable.data.address
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) {
|
||||
var data: Selectable<BitmessageAddress>? = null
|
||||
val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!!
|
||||
val address = v.findViewById<TextView>(R.id.address)!!
|
||||
|
||||
init {
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
data?.selected = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selected: List<BitmessageAddress>
|
||||
get() {
|
||||
return data.asSequence()
|
||||
.filter { it.selected }
|
||||
.mapTo(LinkedList()) { it.data }
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
public class AndroidCryptography extends SpongyCryptography {
|
||||
public AndroidCryptography() {
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
}
|
@ -1,140 +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.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* An adapter for contacts. Can be filtered by alias or address.
|
||||
*/
|
||||
public class ContactAdapter extends BaseAdapter implements Filterable {
|
||||
private final LayoutInflater inflater;
|
||||
private final List<BitmessageAddress> originalData;
|
||||
private List<BitmessageAddress> data;
|
||||
|
||||
public ContactAdapter(Context ctx) {
|
||||
inflater = LayoutInflater.from(ctx);
|
||||
originalData = Singleton.getAddressRepository(ctx).getContacts();
|
||||
data = originalData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.contact_row, parent, false);
|
||||
}
|
||||
BitmessageAddress item = getItem(position);
|
||||
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item));
|
||||
((TextView) convertView.findViewById(R.id.name)).setText(item.toString());
|
||||
((TextView) convertView.findViewById(R.id.address)).setText(item.getAddress());
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return new ContactFilter();
|
||||
}
|
||||
|
||||
private class ContactFilter extends Filter {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence prefix) {
|
||||
FilterResults results = new FilterResults();
|
||||
|
||||
if (prefix == null || prefix.length() == 0) {
|
||||
results.values = originalData;
|
||||
results.count = originalData.size();
|
||||
} else {
|
||||
String prefixString = prefix.toString().toLowerCase();
|
||||
|
||||
final ArrayList<BitmessageAddress> newValues = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < originalData.size(); i++) {
|
||||
final BitmessageAddress value = originalData.get(i);
|
||||
|
||||
// First match against the whole, non-splitted value
|
||||
if (value.getAlias() != null) {
|
||||
String alias = value.getAlias().toLowerCase();
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value);
|
||||
} else {
|
||||
final String[] words = alias.split(" ");
|
||||
|
||||
for (String word : words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
newValues.add(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String address = value.getAddress().toLowerCase();
|
||||
if (address.contains(prefixString)) {
|
||||
newValues.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues;
|
||||
results.count = newValues.size();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
//noinspection unchecked
|
||||
data = (List<BitmessageAddress>) results.values;
|
||||
if (results.count > 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
145
app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.kt
Normal file
145
app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.kt
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* An adapter for contacts. Can be filtered by alias or address.
|
||||
*/
|
||||
class ContactAdapter(
|
||||
ctx: Context,
|
||||
private val originalData: List<BitmessageAddress>,
|
||||
private val slim: Boolean = false
|
||||
) :
|
||||
BaseAdapter(), Filterable {
|
||||
private val inflater = LayoutInflater.from(ctx)
|
||||
private var data: List<BitmessageAddress> = originalData
|
||||
|
||||
override fun getCount() = data.size
|
||||
|
||||
override fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val viewHolder = if (convertView == null) {
|
||||
ViewHolder(
|
||||
inflater.inflate(
|
||||
if (slim) {
|
||||
R.layout.contact_row_slim
|
||||
} else {
|
||||
R.layout.contact_row
|
||||
},
|
||||
parent, false
|
||||
)
|
||||
)
|
||||
} else {
|
||||
convertView.tag as ViewHolder
|
||||
}
|
||||
val item = getItem(position)
|
||||
viewHolder.avatar.setImageDrawable(Identicon(item))
|
||||
viewHolder.name.text = item.toString()
|
||||
viewHolder.address?.text = item.address
|
||||
|
||||
return viewHolder.view
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter = ContactFilter()
|
||||
|
||||
private inner class ViewHolder(val view: View) {
|
||||
val avatar: ImageView = view.findViewById(R.id.avatar)
|
||||
val name: TextView = view.findViewById(R.id.name)
|
||||
val address: TextView? = view.findViewById(R.id.address)
|
||||
|
||||
init {
|
||||
view.tag = this
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class ContactFilter : Filter() {
|
||||
override fun performFiltering(prefix: CharSequence?): Filter.FilterResults {
|
||||
val results = Filter.FilterResults()
|
||||
|
||||
if (prefix?.isEmpty() == false) {
|
||||
val prefixString = prefix.toString().toLowerCase()
|
||||
|
||||
val newValues = ArrayList<BitmessageAddress>()
|
||||
|
||||
originalData
|
||||
.forEach { value ->
|
||||
value.alias?.toLowerCase()?.let { alias ->
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
} else {
|
||||
val words =
|
||||
alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
|
||||
for (word in words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
val address = value.address.toLowerCase()
|
||||
if (address.contains(prefixString)) {
|
||||
newValues.add(value)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
if (newValues.isEmpty()) {
|
||||
try {
|
||||
newValues.add(BitmessageAddress(prefix.toString()))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues
|
||||
results.count = newValues.size
|
||||
} else {
|
||||
results.values = originalData
|
||||
results.count = originalData.size
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data = results.values as List<BitmessageAddress>
|
||||
if (results.count > 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.text.util.Linkify
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.*
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Constants
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
|
||||
|
||||
class ConversationAdapter internal constructor(
|
||||
ctx: Context,
|
||||
private val parent: Fragment,
|
||||
conversation: Conversation,
|
||||
label: BehaviorSubject<Label>
|
||||
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
|
||||
|
||||
private val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
|
||||
private var filteredMessages = label.value?.let { l -> conversation.messages.filter { m -> m.labels.any { it == l } } } ?: emptyList()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ConversationAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val messageView = inflater.inflate(R.layout.item_message_detail, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(messageView, this.parent, messageRepo)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: ConversationAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = filteredMessages[position]
|
||||
|
||||
viewHolder.apply {
|
||||
item = message
|
||||
avatar.setImageDrawable(Identicon(message.from))
|
||||
sender.text = message.from.toString()
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(message.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
|
||||
recipient.text = message.to.toString()
|
||||
status.setImageResource(message.status.getDrawable())
|
||||
text.text = message.text
|
||||
|
||||
Linkify.addLinks(text, Linkify.WEB_URLS)
|
||||
Linkify.addLinks(text,
|
||||
Constants.BITMESSAGE_ADDRESS_PATTERN,
|
||||
Constants.BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
labelAdapter.labels = message.labels.toList()
|
||||
|
||||
// FIXME: I think that's not quite correct
|
||||
if (message.isUnread()) {
|
||||
Singleton.labeler.markAsRead(message)
|
||||
messageRepo.save(message)
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = filteredMessages.size
|
||||
|
||||
inner class ViewHolder(
|
||||
itemView: View,
|
||||
parent: Fragment,
|
||||
messageRepo: MessageRepository
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var item: Plaintext? = null
|
||||
val avatar = itemView.findViewById<ImageView>(R.id.avatar)!!
|
||||
val sender = itemView.findViewById<TextView>(R.id.sender)!!
|
||||
val recipient = itemView.findViewById<TextView>(R.id.recipient)!!
|
||||
val status = itemView.findViewById<ImageView>(R.id.status)!!
|
||||
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.apply {
|
||||
setOnClickListener { view ->
|
||||
PopupMenu(itemView.context, view).apply {
|
||||
|
||||
menuInflater.inflate(R.menu.message, menu)
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(parent, item)
|
||||
true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (MessageDetailFragment.isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
filteredMessages.indexOf(item).let { i ->
|
||||
filteredMessages -= item
|
||||
notifyItemRemoved(i)
|
||||
}
|
||||
MainActivity.apply {
|
||||
updateUnread()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
R.id.archive -> {
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
show()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
|
||||
linksClickable = true
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
val labelAdapter = LabelAdapter(itemView.context, emptySet())
|
||||
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
|
||||
adapter = labelAdapter
|
||||
layoutManager = GridLayoutManager(itemView.context, 2)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.getColor
|
||||
import ch.dissem.apps.abit.util.getIcon
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
|
||||
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
var labels = labels.toList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.icon(label.getIcon())
|
||||
viewHolder.label.text = label.getText(ctx)
|
||||
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
|
||||
fun setBackground(@ColorInt color: Int) {
|
||||
itemView.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,16 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter;
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Selectable<T> {
|
||||
final T data;
|
||||
boolean selected = false;
|
||||
|
||||
Selectable(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
class Selectable<out T>(val data: T) {
|
||||
var selected = false
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Copyright 2015 Haruki Hasegawa
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MultiIdenticon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.apps.abit.util.getString
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||
*
|
||||
* @author Christian Basler
|
||||
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
*/
|
||||
abstract class SwipeableAdapter<T, H>(val ctx: Context) :
|
||||
RecyclerView.Adapter<H>() where H : SwipeableAdapter.AbstractViewHolder {
|
||||
|
||||
protected val data = LinkedList<T>()
|
||||
var eventListener: EventListener? = null
|
||||
|
||||
protected var label: Label? = null
|
||||
var selectedPosition = -1
|
||||
set(value) {
|
||||
val oldPosition = field
|
||||
field = value
|
||||
notifyItemChanged(oldPosition)
|
||||
notifyItemChanged(value)
|
||||
}
|
||||
var activateOnItemClick: Boolean = false
|
||||
|
||||
protected val labelUnknown: String = ctx.getString(R.string.unknown)
|
||||
|
||||
open class AbstractViewHolder(v: View, adapter: SwipeableAdapter<*, *>) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
val container = v.findViewById<FrameLayout>(R.id.container)!!
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
|
||||
container.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: H, position: Int) {
|
||||
val item = data[position]
|
||||
|
||||
holder.apply {
|
||||
if (activateOnItemClick) {
|
||||
container.setBackgroundResource(
|
||||
if (position == selectedPosition)
|
||||
R.drawable.bg_item_selected_state
|
||||
else
|
||||
R.drawable.bg_item_normal_state
|
||||
)
|
||||
}
|
||||
|
||||
setData(holder, item)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun setData(holder: H, item: T)
|
||||
|
||||
fun add(item: T) {
|
||||
val index = data.size
|
||||
data.add(item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
|
||||
fun addFirst(item: T) {
|
||||
data.addFirst(item)
|
||||
notifyItemInserted(0)
|
||||
}
|
||||
|
||||
fun addAll(items: Collection<T>) {
|
||||
val index = data.size
|
||||
data.addAll(items)
|
||||
notifyItemRangeInserted(index, items.size)
|
||||
}
|
||||
|
||||
fun remove(item: T) {
|
||||
val itemId = getItemId(item)
|
||||
val index = data.indexOfFirst { getItemId(it) == itemId }
|
||||
if (index >= 0) {
|
||||
removeAt(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
data.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItemId(data[position])
|
||||
}
|
||||
|
||||
abstract fun getItemId(item: T): Long
|
||||
|
||||
abstract fun update(item: T)
|
||||
|
||||
fun clear(newLabel: Label?) {
|
||||
label = newLabel
|
||||
data.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
}
|
||||
|
||||
class SwipeableConversationAdapter(ctx: Context) : SwipeableAdapter<Conversation, SwipeableConversationAdapter.ViewHolder>(ctx) {
|
||||
override fun getItemId(item: Conversation) = item.id.leastSignificantBits
|
||||
|
||||
override fun update(item: Conversation) {
|
||||
val index = data.indexOfFirst { it.id == item.id }
|
||||
if (index >= 0) {
|
||||
data[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.conversation_row, parent, false)
|
||||
return ViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun setData(holder: ViewHolder, item: Conversation) {
|
||||
holder.apply {
|
||||
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||
|
||||
sender.text = item.participants.sortedBy {
|
||||
(it.alias?.let { 0 } ?: 1) + if (it.isChan) 2 else 0
|
||||
}.map { it.alias ?: labelUnknown }.distinct().joinToString()
|
||||
subject.text = prepareMessageExtract(item.subject)
|
||||
extract.text = prepareMessageExtract(item.extract)
|
||||
item.messages.count { it.labels.contains(label) }.let { size ->
|
||||
if (size <= 1) {
|
||||
count.text = ""
|
||||
} else {
|
||||
count.text = size.toString()
|
||||
}
|
||||
}
|
||||
if (item.hasUnread()) {
|
||||
sender.typeface = Typeface.DEFAULT_BOLD
|
||||
subject.typeface = Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
sender.typeface = Typeface.DEFAULT
|
||||
subject.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(v: View, adapter: SwipeableConversationAdapter) : AbstractViewHolder(v, adapter) {
|
||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||
val count = v.findViewById<TextView>(R.id.count)!!
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeableMessageAdapter(ctx: Context) : SwipeableAdapter<Plaintext, SwipeableMessageAdapter.ViewHolder>(ctx) {
|
||||
override fun getItemId(item: Plaintext) = item.id as Long
|
||||
|
||||
override fun update(item: Plaintext) {
|
||||
val index = data.indexOfFirst { it.id == item.id }
|
||||
if (index >= 0) {
|
||||
data[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.message_row, parent, false)
|
||||
return ViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun setData(holder: ViewHolder, item: Plaintext) {
|
||||
holder.apply {
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
status.setImageResource(item.status.getDrawable())
|
||||
status.contentDescription = holder.status.context.getString(item.status.getString())
|
||||
|
||||
sender.text = item.from.toString()
|
||||
subject.text = prepareMessageExtract(item.subject)
|
||||
extract.text = prepareMessageExtract(item.text)
|
||||
if (item.isUnread()) {
|
||||
sender.typeface = Typeface.DEFAULT_BOLD
|
||||
subject.typeface = Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
sender.typeface = Typeface.DEFAULT
|
||||
subject.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(v: View, adapter: SwipeableMessageAdapter) : AbstractViewHolder(v, adapter) {
|
||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeToDeleteCallback(ctx: Context, swipeDirs: Int, private val eventListener: EventListener) : ItemTouchHelper.SimpleCallback(0, swipeDirs) {
|
||||
|
||||
private val backgroundLeft = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_left)!!
|
||||
private val backgroundRight = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_right)!!
|
||||
private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
|
||||
|
||||
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
/**
|
||||
* To disable "swipe" for specific item return 0 here.
|
||||
* For example:
|
||||
* if (viewHolder?.itemViewType == YourAdapter.SOME_TYPE) return 0
|
||||
* if (viewHolder?.adapterPosition == 0) return 0
|
||||
*/
|
||||
if (viewHolder.adapterPosition == 10) return 0
|
||||
return super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
|
||||
) {
|
||||
|
||||
val itemView = viewHolder.itemView
|
||||
val isCanceled = dX == 0f && !isCurrentlyActive
|
||||
|
||||
if (isCanceled) {
|
||||
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
return
|
||||
}
|
||||
|
||||
if (dX < 0) {
|
||||
backgroundLeft.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
|
||||
backgroundLeft.draw(c)
|
||||
} else {
|
||||
backgroundRight.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
|
||||
backgroundRight.draw(c)
|
||||
}
|
||||
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
}
|
||||
|
||||
private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
|
||||
c?.drawRect(left, top, right, bottom, clearPaint)
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> eventListener.onItemDeleted(viewHolder.adapterPosition)
|
||||
ItemTouchHelper.RIGHT -> eventListener.onItemArchived(viewHolder.adapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onItemDeleted(position: Int)
|
||||
|
||||
fun onItemArchived(position: Int)
|
||||
|
||||
fun onItemSelected(position: Int)
|
||||
}
|
@ -1,329 +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 com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE;
|
||||
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
|
||||
|
||||
/**
|
||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||
*
|
||||
* @author Christian Basler
|
||||
* @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview">
|
||||
* https://github.com/h6ah4i/android-advancedrecyclerview</a>
|
||||
*/
|
||||
public class SwipeableMessageAdapter
|
||||
extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>
|
||||
implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
|
||||
|
||||
private List<Plaintext> data = Collections.emptyList();
|
||||
private EventListener eventListener;
|
||||
private final View.OnClickListener itemViewOnClickListener;
|
||||
private final View.OnClickListener swipeableViewContainerOnClickListener;
|
||||
|
||||
private Label label;
|
||||
private int selectedPosition;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onItemDeleted(Plaintext item);
|
||||
|
||||
void onItemArchived(Plaintext item);
|
||||
|
||||
void onItemViewClicked(View v);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static class ViewHolder extends AbstractSwipeableItemViewHolder {
|
||||
public final FrameLayout container;
|
||||
public final ImageView avatar;
|
||||
public final ImageView status;
|
||||
public final TextView sender;
|
||||
public final TextView subject;
|
||||
public final TextView extract;
|
||||
|
||||
ViewHolder(View v) {
|
||||
super(v);
|
||||
container = (FrameLayout) v.findViewById(R.id.container);
|
||||
avatar = (ImageView) v.findViewById(R.id.avatar);
|
||||
status = (ImageView) v.findViewById(R.id.status);
|
||||
sender = (TextView) v.findViewById(R.id.sender);
|
||||
subject = (TextView) v.findViewById(R.id.subject);
|
||||
extract = (TextView) v.findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getSwipeableContainerView() {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
public SwipeableMessageAdapter() {
|
||||
itemViewOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onItemViewClick(view);
|
||||
}
|
||||
};
|
||||
swipeableViewContainerOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onSwipeableViewContainerClick(view);
|
||||
}
|
||||
};
|
||||
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setData(Label label, List<Plaintext> data) {
|
||||
this.label = label;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private void onItemViewClick(View v) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onItemViewClicked(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSwipeableViewContainerClick(View v) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onItemViewClicked(
|
||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v));
|
||||
}
|
||||
}
|
||||
|
||||
public Plaintext getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return (long) data.get(position).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View v = inflater.inflate(R.layout.message_row, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
final Plaintext item = data.get(position);
|
||||
|
||||
if (activateOnItemClick) {
|
||||
holder.container.setBackgroundResource(
|
||||
position == selectedPosition
|
||||
? R.drawable.bg_item_selected_state
|
||||
: R.drawable.bg_item_normal_state
|
||||
);
|
||||
}
|
||||
|
||||
// set listeners
|
||||
// (if the item is *pinned*, click event comes to the itemView)
|
||||
holder.itemView.setOnClickListener(itemViewOnClickListener);
|
||||
// (if the item is *not pinned*, click event comes to the container)
|
||||
holder.container.setOnClickListener(swipeableViewContainerOnClickListener);
|
||||
|
||||
// set data
|
||||
holder.avatar.setImageDrawable(new Identicon(item.getFrom()));
|
||||
holder.status.setImageResource(Assets.getStatusDrawable(item.getStatus()));
|
||||
holder.status.setContentDescription(
|
||||
holder.status.getContext().getString(Assets.getStatusString(item.getStatus())));
|
||||
holder.sender.setText(item.getFrom().toString());
|
||||
holder.subject.setText(normalizeWhitespaces(item.getSubject()));
|
||||
holder.extract.setText(normalizeWhitespaces(item.getText()));
|
||||
if (item.isUnread()) {
|
||||
holder.sender.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
holder.subject.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
} else {
|
||||
holder.sender.setTypeface(Typeface.DEFAULT);
|
||||
holder.subject.setTypeface(Typeface.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onGetSwipeReactionType(ViewHolder holder, int position, int x, int y) {
|
||||
if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) {
|
||||
return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT;
|
||||
}
|
||||
return REACTION_CAN_SWIPE_BOTH_H;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SwitchIntDef")
|
||||
public void onSetSwipeBackground(ViewHolder holder, int position, int type) {
|
||||
int bgRes = 0;
|
||||
switch (type) {
|
||||
case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND:
|
||||
bgRes = R.drawable.bg_swipe_item_neutral;
|
||||
break;
|
||||
case DRAWABLE_SWIPE_LEFT_BACKGROUND:
|
||||
bgRes = R.drawable.bg_swipe_item_left;
|
||||
break;
|
||||
case DRAWABLE_SWIPE_RIGHT_BACKGROUND:
|
||||
if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) {
|
||||
bgRes = R.drawable.bg_swipe_item_neutral;
|
||||
} else {
|
||||
bgRes = R.drawable.bg_swipe_item_right;
|
||||
}
|
||||
break;
|
||||
}
|
||||
holder.itemView.setBackgroundResource(bgRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SwitchIntDef")
|
||||
public SwipeResultAction onSwipeItem(ViewHolder holder, final int position, int result) {
|
||||
switch (result) {
|
||||
// swipe right
|
||||
case RESULT_SWIPED_RIGHT:
|
||||
return new SwipeRightResultAction(this, position);
|
||||
case RESULT_SWIPED_LEFT:
|
||||
return new SwipeLeftResultAction(this, position);
|
||||
// other --- do nothing
|
||||
case RESULT_CANCELED:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEventListener(EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setSelectedPosition(int selectedPosition) {
|
||||
int oldPosition = this.selectedPosition;
|
||||
this.selectedPosition = selectedPosition;
|
||||
notifyItemChanged(oldPosition);
|
||||
notifyItemChanged(selectedPosition);
|
||||
}
|
||||
|
||||
private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection {
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private final int position;
|
||||
private final Plaintext item;
|
||||
|
||||
SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) {
|
||||
this.adapter = adapter;
|
||||
this.position = position;
|
||||
this.item = adapter.data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPerformAction() {
|
||||
super.onPerformAction();
|
||||
|
||||
adapter.data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSlideAnimationEnd() {
|
||||
super.onSlideAnimationEnd();
|
||||
|
||||
if (adapter.eventListener != null) {
|
||||
adapter.eventListener.onItemDeleted(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleanUp() {
|
||||
super.onCleanUp();
|
||||
// clear the references
|
||||
adapter = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SwipeRightResultAction extends SwipeResultActionRemoveItem {
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private final int position;
|
||||
private final Plaintext item;
|
||||
|
||||
SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) {
|
||||
this.adapter = adapter;
|
||||
this.position = position;
|
||||
this.item = adapter.data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPerformAction() {
|
||||
super.onPerformAction();
|
||||
|
||||
adapter.data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSlideAnimationEnd() {
|
||||
super.onSlideAnimationEnd();
|
||||
|
||||
if (adapter.eventListener != null) {
|
||||
adapter.eventListener.onItemArchived(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleanUp() {
|
||||
super.onCleanUp();
|
||||
// clear the references
|
||||
adapter = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +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.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
/**
|
||||
* Switches between two {@link ProofOfWorkEngine}s depending on the configuration.
|
||||
*
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SwitchingProofOfWorkEngine implements ProofOfWorkEngine, InternalContext.ContextHolder {
|
||||
private final Context ctx;
|
||||
private final String preference;
|
||||
private final ProofOfWorkEngine option;
|
||||
private final ProofOfWorkEngine fallback;
|
||||
|
||||
public SwitchingProofOfWorkEngine(Context ctx, String preference,
|
||||
ProofOfWorkEngine option, ProofOfWorkEngine fallback) {
|
||||
this.ctx = ctx;
|
||||
this.preference = preference;
|
||||
this.option = option;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
if (preferences.getBoolean(preference, false)) {
|
||||
option.calculateNonce(initialHash, target, callback);
|
||||
} else {
|
||||
fallback.calculateNonce(initialHash, target, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext context) {
|
||||
for (ProofOfWorkEngine e : Arrays.asList(option, fallback)) {
|
||||
if (e instanceof InternalContext.ContextHolder) {
|
||||
((InternalContext.ContextHolder) e).setContext(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,162 +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.dialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import ch.dissem.apps.abit.ImportIdentityActivity;
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class AddIdentityDialogFragment extends AppCompatDialogFragment {
|
||||
private BitmessageContext bmc;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
bmc = Singleton.getBitmessageContext(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
getDialog().setTitle(R.string.add_identity);
|
||||
View view = inflater.inflate(R.layout.dialog_add_identity, container, false);
|
||||
final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup);
|
||||
view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final Context ctx = getActivity().getBaseContext();
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.create_identity:
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<Void, Void, BitmessageAddress>() {
|
||||
@Override
|
||||
protected BitmessageAddress doInBackground(Void... args) {
|
||||
return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(BitmessageAddress chan) {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
mainActivity.addIdentityEntry(chan);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
break;
|
||||
case R.id.import_identity:
|
||||
startActivity(new Intent(ctx, ImportIdentityActivity.class));
|
||||
break;
|
||||
case R.id.add_chan:
|
||||
addChanDialog();
|
||||
break;
|
||||
case R.id.add_deterministic_address:
|
||||
new DeterministicIdentityDialogFragment().show(getFragmentManager(),
|
||||
"dialog");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
private void addChanDialog() {
|
||||
FragmentActivity activity = getActivity();
|
||||
final Context ctx = activity.getBaseContext();
|
||||
@SuppressLint("InflateParams")
|
||||
final View dialogView = activity.getLayoutInflater()
|
||||
.inflate(R.layout.dialog_input_passphrase, null);
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.add_chan)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase);
|
||||
Toast.makeText(ctx, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<String, Void, BitmessageAddress>() {
|
||||
@Override
|
||||
protected BitmessageAddress doInBackground(String... args) {
|
||||
String pass = args[0];
|
||||
BitmessageAddress chan = bmc.createChan(pass);
|
||||
chan.setAlias(pass);
|
||||
bmc.addresses().save(chan);
|
||||
return chan;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(BitmessageAddress chan) {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_chan_created,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
mainActivity.addIdentityEntry(chan);
|
||||
}
|
||||
}
|
||||
}.execute(passphrase.getText().toString());
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTheme() {
|
||||
return R.style.FixedDialog;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import ch.dissem.apps.abit.ImportIdentityActivity
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class AddIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private var parent: ViewGroup? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_identity)
|
||||
parent = container
|
||||
return inflater.inflate(R.layout.dialog_add_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
val ctx = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
|
||||
when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.create_identity -> {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.import_identity -> ctx.startActivity<ImportIdentityActivity>()
|
||||
R.id.add_chan -> addChanDialog()
|
||||
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
private fun addChanDialog() {
|
||||
val activity = activity ?: throw IllegalStateException("No activity available")
|
||||
val ctx = activity.baseContext ?: throw IllegalStateException("No context available")
|
||||
val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent)
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.add_chan)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val passphrase = dialogView.findViewById<TextView>(R.id.passphrase)
|
||||
Toast.makeText(ctx, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val pass = passphrase.text.toString()
|
||||
doAsync {
|
||||
val chan = bmc.createChan(pass)
|
||||
chan.alias = pass
|
||||
bmc.addresses.save(chan)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_chan_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply { addIdentityEntry(chan) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
@ -1,136 +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.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment {
|
||||
private BitmessageContext bmc;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
bmc = Singleton.getBitmessageContext(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
getDialog().setTitle(R.string.add_deterministic_address);
|
||||
View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false);
|
||||
view.findViewById(R.id.ok)
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
final Context context = getActivity().getBaseContext();
|
||||
View dialogView = getView();
|
||||
assert dialogView != null;
|
||||
TextView label = (TextView) dialogView.findViewById(R.id.label);
|
||||
TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase);
|
||||
TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id
|
||||
.number_of_identities);
|
||||
Switch shorter = (Switch) dialogView.findViewById(R.id.shorter);
|
||||
|
||||
Toast.makeText(context, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<Object, Void, List<BitmessageAddress>>() {
|
||||
@Override
|
||||
protected List<BitmessageAddress> doInBackground(Object... args) {
|
||||
String label = (String) args[0];
|
||||
String pass = (String) args[1];
|
||||
int numberOfAddresses = (int) args[2];
|
||||
boolean shorter = (boolean) args[3];
|
||||
List<BitmessageAddress> identities = bmc.createDeterministicAddresses
|
||||
(pass,
|
||||
numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter);
|
||||
int i = 0;
|
||||
for (BitmessageAddress identity : identities) {
|
||||
i++;
|
||||
if (identities.size() == 1) {
|
||||
identity.setAlias(label);
|
||||
} else {
|
||||
identity.setAlias(label + " (" + i + ")");
|
||||
}
|
||||
bmc.addresses().save(identity);
|
||||
}
|
||||
return identities;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<BitmessageAddress> identities) {
|
||||
int messageRes;
|
||||
if (identities.size() == 1) {
|
||||
messageRes = R.string.toast_identity_created;
|
||||
} else {
|
||||
messageRes = R.string.toast_identities_created;
|
||||
}
|
||||
Toast.makeText(context,
|
||||
messageRes,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
for (BitmessageAddress identity : identities) {
|
||||
mainActivity.addIdentityEntry(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.execute(
|
||||
label.getText().toString(),
|
||||
passphrase.getText().toString(),
|
||||
Integer.valueOf(numberOfAddresses.getText().toString()),
|
||||
shorter.isChecked()
|
||||
);
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTheme() {
|
||||
return R.style.FixedDialog;
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class DeterministicIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_deterministic_address)
|
||||
return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(dialogView, savedInstanceState)
|
||||
ok.setOnClickListener {
|
||||
dismiss()
|
||||
val context = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
val passphraseText = passphrase.text.toString()
|
||||
|
||||
Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identities = bmc.createDeterministicAddresses(
|
||||
passphraseText,
|
||||
number_of_identities.text.toString().toInt(),
|
||||
Pubkey.LATEST_VERSION,
|
||||
1L,
|
||||
shorter.isChecked
|
||||
)
|
||||
for ((i, identity) in identities.withIndex()) {
|
||||
if (identities.size == 1) {
|
||||
identity.alias = label.text.toString()
|
||||
} else {
|
||||
identity.alias = "${label.text} (${i + 1})"
|
||||
}
|
||||
bmc.addresses.save(identity)
|
||||
}
|
||||
uiThread {
|
||||
val messageRes = if (identities.size == 1) {
|
||||
R.string.toast_identity_created
|
||||
} else {
|
||||
R.string.toast_identities_created
|
||||
}
|
||||
Toast.makeText(context,
|
||||
messageRes,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
identities.forEach { identity ->
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
@ -1,53 +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.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.BitmessageService;
|
||||
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class FullNodeDialogActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.dialog_full_node);
|
||||
findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startService(new Intent(FullNodeDialogActivity.this, BitmessageService.class));
|
||||
updateNodeSwitch();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import kotlinx.android.synthetic.main.dialog_full_node.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class FullNodeDialogActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.dialog_full_node)
|
||||
ok.setOnClickListener {
|
||||
preferences.wifiOnly = false
|
||||
network.scheduleNodeStart()
|
||||
finish()
|
||||
}
|
||||
dismiss.setOnClickListener {
|
||||
network.scheduleNodeStart()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,98 +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.dialog;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class SelectEncodingDialogFragment extends AppCompatDialogFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment.class);
|
||||
private Plaintext.Encoding encoding;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) {
|
||||
encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING);
|
||||
}
|
||||
if (encoding == null) {
|
||||
encoding = SIMPLE;
|
||||
}
|
||||
getDialog().setTitle(R.string.select_encoding_title);
|
||||
View view = inflater.inflate(R.layout.dialog_select_message_encoding, container, false);
|
||||
final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup);
|
||||
switch (encoding) {
|
||||
case SIMPLE:
|
||||
radioGroup.check(R.id.simple);
|
||||
break;
|
||||
case EXTENDED:
|
||||
radioGroup.check(R.id.extended);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected encoding: " + encoding);
|
||||
break;
|
||||
}
|
||||
view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.extended:
|
||||
encoding = EXTENDED;
|
||||
break;
|
||||
case R.id.simple:
|
||||
encoding = SIMPLE;
|
||||
break;
|
||||
default:
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
Intent result = new Intent();
|
||||
result.putExtra(EXTRA_ENCODING, encoding);
|
||||
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, result);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
|
||||
import kotlinx.android.synthetic.main.dialog_select_message_encoding.*
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class SelectEncodingDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var encoding: Plaintext.Encoding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE
|
||||
dialog.setTitle(R.string.select_encoding_title)
|
||||
return inflater.inflate(R.layout.dialog_select_message_encoding, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
when (encoding) {
|
||||
SIMPLE -> radioGroup.check(R.id.simple)
|
||||
EXTENDED -> radioGroup.check(R.id.extended)
|
||||
else -> LOG.warn("Unexpected encoding: $encoding")
|
||||
}
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
encoding = when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.extended -> EXTENDED
|
||||
R.id.simple -> SIMPLE
|
||||
else -> {
|
||||
dismiss()
|
||||
return@OnClickListener
|
||||
}
|
||||
}
|
||||
val result = Intent()
|
||||
result.putExtra(EXTRA_ENCODING, encoding)
|
||||
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result)
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener {
|
||||
|
||||
override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
if (current) {
|
||||
// Show QR code in modal dialog
|
||||
val dialog = Dialog(ctx)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
val imageView = ImageView(ctx)
|
||||
imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
|
||||
imageView.setOnClickListener { dialog.dismiss() }
|
||||
dialog.addContentView(
|
||||
imageView,
|
||||
RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
val display = window.windowManager.defaultDisplay
|
||||
val size = Point()
|
||||
display.getSize(size)
|
||||
val dim = if (size.x < size.y) size.x else size.y
|
||||
|
||||
val lp = WindowManager.LayoutParams()
|
||||
lp.copyFrom(window.attributes)
|
||||
lp.width = dim
|
||||
lp.height = dim
|
||||
|
||||
window.attributes = lp
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import ch.dissem.apps.abit.*
|
||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
class ProfileSelectionListener(
|
||||
private val ctx: Context,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : AccountHeader.OnAccountHeaderListener {
|
||||
|
||||
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
when (profile.identifier.toInt()) {
|
||||
MainActivity.ADD_IDENTITY -> addIdentityDialog()
|
||||
MainActivity.MANAGE_IDENTITY -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show()
|
||||
} else {
|
||||
val show = Intent(ctx, AddressDetailActivity::class.java)
|
||||
show.putExtra(AddressDetailFragment.ARG_ITEM, identity)
|
||||
ctx.startActivity(show)
|
||||
}
|
||||
}
|
||||
else -> if (profile is ProfileDrawerItem) {
|
||||
val tag = profile.tag
|
||||
if (tag is BitmessageAddress) {
|
||||
Singleton.setIdentity(tag)
|
||||
MainActivity.apply {
|
||||
updateUnread()
|
||||
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
if (itemList is ListHolder<*>) {
|
||||
itemList.reloadList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// false if it should close the drawer
|
||||
return false
|
||||
}
|
||||
|
||||
private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public interface ActionBarListener {
|
||||
void updateTitle(CharSequence title);
|
||||
|
||||
void updateUnread();
|
||||
}
|
@ -14,16 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener;
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
/**
|
||||
* A callback interface that all activities containing this fragment must
|
||||
* implement. This mechanism allows activities to be notified of item
|
||||
* selections.
|
||||
*/
|
||||
public interface ListSelectionListener<T> {
|
||||
interface ListSelectionListener<in T> {
|
||||
/**
|
||||
* Callback for when an item has been selected.
|
||||
*/
|
||||
void onItemSelected(T item);
|
||||
fun onItemSelected(item: T)
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.notification.NewMessageNotification;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
/**
|
||||
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||
* <p>
|
||||
* Should show a notification when the app isn't running, but update the message list when it is.
|
||||
* Also,
|
||||
* notifications should be combined.
|
||||
* </p>
|
||||
*/
|
||||
public class MessageListener implements BitmessageContext.Listener {
|
||||
private final Deque<Plaintext> unacknowledged = new LinkedList<>();
|
||||
private int numberOfUnacknowledgedMessages = 0;
|
||||
private final NewMessageNotification notification;
|
||||
private final ExecutorService pool = Executors.newSingleThreadExecutor();
|
||||
|
||||
public MessageListener(Context ctx) {
|
||||
this.notification = new NewMessageNotification(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final Plaintext plaintext) {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
unacknowledged.addFirst(plaintext);
|
||||
numberOfUnacknowledgedMessages++;
|
||||
if (unacknowledged.size() > 5) {
|
||||
unacknowledged.removeLast();
|
||||
}
|
||||
if (numberOfUnacknowledgedMessages == 1) {
|
||||
notification.singleNotification(plaintext);
|
||||
} else {
|
||||
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages);
|
||||
}
|
||||
notification.show();
|
||||
|
||||
// If MainActivity is shown, update the sidebar badges
|
||||
MainActivity main = MainActivity.getInstance();
|
||||
if (main != null) {
|
||||
main.updateUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void resetNotification() {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notification.hide();
|
||||
unacknowledged.clear();
|
||||
numberOfUnacknowledgedMessages = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
import android.content.Context
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.notification.NewMessageNotification
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||
*
|
||||
*
|
||||
* Should show a notification when the app isn't running, but update the message list when it is.
|
||||
* Also,
|
||||
* notifications should be combined.
|
||||
*
|
||||
*/
|
||||
class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
|
||||
override fun setContext(ctx: BitmessageContext) {
|
||||
messageRepo = ctx.messages
|
||||
conversationService = ConversationService(messageRepo)
|
||||
}
|
||||
|
||||
private val unacknowledged = LinkedList<Plaintext>()
|
||||
private var numberOfUnacknowledgedMessages = 0
|
||||
private val notification = NewMessageNotification(ctx)
|
||||
private val pool = Executors.newSingleThreadExecutor()
|
||||
private lateinit var messageRepo: MessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
|
||||
init {
|
||||
emulateConversations = ctx.preferences.emulateConversations
|
||||
}
|
||||
|
||||
override fun receive(plaintext: Plaintext) {
|
||||
pool.submit {
|
||||
updateConversation(plaintext)
|
||||
unacknowledged.addFirst(plaintext)
|
||||
numberOfUnacknowledgedMessages++
|
||||
if (unacknowledged.size > 5) {
|
||||
unacknowledged.removeLast()
|
||||
}
|
||||
if (numberOfUnacknowledgedMessages == 1) {
|
||||
notification.singleNotification(plaintext)
|
||||
} else {
|
||||
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages)
|
||||
}
|
||||
notification.show()
|
||||
|
||||
// If MainActivity is shown, update the sidebar badges
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
}
|
||||
|
||||
fun resetNotification() {
|
||||
pool.submit {
|
||||
notification.hide()
|
||||
unacknowledged.clear()
|
||||
numberOfUnacknowledgedMessages = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateConversation(plaintext: Plaintext) {
|
||||
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
|
||||
conversationService.getSubject(listOf(plaintext))?.let { subject ->
|
||||
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var emulateConversations = false
|
||||
}
|
||||
}
|
@ -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.listener;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
|
||||
public class WifiReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
if ("android.net.conn.CONNECTIVITY_CHANGE".equals(intent.getAction())) {
|
||||
if (Preferences.isWifiOnly(ctx)) {
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
|
||||
|
||||
if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) {
|
||||
bmc.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isConnectedToMeteredNetwork(Context ctx) {
|
||||
NetworkInfo netInfo = getNetworkInfo(ctx);
|
||||
if (netInfo == null || !netInfo.isConnectedOrConnecting()) {
|
||||
return false;
|
||||
}
|
||||
switch (netInfo.getType()) {
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static NetworkInfo getNetworkInfo(Context ctx) {
|
||||
ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context
|
||||
.CONNECTIVITY_SERVICE);
|
||||
return conMan.getActiveNetworkInfo();
|
||||
}
|
||||
}
|
@ -1,53 +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.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Some base class to create and handle notifications.
|
||||
*/
|
||||
public abstract class AbstractNotification {
|
||||
protected final Context ctx;
|
||||
protected final NotificationManager manager;
|
||||
protected Notification notification;
|
||||
|
||||
|
||||
public AbstractNotification(Context ctx) {
|
||||
this.ctx = ctx.getApplicationContext();
|
||||
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an id unique to this notification class
|
||||
*/
|
||||
protected abstract int getNotificationId();
|
||||
|
||||
public Notification getNotification() {
|
||||
return notification;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
manager.notify(getNotificationId(), notification);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
manager.cancel(getNotificationId());
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import org.jetbrains.anko.notificationManager
|
||||
|
||||
/**
|
||||
* Some base class to create and handle notifications.
|
||||
*/
|
||||
abstract class AbstractNotification(ctx: Context) {
|
||||
protected val ctx = ctx.applicationContext!!
|
||||
private val manager = ctx.notificationManager
|
||||
var notification: Notification? = null
|
||||
protected set
|
||||
protected var showing = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return an id unique to this notification class
|
||||
*/
|
||||
protected abstract val notificationId: Int
|
||||
|
||||
open fun show() {
|
||||
manager.notify(notificationId, notification)
|
||||
showing = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
showing = false
|
||||
manager.cancel(notificationId)
|
||||
}
|
||||
|
||||
protected fun initChannel(channelId: String, @ColorRes color: Int = R.color.colorPrimary) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
ctx.getText(R.string.app_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
lightColor = ContextCompat.getColor(ctx, color)
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
internal const val ONGOING_CHANNEL_ID = "abit.ongoing"
|
||||
internal const val MESSAGE_CHANNEL_ID = "abit.message"
|
||||
internal const val ERROR_CHANNEL_ID = "abit.error"
|
||||
|
||||
init {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Job
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class BatchNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(job: Job): BatchNotification {
|
||||
|
||||
builder.setContentTitle(ctx.getString(job.description))
|
||||
.setSmallIcon(job.icon)
|
||||
.setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0)
|
||||
|
||||
notification = builder.build()
|
||||
show()
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
@ -1,61 +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.notification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
|
||||
/**
|
||||
* Easily create notifications with error messages. Use carefully, users probably won't like them.
|
||||
* (But they are useful during development/testing)
|
||||
*
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class ErrorNotification extends AbstractNotification {
|
||||
public static final int ERROR_NOTIFICATION_ID = 4;
|
||||
|
||||
private final NotificationCompat.Builder builder;
|
||||
|
||||
public ErrorNotification(Context ctx) {
|
||||
super(ctx);
|
||||
builder = new NotificationCompat.Builder(ctx);
|
||||
builder.setContentTitle(ctx.getString(R.string.app_name))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
public ErrorNotification setWarning(@StringRes int resId, Object... args) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
||||
.setContentText(ctx.getString(resId, args));
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ErrorNotification setError(@StringRes int resId, Object... args) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
||||
.setContentText(ctx.getString(resId, args));
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return ERROR_NOTIFICATION_ID;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
|
||||
/**
|
||||
* Easily create notifications with error messages. Use carefully, users probably won't like them.
|
||||
* (But they are useful during development/testing)
|
||||
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ERROR_CHANNEL_ID)
|
||||
.setContentTitle(ctx.getString(R.string.app_name))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
|
||||
init {
|
||||
initChannel(ERROR_CHANNEL_ID, R.color.colorPrimaryDark)
|
||||
}
|
||||
|
||||
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = ERROR_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
const val ERROR_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
@ -1,143 +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.notification;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService;
|
||||
import ch.dissem.apps.abit.service.BitmessageService;
|
||||
import ch.dissem.bitmessage.utils.Property;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* Shows the network status (as long as the client is connected as a full node)
|
||||
*/
|
||||
public class NetworkNotification extends AbstractNotification {
|
||||
public static final int NETWORK_NOTIFICATION_ID = 2;
|
||||
|
||||
private final NotificationCompat.Builder builder;
|
||||
private Timer timer;
|
||||
|
||||
public NetworkNotification(Context ctx) {
|
||||
super(ctx);
|
||||
Intent showAppIntent = new Intent(ctx, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0);
|
||||
builder = new NotificationCompat.Builder(ctx);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node)
|
||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
.setContentIntent(pendingIntent);
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean update() {
|
||||
boolean running = BitmessageService.isRunning();
|
||||
builder.setOngoing(running);
|
||||
Property connections = BitmessageService.getStatus().getProperty("network", "connections");
|
||||
if (!running) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected));
|
||||
updateNodeSwitch();
|
||||
} else if (connections.getProperties().length == 0) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending));
|
||||
} else {
|
||||
StringBuilder info = new StringBuilder();
|
||||
for (Property stream : connections.getProperties()) {
|
||||
int streamNumber = Integer.parseInt(stream.getName().substring("stream ".length()));
|
||||
Integer nodeCount = (Integer) stream.getProperty("nodes").getValue();
|
||||
if (nodeCount == 1) {
|
||||
info.append(ctx.getString(R.string.connection_info_1,
|
||||
streamNumber));
|
||||
} else {
|
||||
info.append(ctx.getString(R.string.connection_info_n,
|
||||
streamNumber, nodeCount));
|
||||
}
|
||||
info.append('\n');
|
||||
}
|
||||
builder.setContentText(info);
|
||||
}
|
||||
builder.mActions.clear();
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
if (running) {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true);
|
||||
builder.addAction(R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT));
|
||||
} else {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true);
|
||||
builder.addAction(R.drawable.ic_notification_node_start,
|
||||
ctx.getString(R.string.full_node_restart),
|
||||
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT));
|
||||
}
|
||||
notification = builder.build();
|
||||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
|
||||
timer = new Timer();
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!update()) {
|
||||
cancel();
|
||||
ctx.stopService(new Intent(ctx, BitmessageService.class));
|
||||
}
|
||||
NetworkNotification.super.show();
|
||||
}
|
||||
}, 10_000, 10_000);
|
||||
}
|
||||
|
||||
public void showShutdown() {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
}
|
||||
update();
|
||||
super.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return NETWORK_NOTIFICATION_ID;
|
||||
}
|
||||
|
||||
public void connecting() {
|
||||
builder.setOngoing(true);
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending));
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true);
|
||||
builder.mActions.clear();
|
||||
builder.addAction(R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT));
|
||||
notification = builder.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.NodeStartupService
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Shows the network status (as long as the client is connected as a full node)
|
||||
*/
|
||||
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
val showAppIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
|
||||
builder
|
||||
.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
|
||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
.setContentIntent(pendingIntent)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun update(): Boolean {
|
||||
val running = NodeStartupService.isRunning
|
||||
builder.setOngoing(running)
|
||||
val connections = NodeStartupService.status.getProperty("network", "connections")
|
||||
if (!running) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node_disconnected)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
|
||||
} else if (connections == null || connections.properties.isEmpty()) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
} else {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node)
|
||||
val info = StringBuilder()
|
||||
for (stream in connections.properties) {
|
||||
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
|
||||
val nodeCount = stream.getProperty("nodes")!!.value as Int?
|
||||
if (nodeCount == 1) {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_1,
|
||||
streamNumber
|
||||
)
|
||||
)
|
||||
} else {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_n,
|
||||
streamNumber, nodeCount
|
||||
)
|
||||
)
|
||||
}
|
||||
info.append('\n')
|
||||
}
|
||||
builder.setContentText(info)
|
||||
}
|
||||
builder.mActions.clear()
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
if (running) {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
} else {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_start,
|
||||
ctx.getString(R.string.full_node_restart),
|
||||
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
}
|
||||
notification = builder.build()
|
||||
return running
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
|
||||
if (!update()) {
|
||||
cancel()
|
||||
}
|
||||
super@NetworkNotification.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun showShutdown() {
|
||||
timer?.cancel()
|
||||
update()
|
||||
super.show()
|
||||
}
|
||||
|
||||
override val notificationId = NETWORK_NOTIFICATION_ID
|
||||
|
||||
fun connecting() {
|
||||
builder.setOngoing(true)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.mActions.clear()
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NETWORK_NOTIFICATION_ID = 2
|
||||
}
|
||||
}
|
@ -1,122 +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.notification;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
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 static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static ch.dissem.apps.abit.MainActivity.EXTRA_REPLY_TO_MESSAGE;
|
||||
import static ch.dissem.apps.abit.MainActivity.EXTRA_SHOW_MESSAGE;
|
||||
import static ch.dissem.apps.abit.service.BitmessageIntentService.EXTRA_DELETE_MESSAGE;
|
||||
import static ch.dissem.apps.abit.util.Drawables.toBitmap;
|
||||
|
||||
public class NewMessageNotification extends AbstractNotification {
|
||||
private static final int NEW_MESSAGE_NOTIFICATION_ID = 1;
|
||||
private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD);
|
||||
|
||||
public NewMessageNotification(Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public NewMessageNotification singleNotification(Plaintext plaintext) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText
|
||||
());
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192))
|
||||
.setContentTitle(plaintext.getFrom().toString())
|
||||
.setContentText(plaintext.getSubject())
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText))
|
||||
.setContentInfo("Info");
|
||||
|
||||
builder.setContentIntent(
|
||||
createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext));
|
||||
builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply),
|
||||
createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext));
|
||||
builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete),
|
||||
createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext));
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
private PendingIntent createActivityIntent(String action, Plaintext message) {
|
||||
Intent intent = new Intent(ctx, MainActivity.class);
|
||||
intent.putExtra(action, message);
|
||||
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent createServiceIntent(Context ctx, String action, Plaintext message) {
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
intent.putExtra(action, message);
|
||||
return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unacknowledged will be accessed from different threads, so make sure wherever it's
|
||||
* accessed it will be in a <code>synchronized(unacknowledged)
|
||||
* {}</code> block
|
||||
*/
|
||||
public NewMessageNotification multiNotification(Collection<Plaintext> unacknowledged, int
|
||||
numberOfUnacknowledgedMessages) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
|
||||
.setContentText(ctx.getString(R.string.app_name));
|
||||
|
||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (unacknowledged) {
|
||||
for (Plaintext msg : unacknowledged) {
|
||||
Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject());
|
||||
sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
inboxStyle.addLine(sb);
|
||||
}
|
||||
}
|
||||
builder.setStyle(inboxStyle);
|
||||
|
||||
Intent intent = new Intent(ctx, MainActivity.class);
|
||||
intent.setAction(MainActivity.ACTION_SHOW_INBOX);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0);
|
||||
builder.setContentIntent(pendingIntent);
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return NEW_MESSAGE_NOTIFICATION_ID;
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.BigTextStyle
|
||||
import androidx.core.app.NotificationCompat.InboxStyle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
|
||||
import ch.dissem.apps.abit.util.toBitmap
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
init {
|
||||
initChannel(MESSAGE_CHANNEL_ID, R.color.colorPrimary)
|
||||
}
|
||||
|
||||
fun singleNotification(plaintext: Plaintext): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text)
|
||||
plaintext.subject?.let { subject ->
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(Identicon(plaintext.from).toBitmap(192))
|
||||
.setContentTitle(plaintext.from.toString())
|
||||
.setContentText(plaintext.subject)
|
||||
.setStyle(BigTextStyle().bigText(bigText))
|
||||
.setContentInfo("Info")
|
||||
|
||||
builder.setContentIntent(
|
||||
createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_reply, ctx.getString(R.string.reply),
|
||||
createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_delete, ctx.getString(R.string.delete),
|
||||
createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)
|
||||
)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun createActivityIntent(action: String, message: Plaintext): PendingIntent {
|
||||
val intent = Intent(ctx, MainActivity::class.java).putExtra(action, message)
|
||||
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
private fun createServiceIntent(
|
||||
ctx: Context,
|
||||
action: String,
|
||||
message: Plaintext
|
||||
): PendingIntent {
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(action, message)
|
||||
return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unacknowledged will be accessed from different threads, so make sure wherever it's
|
||||
* * accessed it will be in a `synchronized(unacknowledged)
|
||||
* * {}` block
|
||||
*/
|
||||
fun multiNotification(
|
||||
unacknowledged: Collection<Plaintext>,
|
||||
numberOfUnacknowledgedMessages: Int
|
||||
): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
|
||||
.setContentText(ctx.getString(R.string.app_name))
|
||||
|
||||
val inboxStyle = InboxStyle()
|
||||
|
||||
synchronized(unacknowledged) {
|
||||
for (msg in unacknowledged) {
|
||||
val sb = SpannableString(msg.from.toString() + " " + msg.subject)
|
||||
sb.setSpan(
|
||||
SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
inboxStyle.addLine(sb)
|
||||
}
|
||||
}
|
||||
builder.setStyle(inboxStyle)
|
||||
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.action = MainActivity.ACTION_SHOW_INBOX
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
|
||||
builder.setContentIntent(pendingIntent)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
private const val NEW_MESSAGE_NOTIFICATION_ID = 1
|
||||
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
}
|
@ -1,63 +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.notification;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.R;
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
public class ProofOfWorkNotification extends AbstractNotification {
|
||||
public static final int ONGOING_NOTIFICATION_ID = 3;
|
||||
|
||||
public ProofOfWorkNotification(Context ctx) {
|
||||
super(ctx);
|
||||
update(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return ONGOING_NOTIFICATION_ID;
|
||||
}
|
||||
|
||||
public ProofOfWorkNotification update(int numberOfItems) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
|
||||
Intent showMessageIntent = new Intent(ctx, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_notification_proof_of_work)
|
||||
.setContentTitle(ctx.getString(R.string.proof_of_work_title))
|
||||
.setContentText(numberOfItems == 0
|
||||
? ctx.getString(R.string.proof_of_work_text_0)
|
||||
: ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
|
||||
.setContentIntent(pendingIntent);
|
||||
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService
|
||||
import ch.dissem.apps.abit.util.PowStats
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_notification_proof_of_work)
|
||||
.setContentTitle(ctx.getString(R.string.proof_of_work_title))
|
||||
private var startTime = 0L
|
||||
private var progress = 0
|
||||
private var progressMax = 0
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
update(0)
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(numberOfItems: Int): ProofOfWorkNotification {
|
||||
|
||||
val showMessageIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentText(if (numberOfItems == 0)
|
||||
ctx.getString(R.string.proof_of_work_text_0)
|
||||
else
|
||||
ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun start(item: ProofOfWorkService.PowItem) {
|
||||
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
|
||||
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
|
||||
startTime = System.currentTimeMillis()
|
||||
progress = 0
|
||||
progressMax = delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 2000, period = 2000){
|
||||
val elapsedTime = System.currentTimeMillis() - startTime
|
||||
progress = elapsedTime.toInt()
|
||||
progressMax = progress + delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished() {
|
||||
timer?.cancel()
|
||||
progress = 0
|
||||
progressMax = 0
|
||||
if (showing) {
|
||||
builder.setProgress(0, 0, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
@ -1,97 +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 android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
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.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE;
|
||||
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class ServerPowEngine implements ProofOfWorkEngine, InternalContext
|
||||
.ContextHolder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ServerPowEngine.class);
|
||||
|
||||
private final Context ctx;
|
||||
private InternalContext context;
|
||||
|
||||
private final ExecutorService pool;
|
||||
|
||||
public ServerPowEngine(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
pool = Executors.newCachedThreadPool(new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(@NonNull Runnable r) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(r);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) {
|
||||
pool.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BitmessageAddress identity = Singleton.getIdentity(ctx);
|
||||
if (identity == null) throw new RuntimeException("No Identity for calculating POW");
|
||||
|
||||
ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash,
|
||||
CALCULATE, target);
|
||||
SyncAdapter.startPowSync(ctx);
|
||||
try {
|
||||
CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>
|
||||
(request);
|
||||
cryptoMsg.signAndEncrypt(
|
||||
identity,
|
||||
cryptography().createPublicKey(identity.getPublicDecryptionKey())
|
||||
);
|
||||
context.getNetworkHandler().send(
|
||||
Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx),
|
||||
cryptoMsg);
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
|
||||
import ch.dissem.bitmessage.factory.Factory;
|
||||
import ch.dissem.bitmessage.ports.AddressRepository;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AddressRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidAddressRepository implements AddressRepository {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "Address";
|
||||
private static final String COLUMN_ADDRESS = "address";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
private static final String COLUMN_ALIAS = "alias";
|
||||
private static final String COLUMN_PUBLIC_KEY = "public_key";
|
||||
private static final String COLUMN_PRIVATE_KEY = "private_key";
|
||||
private static final String COLUMN_SUBSCRIBED = "subscribed";
|
||||
private static final String COLUMN_CHAN = "chan";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
public AndroidAddressRepository(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findContact(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("public_key is null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findIdentity(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("private_key is not null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getIdentities() {
|
||||
return find("private_key IS NOT NULL");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getChans() {
|
||||
return find("chan = '1'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions() {
|
||||
return find("subscribed = '1'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions(long broadcastVersion) {
|
||||
if (broadcastVersion > 4) {
|
||||
return find("subscribed = '1' AND version > 3");
|
||||
} else {
|
||||
return find("subscribed = '1' AND version <= 3");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getContacts() {
|
||||
return find("private_key IS NULL OR chan = '1'");
|
||||
}
|
||||
|
||||
private List<BitmessageAddress> find(String where) {
|
||||
List<BitmessageAddress> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_ADDRESS,
|
||||
COLUMN_ALIAS,
|
||||
COLUMN_PUBLIC_KEY,
|
||||
COLUMN_PRIVATE_KEY,
|
||||
COLUMN_SUBSCRIBED,
|
||||
COLUMN_CHAN
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
BitmessageAddress address;
|
||||
|
||||
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
|
||||
if (privateKeyBytes != null) {
|
||||
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream
|
||||
(privateKeyBytes));
|
||||
address = new BitmessageAddress(privateKey);
|
||||
} else {
|
||||
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
|
||||
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
|
||||
if (publicKeyBytes != null) {
|
||||
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address
|
||||
.getStream(),
|
||||
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length,
|
||||
false);
|
||||
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
|
||||
pubkey = new V4Pubkey((V3Pubkey) pubkey);
|
||||
}
|
||||
address.setPubkey(pubkey);
|
||||
}
|
||||
}
|
||||
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS)));
|
||||
address.setChan(c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1);
|
||||
address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
|
||||
|
||||
result.add(address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(BitmessageAddress address) {
|
||||
if (exists(address)) {
|
||||
update(address);
|
||||
} else {
|
||||
insert(address);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exists(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor cursor = db.rawQuery(
|
||||
"SELECT COUNT(*) FROM Address WHERE address=?",
|
||||
new String[]{address.getAddress()}
|
||||
)) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void update(BitmessageAddress address) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
if (address.getAlias() != null) {
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
}
|
||||
if (address.getPubkey() != null) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
address.getPubkey().writeUnencrypted(out);
|
||||
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
|
||||
}
|
||||
if (address.getPrivateKey() != null) {
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
}
|
||||
if (address.isChan()) {
|
||||
values.put(COLUMN_CHAN, true);
|
||||
}
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
int update = db.update(TABLE_NAME, values, "address=?",
|
||||
new String[]{address.getAddress()});
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(BitmessageAddress address) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ADDRESS, address.getAddress());
|
||||
values.put(COLUMN_VERSION, address.getVersion());
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
if (address.getPubkey() != null) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
address.getPubkey().writeUnencrypted(out);
|
||||
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
|
||||
} else {
|
||||
values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
|
||||
}
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
values.put(COLUMN_CHAN, address.isChan());
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
long insert = db.insert(TABLE_NAME, null, values);
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()});
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress getAddress(String address) {
|
||||
List<BitmessageAddress> result = find("address = '" + address + "'");
|
||||
if (result.size() > 0) return result.get(0);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.AddressRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [AddressRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
|
||||
|
||||
override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag)
|
||||
|
||||
override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag)
|
||||
|
||||
private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? {
|
||||
for (address in find(where)) {
|
||||
if (address.version > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.tag)) return address
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.ripe)) return address
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIdentities() = find("private_key IS NOT NULL")
|
||||
|
||||
override fun getChans() = find("chan = '1'")
|
||||
|
||||
override fun getSubscriptions() = find("subscribed = '1'")
|
||||
|
||||
override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) {
|
||||
find("subscribed = '1' AND version > 3")
|
||||
} else {
|
||||
find("subscribed = '1' AND version <= 3")
|
||||
}
|
||||
|
||||
override fun getContacts() = find("private_key IS NULL OR chan = '1'")
|
||||
|
||||
/**
|
||||
* Returns the contacts in the following order:
|
||||
*
|
||||
* * Subscribed addresses come first
|
||||
* * Addresses with aliases (alphabetically)
|
||||
* * Addresses without aliases are omitted
|
||||
*
|
||||
*
|
||||
* @return the ordered list of ids (address strings)
|
||||
*/
|
||||
fun getContactIds(): List<String> = findIds(
|
||||
"($COLUMN_PRIVATE_KEY IS NULL OR $COLUMN_CHAN = '1') AND $COLUMN_ALIAS IS NOT NULL",
|
||||
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS, $COLUMN_ADDRESS"
|
||||
)
|
||||
|
||||
private fun findIds(where: String, orderBy: String): List<String> {
|
||||
val result = LinkedList<String>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
orderBy
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS)))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun find(where: String): List<BitmessageAddress> {
|
||||
val result = LinkedList<BitmessageAddress>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getAddress(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getAddress(c: Cursor): BitmessageAddress {
|
||||
|
||||
fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let {
|
||||
BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it)))
|
||||
}
|
||||
|
||||
fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address ->
|
||||
c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes ->
|
||||
Factory.readPubkey(
|
||||
version = address.version, stream = address.stream,
|
||||
input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size,
|
||||
encrypted = false
|
||||
).let {
|
||||
address.pubkey = if (address.version == 4L && it is V3Pubkey) {
|
||||
V4Pubkey(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (getIdentity(c) ?: getContact(c)).apply {
|
||||
alias = c.getString(c.getColumnIndex(COLUMN_ALIAS))
|
||||
isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1
|
||||
isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(address: BitmessageAddress) = if (exists(address)) {
|
||||
update(address)
|
||||
} else {
|
||||
insert(address)
|
||||
}
|
||||
|
||||
private fun exists(address: BitmessageAddress): Boolean {
|
||||
sql.readableDatabase.rawQuery(
|
||||
"SELECT COUNT(*) FROM Address WHERE address=?",
|
||||
arrayOf(address.address)
|
||||
).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address)
|
||||
|
||||
val update = sql.writableDatabase.update(TABLE_NAME, values, "address=?", arrayOf(address.address))
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address).apply {
|
||||
put(COLUMN_ADDRESS, address.address)
|
||||
put(COLUMN_VERSION, address.version)
|
||||
put(COLUMN_CHAN, address.isChan)
|
||||
}
|
||||
|
||||
val insert = sql.writableDatabase.insert(TABLE_NAME, null, values)
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentValues(address: BitmessageAddress) = ContentValues().apply {
|
||||
address.alias?.let { put(COLUMN_ALIAS, it) }
|
||||
address.pubkey?.let { pubkey ->
|
||||
val out = ByteArrayOutputStream()
|
||||
pubkey.writer().writeUnencrypted(out)
|
||||
put(COLUMN_PUBLIC_KEY, out.toByteArray())
|
||||
}
|
||||
address.privateKey?.let { put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
|
||||
if (address.isChan) {
|
||||
put(COLUMN_CHAN, true)
|
||||
}
|
||||
put(COLUMN_SUBSCRIBED, address.isSubscribed)
|
||||
}
|
||||
|
||||
override fun remove(address: BitmessageAddress) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
|
||||
}
|
||||
|
||||
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Address"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_ALIAS = "alias"
|
||||
private const val COLUMN_PUBLIC_KEY = "public_key"
|
||||
private const val COLUMN_PRIVATE_KEY = "private_key"
|
||||
private const val COLUMN_SUBSCRIBED = "subscribed"
|
||||
private const val COLUMN_CHAN = "chan"
|
||||
}
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage;
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType;
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
|
||||
import ch.dissem.bitmessage.factory.Factory;
|
||||
import ch.dissem.bitmessage.ports.Inventory;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
|
||||
import static ch.dissem.apps.abit.repository.SqlHelper.join;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.now;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* {@link Inventory} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidInventory implements Inventory {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
|
||||
|
||||
private static final String TABLE_NAME = "Inventory";
|
||||
private static final String COLUMN_HASH = "hash";
|
||||
private static final String COLUMN_STREAM = "stream";
|
||||
private static final String COLUMN_EXPIRES = "expires";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public AndroidInventory(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getInventory(long... streams) {
|
||||
List<InventoryVector> result = new LinkedList<>();
|
||||
long now = now();
|
||||
for (long stream : streams) {
|
||||
for (Map.Entry<InventoryVector, Long> e : getCache(stream).entrySet()) {
|
||||
if (e.getValue() > now) {
|
||||
result.add(e.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<InventoryVector, Long> getCache(long stream) {
|
||||
Map<InventoryVector, Long> result = cache.get(stream);
|
||||
if (result == null) {
|
||||
synchronized (cache) {
|
||||
if (cache.get(stream) == null) {
|
||||
result = new ConcurrentHashMap<>();
|
||||
cache.put(stream, result);
|
||||
|
||||
String[] projection = {
|
||||
COLUMN_HASH, COLUMN_EXPIRES
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = " + stream,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
|
||||
long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES));
|
||||
result.put(new InventoryVector(blob), expires);
|
||||
}
|
||||
}
|
||||
LOG.info("Stream #" + stream + " inventory size: " + result.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) {
|
||||
for (long stream : streams) {
|
||||
offer.removeAll(getCache(stream).keySet());
|
||||
}
|
||||
LOG.info(offer.size() + " objects missing.");
|
||||
return offer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectMessage getObject(InventoryVector vector) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'" + vector + "'",
|
||||
null, null, null, null
|
||||
)) {
|
||||
if (!c.moveToFirst()) {
|
||||
LOG.info("Object requested that we don't have. IV: " + vector);
|
||||
return null;
|
||||
}
|
||||
|
||||
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
StringBuilder where = new StringBuilder("1=1");
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream);
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version);
|
||||
}
|
||||
if (types.length > 0) {
|
||||
where.append(" AND type IN (").append(join(types)).append(")");
|
||||
}
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
List<ObjectMessage> result = new LinkedList<>();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(),
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob),
|
||||
blob.length));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeObject(ObjectMessage object) {
|
||||
InventoryVector iv = object.getInventoryVector();
|
||||
|
||||
if (getCache(object.getStream()).containsKey(iv))
|
||||
return;
|
||||
|
||||
LOG.trace("Storing object " + iv);
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_HASH, object.getInventoryVector().getHash());
|
||||
values.put(COLUMN_STREAM, object.getStream());
|
||||
values.put(COLUMN_EXPIRES, object.getExpiresTime());
|
||||
values.put(COLUMN_DATA, Encode.bytes(object));
|
||||
values.put(COLUMN_TYPE, object.getType());
|
||||
values.put(COLUMN_VERSION, object.getVersion());
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
|
||||
getCache(object.getStream()).put(iv, object.getExpiresTime());
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(ObjectMessage object) {
|
||||
return getCache(object.getStream()).keySet().contains(object.getInventoryVector());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
long fiveMinutesAgo = now() - 5 * MINUTE;
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "expires < ?", new String[]{valueOf(fiveMinutesAgo)});
|
||||
|
||||
for (Map<InventoryVector, Long> c : cache.values()) {
|
||||
Iterator<Map.Entry<InventoryVector, Long>> iterator = c.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getValue() < fiveMinutesAgo) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.Inventory
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* [Inventory] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidInventory(private val sql: SqlHelper) : Inventory {
|
||||
|
||||
private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>()
|
||||
|
||||
override fun getInventory(vararg streams: Long): List<InventoryVector> {
|
||||
val result = LinkedList<InventoryVector>()
|
||||
val now = now
|
||||
for (stream in streams) {
|
||||
for ((key, value) in getCache(stream)) {
|
||||
if (value > now) {
|
||||
result.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
|
||||
cache[stream] = result
|
||||
|
||||
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = $stream", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH))
|
||||
val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES))
|
||||
InventoryVector.fromHash(blob)?.let { result.put(it, expires) }
|
||||
}
|
||||
}
|
||||
LOG.info("Stream #$stream inventory size: ${result.size}")
|
||||
return result
|
||||
}
|
||||
return cache[stream] ?: synchronized(cache) {
|
||||
return@synchronized cache[stream] ?: addToCache(stream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys }
|
||||
|
||||
override fun getObject(vector: InventoryVector): ObjectMessage? {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'$vector'", null, null, null, null
|
||||
).use { c ->
|
||||
if (!c.moveToFirst()) {
|
||||
LOG.info("Object requested that we don't have. IV: {}", vector)
|
||||
return null
|
||||
}
|
||||
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
val where = StringBuilder("1=1")
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream)
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version)
|
||||
}
|
||||
if (types.isNotEmpty()) {
|
||||
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
|
||||
}
|
||||
|
||||
val result = LinkedList<ObjectMessage>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(), null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun storeObject(objectMessage: ObjectMessage) {
|
||||
val iv = objectMessage.inventoryVector
|
||||
|
||||
if (getCache(objectMessage.stream).containsKey(iv))
|
||||
return
|
||||
|
||||
LOG.trace("Storing object {}", iv)
|
||||
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_HASH, objectMessage.inventoryVector.hash)
|
||||
put(COLUMN_STREAM, objectMessage.stream)
|
||||
put(COLUMN_EXPIRES, objectMessage.expiresTime)
|
||||
put(COLUMN_DATA, Encode.bytes(objectMessage))
|
||||
put(COLUMN_TYPE, objectMessage.type)
|
||||
put(COLUMN_VERSION, objectMessage.version)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
|
||||
getCache(objectMessage.stream)[iv] = objectMessage.expiresTime
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector)
|
||||
|
||||
override fun cleanup() {
|
||||
val fiveMinutesAgo = now - 5 * MINUTE
|
||||
sql.writableDatabase.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
|
||||
|
||||
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Inventory"
|
||||
private const val COLUMN_HASH = "hash"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_EXPIRES = "expires"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractLabelRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) :
|
||||
AbstractLabelRepository() {
|
||||
|
||||
override fun find(where: String): List<Label> {
|
||||
val result = LinkedList<Label>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
COLUMN_ORDER
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getLabel(c, context))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun save(label: Label) {
|
||||
val db = sql.writableDatabase
|
||||
if (label.id != null) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
|
||||
} else {
|
||||
db.transaction {
|
||||
val exists = DatabaseUtils.queryNumEntries(
|
||||
db,
|
||||
TABLE_NAME,
|
||||
"label=?",
|
||||
arrayOf(label.toString())
|
||||
) > 0
|
||||
|
||||
if (exists) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString()))
|
||||
} else {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.insertOrThrow(TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun findLabels(msgId: Any) =
|
||||
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
|
||||
companion object {
|
||||
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
|
||||
|
||||
private const val TABLE_NAME = "Label"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_LABEL = "label"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_COLOR = "color"
|
||||
private const val COLUMN_ORDER = "ord"
|
||||
|
||||
internal fun getLabel(c: Cursor, context: Context): Label {
|
||||
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
|
||||
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
|
||||
val text: String? = type?.getText(null, context)
|
||||
val label = Label(
|
||||
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(COLUMN_COLOR))
|
||||
)
|
||||
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
@ -1,386 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
import ch.dissem.apps.abit.util.UuidUtils;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import ch.dissem.bitmessage.ports.AbstractMessageRepository;
|
||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
|
||||
import static ch.dissem.apps.abit.util.UuidUtils.asUuid;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* {@link MessageRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidMessageRepository extends AbstractMessageRepository {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
|
||||
|
||||
public static final Label LABEL_ARCHIVE = new Label("archive", null, 0);
|
||||
|
||||
private static final String TABLE_NAME = "Message";
|
||||
private static final String COLUMN_ID = "id";
|
||||
private static final String COLUMN_IV = "iv";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_SENDER = "sender";
|
||||
private static final String COLUMN_RECIPIENT = "recipient";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_ACK_DATA = "ack_data";
|
||||
private static final String COLUMN_SENT = "sent";
|
||||
private static final String COLUMN_RECEIVED = "received";
|
||||
private static final String COLUMN_STATUS = "status";
|
||||
private static final String COLUMN_TTL = "ttl";
|
||||
private static final String COLUMN_RETRIES = "retries";
|
||||
private static final String COLUMN_NEXT_TRY = "next_try";
|
||||
private static final String COLUMN_INITIAL_HASH = "initial_hash";
|
||||
private static final String COLUMN_CONVERSATION = "conversation";
|
||||
|
||||
private static final String PARENTS_TABLE_NAME = "Message_Parent";
|
||||
|
||||
private static final String JOIN_TABLE_NAME = "Message_Label";
|
||||
private static final String JT_COLUMN_MESSAGE = "message_id";
|
||||
private static final String JT_COLUMN_LABEL = "label_id";
|
||||
|
||||
private static final String LBL_TABLE_NAME = "Label";
|
||||
private static final String LBL_COLUMN_ID = "id";
|
||||
private static final String LBL_COLUMN_LABEL = "label";
|
||||
private static final String LBL_COLUMN_TYPE = "type";
|
||||
private static final String LBL_COLUMN_COLOR = "color";
|
||||
private static final String LBL_COLUMN_ORDER = "ord";
|
||||
private final SqlHelper sql;
|
||||
private final Context context;
|
||||
|
||||
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
|
||||
this.sql = sql;
|
||||
this.context = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plaintext> findMessages(Label label) {
|
||||
if (label == LABEL_ARCHIVE) {
|
||||
return super.findMessages((Label) null);
|
||||
} else {
|
||||
return super.findMessages(label);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Label> findLabels(String where) {
|
||||
List<Label> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
LBL_COLUMN_ID,
|
||||
LBL_COLUMN_LABEL,
|
||||
LBL_COLUMN_TYPE,
|
||||
LBL_COLUMN_COLOR
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
LBL_TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null,
|
||||
LBL_COLUMN_ORDER
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
result.add(getLabel(c));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Label getLabel(Cursor c) {
|
||||
String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE));
|
||||
Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName);
|
||||
String text = Labels.getText(type, null, context);
|
||||
if (text == null) {
|
||||
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
|
||||
}
|
||||
Label label = new Label(
|
||||
text,
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)));
|
||||
label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID)));
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUnread(Label label) {
|
||||
String[] args;
|
||||
String where;
|
||||
if (label == null) {
|
||||
return 0;
|
||||
}
|
||||
if (label == LABEL_ARCHIVE) {
|
||||
where = "";
|
||||
args = new String[]{
|
||||
Label.Type.UNREAD.name()
|
||||
};
|
||||
} else {
|
||||
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND ";
|
||||
args = new String[]{
|
||||
label.getId().toString(),
|
||||
Label.Type.UNREAD.name()
|
||||
};
|
||||
}
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME,
|
||||
where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" +
|
||||
"SELECT id FROM Label WHERE type=?))",
|
||||
args
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UUID> findConversations(Label label) {
|
||||
String[] projection = {
|
||||
COLUMN_CONVERSATION,
|
||||
};
|
||||
|
||||
String where;
|
||||
if (label == null) {
|
||||
where = "id NOT IN (SELECT message_id FROM Message_Label)";
|
||||
} else {
|
||||
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")";
|
||||
}
|
||||
List<UUID> result = new LinkedList<>();
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION));
|
||||
result.add(asUuid(uuidBytes));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private void updateParents(SQLiteDatabase db, Plaintext message) {
|
||||
if (message.getInventoryVector() == null || message.getParents().isEmpty()) {
|
||||
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
|
||||
return;
|
||||
}
|
||||
byte[] childIV = message.getInventoryVector().getHash();
|
||||
db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()});
|
||||
|
||||
// save new parents
|
||||
int order = 0;
|
||||
for (InventoryVector parentIV : message.getParents()) {
|
||||
Plaintext parent = getMessage(parentIV);
|
||||
mergeConversations(db, parent.getConversationId(), message.getConversationId());
|
||||
order++;
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("parent", parentIV.getHash());
|
||||
values.put("child", childIV);
|
||||
values.put("pos", order);
|
||||
values.put("conversation", UuidUtils.asBytes(message.getConversationId()));
|
||||
db.insertOrThrow(PARENTS_TABLE_NAME, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every occurrence of the source conversation ID with the target ID
|
||||
*
|
||||
* @param db is used to keep everything within one transaction
|
||||
* @param source ID of the conversation to be merged
|
||||
* @param target ID of the merge target
|
||||
*/
|
||||
private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("conversation", UuidUtils.asBytes(target));
|
||||
String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()};
|
||||
db.update(TABLE_NAME, values, "conversation=?", whereArgs);
|
||||
db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs);
|
||||
}
|
||||
|
||||
protected List<Plaintext> find(String where) {
|
||||
List<Plaintext> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
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
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null,
|
||||
COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC"
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV));
|
||||
byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex
|
||||
(COLUMN_TYPE)));
|
||||
Plaintext.Builder builder = Plaintext.readWithoutSignature(type,
|
||||
new ByteArrayInputStream(data));
|
||||
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
|
||||
builder.id(id);
|
||||
builder.IV(new InventoryVector(iv));
|
||||
builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex
|
||||
(COLUMN_SENDER))));
|
||||
builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex
|
||||
(COLUMN_RECIPIENT))));
|
||||
builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA)));
|
||||
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)));
|
||||
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)));
|
||||
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex
|
||||
(COLUMN_STATUS))));
|
||||
builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL)));
|
||||
builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES)));
|
||||
int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY);
|
||||
if (!c.isNull(nextTryColumn)) {
|
||||
builder.nextTry(c.getLong(nextTryColumn));
|
||||
}
|
||||
builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))));
|
||||
builder.labels(findLabels(id));
|
||||
result.add(builder.build());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Collection<Label> findLabels(long id) {
|
||||
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Plaintext message) {
|
||||
saveContactIfNecessary(message.getFrom());
|
||||
saveContactIfNecessary(message.getTo());
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
// save message
|
||||
if (message.getId() == null) {
|
||||
insert(db, message);
|
||||
} else {
|
||||
update(db, message);
|
||||
}
|
||||
|
||||
updateParents(db, message);
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())});
|
||||
|
||||
// save labels
|
||||
ContentValues values = new ContentValues();
|
||||
for (Label label : message.getLabels()) {
|
||||
values.put(JT_COLUMN_LABEL, (Long) label.getId());
|
||||
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(SQLiteDatabase db, Plaintext message) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message
|
||||
.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_ACK_DATA, message.getAckData());
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
values.put(COLUMN_INITIAL_HASH, message.getInitialHash());
|
||||
values.put(COLUMN_TTL, message.getTTL());
|
||||
values.put(COLUMN_RETRIES, message.getRetries());
|
||||
values.put(COLUMN_NEXT_TRY, message.getNextTry());
|
||||
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
|
||||
long id = db.insertOrThrow(TABLE_NAME, null, values);
|
||||
message.setId(id);
|
||||
}
|
||||
|
||||
private void update(SQLiteDatabase db, Plaintext message) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message
|
||||
.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_ACK_DATA, message.getAckData());
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
values.put(COLUMN_INITIAL_HASH, message.getInitialHash());
|
||||
values.put(COLUMN_TTL, message.getTTL());
|
||||
values.put(COLUMN_RETRIES, message.getRetries());
|
||||
values.put(COLUMN_NEXT_TRY, message.getNextTry());
|
||||
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
|
||||
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Plaintext message) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "id = " + message.getId(), null);
|
||||
}
|
||||
}
|
@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import ch.dissem.apps.abit.util.UuidUtils.asUuid
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractMessageRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
|
||||
|
||||
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
|
||||
if (label === LABEL_ARCHIVE || label === null) {
|
||||
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
|
||||
} else {
|
||||
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
|
||||
}
|
||||
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) =
|
||||
if (label === LABEL_ARCHIVE) {
|
||||
super.findMessages(null as Label?, offset, limit)
|
||||
} else {
|
||||
super.findMessages(label, offset, limit)
|
||||
}
|
||||
|
||||
fun count() = DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
null,
|
||||
null
|
||||
).toInt()
|
||||
|
||||
private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
|
||||
if (separateIdentities) {
|
||||
val identity = prefs.currentIdentity
|
||||
return if (prefs.separateIdentities && identity != null) {
|
||||
"AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
|
||||
} else {
|
||||
"" to emptyArray()
|
||||
}
|
||||
} else {
|
||||
return "" to emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun countUnread(label: Label?) = countUnread(label, false)
|
||||
|
||||
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
|
||||
when {
|
||||
label === LABEL_ARCHIVE -> 0
|
||||
label == null -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
|
||||
selectIdentityQuery,
|
||||
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
|
||||
).toInt()
|
||||
else -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
|
||||
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
|
||||
selectIdentityQuery,
|
||||
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
|
||||
|
||||
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
|
||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
||||
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
|
||||
|
||||
val where = when {
|
||||
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
|
||||
label == null -> if (selectIdentityQuery.isNotBlank()) {
|
||||
"type = 'BROADCAST' OR recipient=? OR sender=?"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
|
||||
}
|
||||
val result = LinkedList<UUID>()
|
||||
sql.readableDatabase.query(
|
||||
true,
|
||||
TABLE_NAME,
|
||||
projection,
|
||||
where,
|
||||
selectIdentityArgs, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
||||
result.add(asUuid(uuidBytes))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
|
||||
val inventoryVector = message.inventoryVector
|
||||
if (inventoryVector == null || message.parents.isEmpty()) {
|
||||
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
|
||||
return
|
||||
}
|
||||
val childIV = inventoryVector.hash
|
||||
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
|
||||
|
||||
// save new parents
|
||||
var order = 0
|
||||
val values = ContentValues()
|
||||
for (parentIV in message.parents) {
|
||||
getMessage(parentIV)?.let { parent ->
|
||||
mergeConversations(db, parent.conversationId, message.conversationId)
|
||||
order++
|
||||
values.put("parent", parentIV.hash)
|
||||
values.put("child", childIV)
|
||||
values.put("pos", order)
|
||||
values.put("conversation", UuidUtils.asBytes(message.conversationId))
|
||||
db.insertOrThrow(PARENTS_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every occurrence of the source conversation ID with the target ID
|
||||
|
||||
* @param db is used to keep everything within one transaction
|
||||
* *
|
||||
* @param source ID of the conversation to be merged
|
||||
* *
|
||||
* @param target ID of the merge target
|
||||
*/
|
||||
private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
|
||||
val values = ContentValues()
|
||||
values.put("conversation", UuidUtils.asBytes(target))
|
||||
val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
|
||||
db.update(TABLE_NAME, values, where, null)
|
||||
db.update(PARENTS_TABLE_NAME, values, where, null)
|
||||
}
|
||||
|
||||
override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
|
||||
|
||||
private fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
|
||||
val result = LinkedList<Plaintext>()
|
||||
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$where $selectIdentityQuery", selectIdentityArgs, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature(
|
||||
Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))),
|
||||
ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA)))
|
||||
).build {
|
||||
id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV)))
|
||||
c.getString(c.getColumnIndex(COLUMN_SENDER))?.let {
|
||||
from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let {
|
||||
to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))
|
||||
sent = c.getLong(c.getColumnIndex(COLUMN_SENT))
|
||||
received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED))
|
||||
status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))
|
||||
ttl = c.getLong(c.getColumnIndex(COLUMN_TTL))
|
||||
retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES))
|
||||
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
|
||||
if (!c.isNull(nextTryColumn)) {
|
||||
nextTry = c.getLong(nextTryColumn)
|
||||
}
|
||||
conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))
|
||||
labels = findLabels(id!!)
|
||||
}
|
||||
|
||||
private fun findLabels(msgId: Any) =
|
||||
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||
|
||||
override fun save(message: Plaintext) {
|
||||
saveContactIfNecessary(message.from)
|
||||
saveContactIfNecessary(message.to)
|
||||
val db = sql.writableDatabase
|
||||
db.transaction {
|
||||
// save message
|
||||
if (message.id == null) {
|
||||
insert(db, message)
|
||||
} else {
|
||||
update(db, message)
|
||||
}
|
||||
|
||||
updateParents(db, message)
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
|
||||
|
||||
// save labels
|
||||
val values = ContentValues()
|
||||
for (label in message.labels) {
|
||||
values.put(JT_COLUMN_LABEL, label.id as Long?)
|
||||
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValues(message: Plaintext) = ContentValues(14).apply {
|
||||
put(COLUMN_IV, message.inventoryVector?.hash)
|
||||
put(COLUMN_TYPE, message.type.name)
|
||||
put(COLUMN_SENDER, message.from.address)
|
||||
put(COLUMN_RECIPIENT, message.to?.address)
|
||||
put(COLUMN_DATA, Encode.bytes(message))
|
||||
put(COLUMN_ACK_DATA, message.ackData)
|
||||
put(COLUMN_SENT, message.sent)
|
||||
put(COLUMN_RECEIVED, message.received)
|
||||
put(COLUMN_STATUS, message.status.name)
|
||||
put(COLUMN_INITIAL_HASH, message.initialHash)
|
||||
put(COLUMN_TTL, message.ttl)
|
||||
put(COLUMN_RETRIES, message.retries)
|
||||
put(COLUMN_NEXT_TRY, message.nextTry)
|
||||
put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
|
||||
}
|
||||
|
||||
private fun insert(db: SQLiteDatabase, message: Plaintext) {
|
||||
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
|
||||
message.id = id
|
||||
}
|
||||
|
||||
private fun update(db: SQLiteDatabase, message: Plaintext) {
|
||||
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
override fun remove(message: Plaintext) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
|
||||
val result = mutableListOf<Plaintext>()
|
||||
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
|
||||
"$COLUMN_ID ASC",
|
||||
"$limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "Message"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_IV = "iv"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_SENDER = "sender"
|
||||
private const val COLUMN_RECIPIENT = "recipient"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_ACK_DATA = "ack_data"
|
||||
private const val COLUMN_SENT = "sent"
|
||||
private const val COLUMN_RECEIVED = "received"
|
||||
private const val COLUMN_STATUS = "status"
|
||||
private const val COLUMN_TTL = "ttl"
|
||||
private const val COLUMN_RETRIES = "retries"
|
||||
private const val COLUMN_NEXT_TRY = "next_try"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_CONVERSATION = "conversation"
|
||||
|
||||
private const val PARENTS_TABLE_NAME = "Message_Parent"
|
||||
|
||||
private const val JOIN_TABLE_NAME = "Message_Label"
|
||||
private const val JT_COLUMN_MESSAGE = "message_id"
|
||||
private const val JT_COLUMN_LABEL = "label_id"
|
||||
}
|
||||
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
|
||||
import ch.dissem.bitmessage.exception.ApplicationException;
|
||||
import ch.dissem.bitmessage.ports.NodeRegistry;
|
||||
import ch.dissem.bitmessage.utils.Collections;
|
||||
import ch.dissem.bitmessage.utils.SqlStrings;
|
||||
|
||||
import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.now;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AndroidNodeRegistry implements NodeRegistry {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
|
||||
|
||||
private static final String TABLE_NAME = "Node";
|
||||
private static final String COLUMN_STREAM = "stream";
|
||||
private static final String COLUMN_ADDRESS = "address";
|
||||
private static final String COLUMN_PORT = "port";
|
||||
private static final String COLUMN_SERVICES = "services";
|
||||
private static final String COLUMN_TIME = "time";
|
||||
|
||||
private final ThreadLocal<SQLiteStatement> loadExistingStatement = new ThreadLocal<>();
|
||||
|
||||
private final SqlHelper sql;
|
||||
private Map<Long, Set<NetworkAddress>> stableNodes;
|
||||
|
||||
public AndroidNodeRegistry(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
cleanUp();
|
||||
}
|
||||
|
||||
private void cleanUp() {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
private Long loadExistingTime(NetworkAddress node) {
|
||||
SQLiteStatement statement = loadExistingStatement.get();
|
||||
if (statement == null) {
|
||||
statement = sql.getWritableDatabase().compileStatement(
|
||||
"SELECT " + COLUMN_TIME +
|
||||
" FROM " + TABLE_NAME +
|
||||
" WHERE stream=? AND address=? AND port=?"
|
||||
);
|
||||
loadExistingStatement.set(statement);
|
||||
}
|
||||
statement.bindLong(1, node.getStream());
|
||||
statement.bindBlob(2, node.getIPv6());
|
||||
statement.bindLong(3, node.getPort());
|
||||
try {
|
||||
return statement.simpleQueryForLong();
|
||||
} catch (SQLiteDoneException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NetworkAddress> getKnownAddresses(int limit, long... streams) {
|
||||
String[] projection = {
|
||||
COLUMN_STREAM,
|
||||
COLUMN_ADDRESS,
|
||||
COLUMN_PORT,
|
||||
COLUMN_SERVICES,
|
||||
COLUMN_TIME
|
||||
};
|
||||
|
||||
List<NetworkAddress> result = new LinkedList<>();
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream IN (?)",
|
||||
new String[]{SqlStrings.join(streams).toString()},
|
||||
null, null,
|
||||
"time DESC",
|
||||
valueOf(limit)
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
result.add(
|
||||
new NetworkAddress.Builder()
|
||||
.stream(c.getLong(c.getColumnIndex(COLUMN_STREAM)))
|
||||
.ipv6(c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)))
|
||||
.port(c.getInt(c.getColumnIndex(COLUMN_PORT)))
|
||||
.services(c.getLong(c.getColumnIndex(COLUMN_SERVICES)))
|
||||
.time(c.getLong(c.getColumnIndex(COLUMN_TIME)))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
throw new ApplicationException(e);
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
synchronized (this) {
|
||||
if (stableNodes == null) {
|
||||
stableNodes = loadStableNodes();
|
||||
}
|
||||
}
|
||||
for (long stream : streams) {
|
||||
Set<NetworkAddress> nodes = stableNodes.get(stream);
|
||||
if (nodes != null && !nodes.isEmpty()) {
|
||||
result.add(Collections.selectRandom(nodes));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offerAddresses(List<NetworkAddress> nodes) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
cleanUp();
|
||||
for (NetworkAddress node : nodes) {
|
||||
if (node.getTime() < now(+5 * MINUTE) && node.getTime() > now(-28 * DAY)) {
|
||||
synchronized (this) {
|
||||
Long existing = loadExistingTime(node);
|
||||
if (existing == null) {
|
||||
insert(node);
|
||||
} else if (node.getTime() > existing) {
|
||||
update(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(NetworkAddress node) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_STREAM, node.getStream());
|
||||
values.put(COLUMN_ADDRESS, node.getIPv6());
|
||||
values.put(COLUMN_PORT, node.getPort());
|
||||
values.put(COLUMN_SERVICES, node.getServices());
|
||||
values.put(COLUMN_TIME, node.getTime());
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void update(NetworkAddress node) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_SERVICES, node.getServices());
|
||||
values.put(COLUMN_TIME, node.getTime());
|
||||
|
||||
db.update(TABLE_NAME, values,
|
||||
"stream=" + node.getStream() + " AND address=X'" + hex(node.getIPv6()) + "' AND " +
|
||||
"port=" + node.getPort(),
|
||||
null);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.database.sqlite.SQLiteDoneException
|
||||
import android.database.sqlite.SQLiteStatement
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.ports.NodeRegistry
|
||||
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
|
||||
import ch.dissem.bitmessage.utils.Collections
|
||||
import ch.dissem.bitmessage.utils.SqlStrings
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import ch.dissem.bitmessage.utils.UnixTime.DAY
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import ch.dissem.bitmessage.utils.max
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import kotlin.concurrent.getOrSet
|
||||
|
||||
const val MAX_ENTRY_AGE = 7 * DAY
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
|
||||
|
||||
private val loadExistingStatement = ThreadLocal<SQLiteStatement>()
|
||||
private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap()
|
||||
get() {
|
||||
if (field.isEmpty())
|
||||
field = loadStableNodes()
|
||||
return field
|
||||
}
|
||||
|
||||
init {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
private fun cleanUp() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString()))
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
private fun loadExistingTime(node: NetworkAddress): Long? {
|
||||
val statement: SQLiteStatement = loadExistingStatement.getOrSet {
|
||||
sql.writableDatabase.compileStatement(
|
||||
"SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?"
|
||||
)
|
||||
}
|
||||
statement.bindLong(1, node.stream)
|
||||
statement.bindBlob(2, node.IPv6)
|
||||
statement.bindLong(3, node.port.toLong())
|
||||
return try {
|
||||
statement.simpleQueryForLong()
|
||||
} catch (e: SQLiteDoneException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> {
|
||||
val result = LinkedList<NetworkAddress>()
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME),
|
||||
"stream IN (?)",
|
||||
arrayOf(SqlStrings.join(*streams)), null, null,
|
||||
"time DESC",
|
||||
limit.toString()
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(NetworkAddress(
|
||||
time = c.getLong(c.getColumnIndex(COLUMN_TIME)),
|
||||
stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)),
|
||||
services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)),
|
||||
IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)),
|
||||
port = c.getInt(c.getColumnIndex(COLUMN_PORT))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
streams
|
||||
.asSequence()
|
||||
.mapNotNull { stableNodes[it] }
|
||||
.filterNot { it.isEmpty() }
|
||||
.mapTo(result) { Collections.selectRandom(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun offerAddresses(nodes: List<NetworkAddress>) {
|
||||
sql.writableDatabase.transaction {
|
||||
cleanUp()
|
||||
nodes
|
||||
.filter {
|
||||
// Don't accept nodes from the future, it might be a trap
|
||||
it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE
|
||||
}
|
||||
.forEach { node ->
|
||||
synchronized(this) {
|
||||
val existing = loadExistingTime(node)
|
||||
if (existing == null) {
|
||||
insert(node)
|
||||
} else if (node.time > existing) {
|
||||
update(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(node: NetworkAddress) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_STREAM, node.stream)
|
||||
put(COLUMN_ADDRESS, node.IPv6)
|
||||
put(COLUMN_PORT, node.port)
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME,
|
||||
if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(node: NetworkAddress) {
|
||||
try {
|
||||
val time = if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME, max(node.time, time))
|
||||
}
|
||||
|
||||
sql.writableDatabase.update(
|
||||
TABLE_NAME,
|
||||
values,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(node: NetworkAddress) {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"time<${UnixTime.now - 8 * DAY}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Node"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_PORT = "port"
|
||||
private const val COLUMN_SERVICES = "services"
|
||||
private const val COLUMN_TIME = "time"
|
||||
}
|
||||
}
|
@ -1,172 +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.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext;
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage;
|
||||
import ch.dissem.bitmessage.factory.Factory;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
|
||||
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext
|
||||
.ContextHolder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "POW";
|
||||
private static final String COLUMN_INITIAL_HASH = "initial_hash";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte";
|
||||
private static final String COLUMN_EXTRA_BYTES = "extra_bytes";
|
||||
private static final String COLUMN_EXPIRATION_TIME = "expiration_time";
|
||||
private static final String COLUMN_MESSAGE_ID = "message_id";
|
||||
|
||||
private final SqlHelper sql;
|
||||
private InternalContext bmc;
|
||||
|
||||
public AndroidProofOfWorkRepository(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext internalContext) {
|
||||
this.bmc = internalContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item getItem(byte[] initialHash) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_DATA,
|
||||
COLUMN_VERSION,
|
||||
COLUMN_NONCE_TRIALS_PER_BYTE,
|
||||
COLUMN_EXTRA_BYTES,
|
||||
COLUMN_EXPIRATION_TIME,
|
||||
COLUMN_MESSAGE_ID
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'" + hex(initialHash) + "'",
|
||||
null, null, null, null
|
||||
)) {
|
||||
if (c.moveToFirst()) {
|
||||
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
|
||||
return new Item(
|
||||
Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob
|
||||
.length),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
|
||||
);
|
||||
} else {
|
||||
return new Item(
|
||||
Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob
|
||||
.length),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
|
||||
bmc.getMessageRepository().getMessage(
|
||||
c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Object requested that we don't have. Initial hash: " +
|
||||
hex(initialHash));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> getItems() {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_INITIAL_HASH
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
List<byte[]> result = new LinkedList<>();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
null, null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH));
|
||||
result.add(initialHash);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(Item item) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.object));
|
||||
values.put(COLUMN_DATA, Encode.bytes(item.object));
|
||||
values.put(COLUMN_VERSION, item.object.getVersion());
|
||||
values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte);
|
||||
values.put(COLUMN_EXTRA_BYTES, item.extraBytes);
|
||||
if (item.message != null) {
|
||||
values.put(COLUMN_EXPIRATION_TIME, item.expirationTime);
|
||||
values.put(COLUMN_MESSAGE_ID, (Long) item.message.getId());
|
||||
}
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
|
||||
putObject(new Item(object, nonceTrialsPerByte, extraBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObject(byte[] initialHash) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(
|
||||
TABLE_NAME,
|
||||
"initial_hash=X'" + hex(initialHash) + "'",
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.LinkedList
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder {
|
||||
private lateinit var bmc: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
this.bmc = context
|
||||
}
|
||||
|
||||
override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'${hex(initialHash)}'",
|
||||
null, null, null, null
|
||||
).use { c ->
|
||||
if (c.moveToFirst()) {
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
|
||||
)
|
||||
} else {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
|
||||
bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}")
|
||||
}
|
||||
|
||||
override fun getItems(): List<ByteArray> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_INITIAL_HASH)
|
||||
|
||||
val result = LinkedList<ByteArray>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection, null, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH))
|
||||
result.add(initialHash)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun putObject(item: ProofOfWorkRepository.Item) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
|
||||
put(COLUMN_DATA, Encode.bytes(item.objectMessage))
|
||||
put(COLUMN_VERSION, item.objectMessage.version)
|
||||
put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
|
||||
put(COLUMN_EXTRA_BYTES, item.extraBytes)
|
||||
item.message?.let { message ->
|
||||
put(COLUMN_EXPIRATION_TIME, item.expirationTime)
|
||||
put(COLUMN_MESSAGE_ID, message.id as Long?)
|
||||
}
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) =
|
||||
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
|
||||
|
||||
override fun removeObject(initialHash: ByteArray) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "POW"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
|
||||
private const val COLUMN_EXTRA_BYTES = "extra_bytes"
|
||||
private const val COLUMN_EXPIRATION_TIME = "expiration_time"
|
||||
private const val COLUMN_MESSAGE_ID = "message_id"
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
public class SqlHelper extends SQLiteOpenHelper {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
private static final int DATABASE_VERSION = 7;
|
||||
private static final String DATABASE_NAME = "jabit.db";
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
public SqlHelper(Context ctx) {
|
||||
super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
executeMigration(db, "V1.0__Create_table_inventory");
|
||||
executeMigration(db, "V1.1__Create_table_address");
|
||||
executeMigration(db, "V1.2__Create_table_message");
|
||||
case 1:
|
||||
// executeMigration(db, "V2.0__Update_table_message");
|
||||
executeMigration(db, "V2.1__Create_table_POW");
|
||||
case 2:
|
||||
executeMigration(db, "V3.0__Update_table_address");
|
||||
case 3:
|
||||
executeMigration(db, "V3.1__Update_table_POW");
|
||||
executeMigration(db, "V3.2__Update_table_message");
|
||||
case 4:
|
||||
executeMigration(db, "V3.3__Create_table_node");
|
||||
case 5:
|
||||
executeMigration(db, "V3.4__Add_label_outbox");
|
||||
case 6:
|
||||
executeMigration(db, "V4.0__Create_table_message_parent");
|
||||
default:
|
||||
// Nothing to do. Let's assume we won't upgrade from a version that's newer than
|
||||
// DATABASE_VERSION.
|
||||
}
|
||||
}
|
||||
|
||||
private void executeMigration(SQLiteDatabase db, String name) {
|
||||
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
|
||||
db.execSQL(statement);
|
||||
}
|
||||
}
|
||||
|
||||
static StringBuilder join(long... numbers) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append(numbers[i]);
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
|
||||
static StringBuilder join(Enum<?>... types) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append('\'').append(types[i].name()).append('\'');
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION)
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf(
|
||||
0 to {
|
||||
executeMigration(db, "V1.0__Create_table_inventory")
|
||||
executeMigration(db, "V1.1__Create_table_address")
|
||||
executeMigration(db, "V1.2__Create_table_message")
|
||||
},
|
||||
1 to {
|
||||
// executeMigration(db, "V2.0__Update_table_message");
|
||||
executeMigration(db, "V2.1__Create_table_POW")
|
||||
},
|
||||
2 to {
|
||||
executeMigration(db, "V3.0__Update_table_address")
|
||||
},
|
||||
3 to {
|
||||
executeMigration(db, "V3.1__Update_table_POW")
|
||||
executeMigration(db, "V3.2__Update_table_message")
|
||||
},
|
||||
4 to {
|
||||
executeMigration(db, "V3.3__Create_table_node")
|
||||
},
|
||||
5 to {
|
||||
executeMigration(db, "V3.4__Add_label_outbox")
|
||||
},
|
||||
6 to {
|
||||
executeMigration(db, "V4.0__Create_table_message_parent")
|
||||
},
|
||||
7 to {
|
||||
setMissingConversationIds(db)
|
||||
}
|
||||
).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() }
|
||||
|
||||
/**
|
||||
* Set UUIDs for all messages that have no conversation ID
|
||||
*/
|
||||
private fun setMissingConversationIds(db: SQLiteDatabase) = db.query(
|
||||
"Message", arrayOf("id"),
|
||||
"conversation IS NULL", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(0)
|
||||
setMissingConversationId(id, db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
|
||||
val values = ContentValues(1).apply {
|
||||
put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
|
||||
}
|
||||
db.update("Message", values, "id=?", arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
private fun executeMigration(db: SQLiteDatabase, name: String) {
|
||||
for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) {
|
||||
db.execSQL(statement)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
private const val DATABASE_VERSION = 7
|
||||
const val DATABASE_NAME = "jabit.db"
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import ch.dissem.apps.abit.notification.BatchNotification
|
||||
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.util.*
|
||||
|
||||
class BatchProcessorService : Service() {
|
||||
private lateinit var notification: BatchNotification
|
||||
|
||||
override fun onCreate() {
|
||||
notification = BatchNotification(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = BatchBinder(this)
|
||||
|
||||
class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() {
|
||||
private val notification = service.notification
|
||||
|
||||
fun process(job: Job) = synchronized(queue) {
|
||||
ContextCompat.startForegroundService(
|
||||
service,
|
||||
Intent(service, BatchProcessorService::class.java)
|
||||
)
|
||||
service.startForeground(
|
||||
ONGOING_NOTIFICATION_ID,
|
||||
notification.notification
|
||||
)
|
||||
if (!working) {
|
||||
working = true
|
||||
service.processQueue(job)
|
||||
} else {
|
||||
queue.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processQueue(job: Job) {
|
||||
doAsync {
|
||||
var next: Job? = job
|
||||
while (next != null) {
|
||||
next.process(notification)
|
||||
|
||||
synchronized(queue) {
|
||||
next = queue.poll()
|
||||
if (next == null) {
|
||||
working = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var working = false
|
||||
private val queue = LinkedList<Job>()
|
||||
}
|
||||
}
|
||||
|
||||
interface Job {
|
||||
val icon: Int
|
||||
@DrawableRes get
|
||||
|
||||
val description: Int
|
||||
@StringRes get
|
||||
|
||||
val numberOfItems: Int
|
||||
var numberOfProcessedItems: Int
|
||||
|
||||
/**
|
||||
* Runs the job. This shouldn't happen in a separate thread, as this is handled by the service.
|
||||
*/
|
||||
fun process(notification: BatchNotification)
|
||||
}
|
||||
|
||||
data class SimpleJob<T>(
|
||||
override val numberOfItems: Int,
|
||||
/**
|
||||
* Provides the next batch of items, given the last item of the previous batch,
|
||||
* or null for the first batch.
|
||||
*/
|
||||
private val provider: (T?) -> List<T>,
|
||||
/**
|
||||
* Processes an item.
|
||||
*/
|
||||
private val processor: (T) -> Unit,
|
||||
override val icon: Int,
|
||||
override val description: Int
|
||||
) : Job {
|
||||
override var numberOfProcessedItems: Int = 0
|
||||
|
||||
override fun process(notification: BatchNotification) {
|
||||
notification.update(this)
|
||||
var batch = provider.invoke(null)
|
||||
while (batch.isNotEmpty()) {
|
||||
Thread.yield()
|
||||
batch.forEach {
|
||||
processor.invoke(it)
|
||||
Thread.yield()
|
||||
}
|
||||
numberOfProcessedItems += batch.size
|
||||
notification.update(this)
|
||||
batch = provider.invoke(batch.last())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,73 +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.IntentService;
|
||||
import android.content.Intent;
|
||||
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class BitmessageIntentService extends IntentService {
|
||||
public static final String EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage";
|
||||
public static final String EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode";
|
||||
public static final String EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode";
|
||||
|
||||
private BitmessageContext bmc;
|
||||
|
||||
public BitmessageIntentService() {
|
||||
super("BitmessageIntentService");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
bmc = Singleton.getBitmessageContext(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (intent.hasExtra(EXTRA_DELETE_MESSAGE)) {
|
||||
Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_DELETE_MESSAGE);
|
||||
bmc.labeler().delete(item);
|
||||
bmc.messages().save(item);
|
||||
Singleton.getMessageListener(this).resetNotification();
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_STARTUP_NODE)) {
|
||||
if (Preferences.isConnectionAllowed(this)) {
|
||||
startService(new Intent(this, BitmessageService.class));
|
||||
updateNodeSwitch();
|
||||
} else {
|
||||
Intent dialogIntent = new Intent(this, FullNodeDialogActivity.class);
|
||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(dialogIntent);
|
||||
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
||||
stopService(new Intent(this, BitmessageService.class));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class BitmessageIntentService : IntentService("BitmessageIntentService") {
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
}
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
intent?.let {
|
||||
if (it.hasExtra(EXTRA_DELETE_MESSAGE)) {
|
||||
val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext
|
||||
bmc.labeler.delete(item)
|
||||
bmc.messages.save(item)
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
}
|
||||
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
|
||||
network.enableNode()
|
||||
}
|
||||
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
||||
network.disableNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"
|
||||
const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"
|
||||
const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user