77 Commits

Author SHA1 Message Date
8b89d81970 😴 Minor improvements 2018-06-13 19:48:00 +02:00
76317a2488 🔀 Merge branch 'feature/conversations' into develop 2018-06-12 16:58:19 +02:00
85f114a33d 🔌 Add preference to connect on charging only 2018-06-12 16:56:15 +02:00
9e7f247763 ⬆️ Bump android build tools version 2018-06-12 16:54:00 +02:00
ec4615b639 🐘 Tweak memory usage 2018-06-12 16:52:42 +02:00
2ddd78dfe2 📡 Improve and simplify connectivity handling 2018-06-04 12:55:00 +02:00
90bb538692 🚀 Improve performance 2018-05-25 20:48:42 +02:00
9cc07f73ae 🐛 Prevent ANR 2018-05-24 21:08:06 +02:00
0b432b6a67 🔃 Switch showcase library
Which, unfortunately, pulls along a lot of other
changes (mostly for the better)
2018-05-23 19:04:27 +02:00
725ec60fd4 📄 Add some project documentation
🤝 Code of Conduct
👩‍💻 Contribution
📄 License
2018-05-17 11:06:20 +00:00
60c4a4d8a0 ⬆️ Kotlin version bump 2018-04-22 12:06:10 +02:00
6585876b25 🚸 Add number of messages to conversation list item 2018-04-22 11:29:39 +02:00
b1fd9d9ef9 😴 Minor code style improvements 2018-04-20 17:51:14 +02:00
e05d27bfbc 🎨 Conversation rendering improvements 2018-04-20 07:04:31 +02:00
be7a7f1af6 🎨 Identicon rendering improvements 2018-04-20 07:00:55 +02:00
61e579c0d4 🚸 Improve settings structure 2018-04-17 19:55:56 +02:00
eee1be873a 🎨 Add new icon vor Oreo and later 2018-04-17 09:42:25 +02:00
4c213d3e9c Merge branch 'develop' into feature/conversations 2018-04-14 20:44:19 +02:00
76cb5df998 🚸 Improved network notification 2018-04-14 20:42:53 +02:00
3026ae8505 🐛 Fixed bug 2018-04-14 20:42:10 +02:00
412180f443 Add batch service for migrating existing messages 2018-04-14 20:27:29 +02:00
78f9621afa 🚧 Add code to migrate existing conversations
Work in progress: does work, but usually doesn't finish. This needs to be
moved into some proper batch processing.
2018-04-13 12:39:59 +02:00
85562efc0d ⬆️ Update Android build tools 2018-04-13 12:39:20 +02:00
a89f80f400 🎨 minor multi-identicon improvements/fixes 2018-04-13 07:45:31 +02:00
1426b786e8 Add message grouping by subject 2018-04-03 22:14:46 +02:00
6a311a0346 Fix minor lint warning 2018-04-03 22:14:00 +02:00
9b75a8c2ef Fixed tests 2018-04-03 22:13:23 +02:00
4e5ba4401a Improved multi identicon background colour 2018-04-03 22:12:13 +02:00
f374748f71 Fixed tests and updated dependencies 2018-04-03 22:11:37 +02:00
49e77199b0 Improve utilities 2018-03-23 17:50:43 +01:00
46e5bb7ece Improve conversation view 2018-03-18 07:00:21 +01:00
8004865e01 Nicer labels 2018-03-12 21:18:10 +01:00
101913a531 Show messages in inbox and archive as conversations
Work in progress - detail view not yet adapted, and needs extended encoding
for sensible results.
2018-03-05 09:48:49 +01:00
40f8bc87a2 Bump dependencies 2018-03-05 09:43:40 +01:00
0d1cfff883 Fix lint issues 2018-02-27 12:54:30 +01:00
d7b7b11cdf Fix deleting messages from archive 2018-02-27 06:43:45 +01:00
d8d5f70b37 Merge tag '1.0-rc1.1' into develop
1.0-rc1.1
2018-02-25 23:38:45 +01:00
06e99ea0cd Merge branch 'hotfix/1.0-rc1.1' 2018-02-25 23:38:18 +01:00
ad3af929d7 Hotfix: properly select recipient 2018-02-25 23:35:03 +01:00
16f1dfa6f6 Merge tag '1.0-rc1' into develop
1.0-rc1
2018-02-24 08:50:31 +01:00
2bddd0f256 Merge branch 'release/1.0-rc1' 2018-02-24 08:50:13 +01:00
ab70e6df12 Version 1.0-rc1 bump 2018-02-24 08:49:05 +01:00
fb72356467 Retain content of compose view on orientation change 2018-02-24 07:45:33 +01:00
e3c7c4d557 Merge branch 'develop' into 'develop'
Develop

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

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

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

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

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

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

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

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

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

Translation: Abit/App
Translate-URL: http://translations.dissem.ch/projects/abit/app/fr/
2018-02-11 10:01:13 +00:00
66c8536a84 Added translation using Weblate (French) 2018-02-09 21:48:09 +00:00
72213a53a5 Merge tag '1.0-beta20' into develop
1.0-beta20
2018-01-27 21:43:43 +01:00
8a668f4af2 Merge branch 'release/1.0-beta20' 2018-01-27 21:43:22 +01:00
6829614da0 Version 1.0-beta20 bump 2018-01-27 21:28:18 +01:00
1906a2e13c Fixed crash on Oreo and issue on mobile network 2018-01-27 21:10:43 +01:00
708529fc0a Merge tag '1.0-beta19' into develop
1.0-beta19
2018-01-18 22:50:21 +01:00
b5dbbeb46a Merge branch 'release/1.0-beta19' 2018-01-18 22:50:08 +01:00
b368c8251d 1.0-beta19 2018-01-18 22:49:55 +01:00
6986d9a2df Bumped Kotlin version 2018-01-18 22:38:11 +01:00
35249a0145 Improved some repository code in search for an unclosed cursor
(that I didn't find)
2018-01-18 22:37:40 +01:00
2a1aa736cc Fixed newly introduced bugs 2018-01-18 22:33:07 +01:00
9f26ade617 Merge tag '1.0-beta18' into develop
Version 1.0-beta18
2018-01-18 17:26:11 +01:00
112 changed files with 3950 additions and 1238 deletions

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abit@dissem.ch. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/

16
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,16 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue or another method with the owners of this repository before making a change.
Make sure Abit is the right place for the change - changes on the protocol level might be better placed in [Jabit](https://git.dissem.ch/bitmessage/Jabit).
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. A pull request should always merge into `develop`; only releases are merged into master.
3. A reviewer usually merges the pull requests. They may however request you to do it in case you have sufficient permissions, or may request you to update your branch to include the latest changes from `develop`.

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -14,14 +14,17 @@ if (project.hasProperty("project.configs")
//noinspection GroovyMissingReturnStatement
android {
compileSdkVersion 27
buildToolsVersion "26.0.2"
buildToolsVersion "27.0.3"
signingConfigs {
release
}
defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19
targetSdkVersion 27
versionCode 18
versionName "1.0-beta18"
versionCode 23
versionName "1.0-rc1"
multiDexEnabled true
}
compileOptions {
@ -51,62 +54,64 @@ android {
//ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
ext.supportVersion = '27.0.2'
ext.supportVersion = '27.1.1'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:preference-v7:$supportVersion"
implementation "com.android.support:cardview-v7:$supportVersion"
implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:support-v13:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:multidex:1.0.2"
implementation "com.android.support:multidex:1.0.3"
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
testImplementation "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
implementation 'org.slf4j:slf4j-android:1.7.25'
implementation 'com.mikepenz:materialize:1.1.2@aar'
implementation('com.mikepenz:materialdrawer:6.0.2@aar') {
implementation('com.mikepenz:materialdrawer:6.0.6@aar') {
transitive = true
}
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') {
implementation('com.mikepenz:aboutlibraries:6.0.6@aar') {
transitive = true
}
implementation "com.mikepenz:iconics-core:3.0.0@aar"
implementation "com.mikepenz:iconics-views:3.0.0@aar"
implementation "com.mikepenz:iconics-core:3.0.3@aar"
implementation "com.mikepenz:iconics-views:3.0.3@aar"
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
implementation 'com.google.zxing:core:3.3.1'
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
implementation 'com.google.zxing:core:3.3.2'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8'
implementation 'com.github.amlcurran.showcaseview:library:5.4.3'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
transitive = true
}
implementation 'com.github.angads25:filepicker:1.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.1.1'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.13.0'
testImplementation 'org.mockito:mockito-core:2.15.0'
testImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.6.1'
testImplementation "org.robolectric:shadows-multidex:3.6.1"
testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation "org.robolectric:shadows-multidex:3.7.1"
androidTestImplementation "com.android.support:multidex:1.0.2"
androidTestImplementation "com.android.support:multidex:1.0.3"
}
idea.module {

View File

@ -1,32 +1,33 @@
<?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.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.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.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<application
android:name="android.support.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">
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
@ -36,7 +37,7 @@
tools:ignore="UnusedAttribute">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
android:value=".MainActivity" />
</activity>
<activity
android:name=".AddressDetailActivity"
@ -45,42 +46,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"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
@ -88,14 +89,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
@ -104,34 +105,34 @@
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"/>
android:exported="false" />
<service
android:name=".service.ProofOfWorkService"
android:exported="false"/>
android:exported="false" />
<!-- Synchronization -->
<provider
android:name=".synchronization.StubProvider"
android:authorities="ch.dissem.apps.abit.provider"
android:exported="false"
android:syncable="true"/>
android:syncable="true" />
<!-- Exports -->
<provider
@ -149,44 +150,58 @@
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator"/>
android:resource="@xml/authenticator" />
</service>
<service
android:name=".synchronization.SyncService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/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" android:enabled="@bool/is_pre_api_21">
<receiver
android:name=".listener.WifiReceiver"
android:enabled="@bool/is_pre_api_21">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
<!-- This is bad for battery life, but needed on older devices to check
if WiFi is available. Let's be honest, the whole app is bad for
battery life. -->
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
</intent-filter>
</receiver>
<receiver android:name=".service.StartServiceReceiver" android:enabled="@bool/is_post_api_21">
<receiver
android:name=".service.StartServiceReceiver"
android:enabled="@bool/is_post_api_21">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".service.StartupNodeOnWifiService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".service.BatchProcessorService"
android:exported="false" />
<activity
android:name=".StatusActivity"
@ -194,7 +209,7 @@
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
android:value=".MainActivity" />
</activity>
</application>

View File

@ -27,7 +27,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
/**
* @author Christian Basler
*/
abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> {
abstract class AbstractItemListFragment<in L, T> : ListFragment(), ListHolder<L> {
/**
* The fragment's current callback object, which is notified of list item
* clicks.

View File

@ -26,6 +26,7 @@ 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
@ -185,7 +186,7 @@ class AddressDetailFragment : Fragment() {
}
// QR code
qr_code.setImageBitmap(Drawables.qrCode(item))
qr_code.setImageBitmap(item.qrCode())
}
}
@ -204,7 +205,7 @@ class AddressDetailFragment : Fragment() {
* The fragment argument representing the item ID that this fragment
* represents.
*/
val ARG_ITEM = "item"
val EXPORT_POSTFIX = ".keys.dat"
const val ARG_ITEM = "item"
const val EXPORT_POSTFIX = ".keys.dat"
}
}

View File

@ -27,7 +27,6 @@ import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.integration.android.IntentIntegrator
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
@ -48,7 +47,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
activity,
R.layout.subscription_row,
R.id.name,
LinkedList()) {
LinkedList()
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val result: View
val v: ViewHolder
@ -72,7 +72,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
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
v.subscribed.visibility =
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
}
return result
}
@ -105,11 +106,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
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)
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu)
activity.initFab(R.drawable.ic_action_add_contact, menu)
.addOnMenuItemClickListener { _, _, itemId ->
when (itemId) {
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.initiateScan()
2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
@ -121,7 +122,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_address_list, container, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -43,16 +43,19 @@ class ComposeMessageActivity : AppCompatActivity() {
setHomeButtonEnabled(false)
}
// Display the fragment as the main content.
val fragment = ComposeMessageFragment()
fragment.arguments = intent.extras
supportFragmentManager
.beginTransaction()
.replace(R.id.content, fragment)
.commit()
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"
@ -62,10 +65,13 @@ class ComposeMessageActivity : AppCompatActivity() {
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
))
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))
@ -89,15 +95,21 @@ class ComposeMessageActivity : AppCompatActivity() {
}
replyIntent.putExtra(EXTRA_PARENT, item)
item.subject?.let { subject ->
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals("RE:", ignoreCase = true)) {
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!!)
replyIntent.putExtra(
EXTRA_CONTENT,
"\n\n------------------------------------------------------\n${item.text ?: ""}"
)
return replyIntent
}
}

View File

@ -17,6 +17,7 @@
package ch.dissem.apps.abit
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
@ -25,6 +26,7 @@ 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
@ -38,6 +40,9 @@ 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.*
@ -52,72 +57,109 @@ class ComposeMessageFragment : Fragment() {
private var broadcast: Boolean = false
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
private var parent: Plaintext? = null
private val parents = mutableListOf<InventoryVector>()
private var draft: Plaintext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
var id = arguments.getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && (id == null || id.privateKey == null)) {
id = Singleton.getIdentity(context!!)
}
if (id?.privateKey != null) {
identity = id
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 {
throw IllegalStateException("No identity set for ComposeMessageFragment")
}
broadcast = arguments.getBoolean(EXTRA_BROADCAST, false)
if (arguments.containsKey(EXTRA_RECIPIENT)) {
recipient = arguments.getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
}
if (arguments.containsKey(EXTRA_SUBJECT)) {
subject = arguments.getString(EXTRA_SUBJECT)
}
if (arguments.containsKey(EXTRA_CONTENT)) {
content = arguments.getString(EXTRA_CONTENT)
}
encoding = arguments.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && (id == 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)
}
if (containsKey(EXTRA_CONTENT)) {
content = getString(EXTRA_CONTENT)
}
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?:
Plaintext.Encoding.SIMPLE
if (arguments.containsKey(EXTRA_PARENT)) {
parent = arguments.getSerializable(EXTRA_PARENT) as Plaintext
if (containsKey(EXTRA_PARENT)) {
val parent = getSerializable(EXTRA_PARENT) as Plaintext
parent.inventoryVector?.let { parents.add(it) }
}
}
} ?: {
throw IllegalStateException("No identity set for ComposeMessageFragment")
}.invoke()
} ?: 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 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)
if (broadcast) {
recipient_input.visibility = View.GONE
} else {
val adapter = ContactAdapter(context!!)
recipient_input.setAdapter(adapter)
recipient_input.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> 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
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)
}
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)
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)
}
}
}
}
@ -146,18 +188,17 @@ class ComposeMessageFragment : Fragment() {
}
}
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)
}
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 send() {
private fun build(ctx: Context): Plaintext {
val builder: Plaintext.Builder
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
val bmc = Singleton.getBitmessageContext(ctx)
if (broadcast) {
builder = Plaintext.Builder(BROADCAST).from(identity)
builder = Plaintext.Builder(BROADCAST)
} else {
val inputString = recipient_input.text.toString()
if (recipient == null || recipient?.toString() != inputString) {
@ -175,42 +216,77 @@ class ComposeMessageFragment : Fragment() {
}
}
if (recipient == null) {
Toast.makeText(context, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG).show()
return
}
builder = Plaintext.Builder(MSG)
.from(identity)
.to(recipient)
.to(recipient)
}
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
sender?.let { builder.from(it) }
if (!Preferences.requestAcknowledgements(ctx)) {
builder.preventAck()
}
when (encoding) {
Plaintext.Encoding.SIMPLE -> builder.message(
subject_input.text.toString(),
body_input.text.toString()
subject_input.text.toString(),
body_input.text.toString()
)
Plaintext.Encoding.EXTENDED -> builder.message(
Message.Builder()
.subject(subject_input.text.toString())
.body(body_input.text.toString())
.addParent(parent)
.build()
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
ctx,
ctx.getString(R.string.error_unsupported_encoding, encoding),
Toast.LENGTH_LONG
).show()
builder.message(
subject_input.text.toString(),
body_input.text.toString()
subject_input.text.toString(),
body_input.text.toString()
)
}
}
bmc.send(builder.build())
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()
}
}

View File

@ -0,0 +1,128 @@
/*
* 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.v4.app.Fragment
import android.support.v7.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.value)
messages.layoutManager = LinearLayoutManager(activity)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.conversation, menu)
activity?.let { activity ->
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val messageRepo = Singleton.getMessageRepository(
context ?: throw IllegalStateException("No context available")
)
item?.let { item ->
when (menuItem.itemId) {
R.id.delete -> {
item.messages.forEach {
Singleton.labeler.delete(it)
messageRepo.remove(it)
}
MainActivity.apply { updateUnread() }
activity?.onBackPressed()
return true
}
R.id.archive -> {
item.messages.forEach {
Singleton.labeler.archive(it)
messageRepo.save(it)
}
MainActivity.apply { updateUnread() }
return true
}
else -> return false
}
}
return false
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
const val ARG_ITEM_ID = "item_id"
}
}

View File

@ -0,0 +1,339 @@
/*
* 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.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.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.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.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.ConversationService
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
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 var wrappedAdapter: RecyclerView.Adapter<*>? = null
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = 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 lateinit var messageRepo: AndroidMessageRepository
private lateinit var conversationService: ConversationService
private var activateOnItemClick: Boolean = false
private val backStack = Stack<Label>()
fun loadMoreItems() {
isLoading = true
swipeableConversationAdapter?.let { messageAdapter ->
doAsync {
val conversationIds = messageRepo.findConversations(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE
)
conversationIds.forEach { conversationId ->
val conversation = conversationService.getConversation(conversationId)
onUiThread {
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)
currentLabel.addObserver(this) { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
currentLabel.removeObserver(this)
super.onPause()
}
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
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")
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
setInterceptVerticalScrollingWhileAnimationRunning(true)
isEnabled = true
}
// swipe manager
val swipeManager = RecyclerViewSwipeManager()
//swipeableConversationAdapter
val adapter = SwipeableConversationAdapter(context).apply {
setActivateOnItemClick(activateOnItemClick)
}
adapter.eventListener = object : SwipeableConversationAdapter.EventListener {
override fun onItemDeleted(item: Conversation) {
item.messages.forEach {
Singleton.labeler.delete(it)
messageRepo.save(it)
}
}
override fun onItemArchived(item: Conversation) {
item.messages.forEach { Singleton.labeler.archive(it) }
}
override fun onItemViewClicked(v: View?) {
val position = recycler_view.getChildAdapterPosition(v)
adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) {
MainActivity.apply { onItemSelected(adapter.getItem(position)) }
}
}
}
// wrap for swiping
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
val animator = SwipeDismissItemAnimator()
// Change animations are enabled by default since support-v7-recyclerview v22.
// Disable the change animation in order to make turning back animation of swiped item
// works properly.
animator.supportsChangeAnimations = false
recycler_view.layoutManager = layoutManager
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableConversationAdapter
recycler_view.itemAnimator = animator
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(context, 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
touchActionGuardManager.attachRecyclerView(recycler_view)
swipeManager.attachRecyclerView(recycler_view)
recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager
swipeableConversationAdapter = adapter
// 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() {
recyclerViewSwipeManager?.release()
recyclerViewSwipeManager = null
recyclerViewTouchActionGuardManager?.release()
recyclerViewTouchActionGuardManager = null
recycler_view.itemAnimator = null
recycler_view.adapter = null
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
wrappedAdapter = null
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)
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
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
return true
}
else -> return false
}
}
override fun updateList(label: Label) {
currentLabel.value = label
}
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableConversationAdapter?.setActivateOnItemClick(activateOnItemClick)
this.activateOnItemClick = activateOnItemClick
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.value = backStack.pop()
true
}
}

View File

@ -18,9 +18,11 @@ package ch.dissem.apps.abit
import android.graphics.*
import android.graphics.drawable.Drawable
import android.support.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
@ -45,8 +47,20 @@ class Identicon(input: BitmessageAddress) : Drawable() {
}
}
}
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f))
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f))
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()
@ -54,30 +68,34 @@ class Identicon(input: BitmessageAddress) : Drawable() {
}
override fun draw(canvas: Canvas) {
var x: Float
var y: Float
val width = canvas.width.toFloat()
val height = canvas.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(width / 2, height / 2, width / 2, paint)
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 = cellWidth * column
y = cellHeight * row
x = offsetX + cellWidth * column
y = offsetY + cellHeight * row
canvas.drawCircle(
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint
)
}
}
}
if (isChan) {
textPaint.textSize = 2 * cellHeight
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint)
canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
}
}
@ -92,7 +110,72 @@ class Identicon(input: BitmessageAddress) : Drawable() {
override fun getOpacity() = PixelFormat.TRANSPARENT
companion object {
private val SIZE = 9
private val CENTER_COLUMN = 5
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.sortedBy { it.isChan }.map { Identicon(it) }.take(4)
override fun draw(canvas: Canvas) {
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
when (identicons.size) {
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
1 -> identicons.first().draw(canvas, 0f, 0f, width, height)
2 -> {
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
val w = width / 2
val h = height / 2
var x = 0f
val y = height / 4
identicons.forEach {
it.draw(canvas, x, y, w, h)
x += w
}
}
3 -> {
val scale = 2f / (1f + 2f * sqrt(3f))
val w = width * scale
val h = height * scale
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
}
4 -> {
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
val scale = 1f / (1f + sqrt(2f))
val borderScale = 0.5f - scale
val w = width * scale
val h = height * scale
val x = width * borderScale
val y = height * borderScale
identicons.forEachWithIndex { i, identicon ->
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
}
}
}
}
override fun setAlpha(alpha: Int) {
identicons.forEach { it.alpha = alpha }
}
override fun setColorFilter(colorFilter: ColorFilter?) {
identicons.forEach { it.colorFilter = colorFilter }
}
override fun getOpacity() = PixelFormat.TRANSPARENT
}

View File

@ -25,12 +25,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.wif.WifImporter
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import org.ini4j.InvalidFileFormatException
import org.jetbrains.anko.longToast
/**
* @author Christian Basler
@ -39,8 +39,12 @@ 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 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?) {
super.onViewCreated(view, savedInstanceState)
@ -48,17 +52,29 @@ class ImportIdentitiesFragment : Fragment() {
val wifData = arguments.getString(WIF_DATA)
val bmc = Singleton.getBitmessageContext(activity)
importer = WifImporter(bmc, wifData)
try {
importer = WifImporter(bmc, wifData)
} catch (e: InvalidFileFormatException) {
longToast(R.string.invalid_wif_file)
activity.finish()
return
}
adapter = AddressSelectorAdapter(importer.getIdentities())
val layoutManager = LinearLayoutManager(activity,
LinearLayoutManager.VERTICAL,
false)
val layoutManager = LinearLayoutManager(
activity,
LinearLayoutManager.VERTICAL,
false
)
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
recyclerView.addItemDecoration(SimpleListDividerDecorator(
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true))
recyclerView.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true
)
)
view.findViewById<Button>(R.id.finish).setOnClickListener {
importer.importAll(adapter.selected)
@ -72,6 +88,6 @@ class ImportIdentitiesFragment : Fragment() {
}
companion object {
val WIF_DATA = "wif_data"
const val WIF_DATA = "wif_data"
}
}

View File

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

View File

@ -17,14 +17,14 @@
package ch.dissem.apps.abit
import android.content.Intent
import android.graphics.Point
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener
@ -32,14 +32,15 @@ import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARC
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.getColor
import ch.dissem.apps.abit.util.getIcon
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.github.amlcurran.showcaseview.ShowcaseView
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
@ -52,9 +53,13 @@ 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 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.*
@ -110,7 +115,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val listFragment = MessageListFragment()
val listFragment = ConversationListFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.item_list, listFragment)
@ -146,30 +151,33 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
SyncAdapter.stopSync(this)
}
if (drawer.isDrawerOpen) {
val lps = RelativeLayout.LayoutParams(ViewGroup
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
lps.setMargins(margin, margin, margin, margin)
ShowcaseView.Builder(this)
.withMaterialShowcase()
.setStyle(R.style.CustomShowcaseTheme)
.setContentTitle(R.string.full_node)
MaterialShowcaseView.Builder(this)
.setMaskColour(R.color.colorPrimary)
.setTitleText(R.string.full_node)
.setContentText(R.string.full_node_description)
.setTarget {
val view = drawer.stickyFooter
val location = IntArray(2)
view.getLocationInWindow(location)
val x = location[0] + 7 * view.width / 8
val y = location[1] + view.height / 2
Point(x, y)
}
.replaceEndButton(R.layout.showcase_button)
.hideOnTouchOutside()
.build()
.setButtonPosition(lps)
.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()
}
}
@ -192,19 +200,23 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
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.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())
profiles.add(
ProfileSettingDrawerItem()
.withName(getString(R.string.manage_identity))
.withIcon(GoogleMaterial.Icon.gmd_settings)
.withIdentifier(MANAGE_IDENTITY.toLong())
)
// Create the AccountHeader
accountHeader = AccountHeaderBuilder()
@ -212,25 +224,36 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.withHeaderBackground(R.drawable.header)
.withProfiles(profiles)
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
.withOnAccountHeaderListener(ProfileSelectionListener(this@MainActivity, supportFragmentManager))
.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()
.withName(R.string.archive)
.withTag(LABEL_ARCHIVE)
.withIcon(CommunityMaterial.Icon.cmd_archive)
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))
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)
@ -281,6 +304,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
} else if (currentLabel.value == null) {
currentLabel.value = labels[0]
}
for (label in labels) {
addLabelEntry(label)
@ -306,8 +330,14 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val tag = item.tag
if (tag is Label) {
currentLabel.value = tag
if (itemList !is MessageListFragment) {
changeList(MessageListFragment())
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<*>) {
@ -343,7 +373,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
}
Singleton.getMessageListener(this).resetNotification()
currentLabel.addObserver(this) { label ->
if (label != null) {
if (label != null && label.id is Long) {
drawer.setSelection(label.id as Long)
}
}
@ -368,7 +398,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// we know that there are 2 setting elements.
// Set the new profile above them ;)
accountHeader.addProfile(
newProfile, accountHeader.profiles.size - 2)
newProfile, accountHeader.profiles.size - 2
)
} else {
accountHeader.addProfiles(newProfile)
}
@ -379,8 +410,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.withIdentifier(label.id as Long)
.withName(label.toString())
.withTag(label)
.withIcon(Labels.getIcon(label))
.withIconColor(Labels.getColor(label))
.withIcon(label.getIcon())
.withIconColor(label.getColor(0xFF000000.toInt()))
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
}
@ -419,7 +450,9 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
} else {
(item as PrimaryDrawerItem).withBadge(null as String?)
}
drawer.updateItem(item)
runOnUiThread {
drawer.updateItem(item)
}
}
}
}
@ -434,14 +467,38 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
val fragment = when (item) {
is Plaintext -> MessageDetailFragment()
is BitmessageAddress -> AddressDetailFragment()
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}")
}
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
@ -449,20 +506,35 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
val detailIntent = when (item) {
is Plaintext -> {
Intent(this, MessageDetailActivity::class.java)
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)
}
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java)
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
}
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
startActivity(detailIntent)
}
}
fun setDetailView(fragment: Fragment) {
if (hasDetailPane) {
supportFragmentManager.beginTransaction()
supportFragmentManager
.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
}
@ -472,16 +544,35 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
supportActionBar?.title = title
}
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
fab.removeAllOnMenuItemClickListeners()
fab.show()
fab.closeMenu()
val mainFab = fab.mainFab
mainFab.setImageResource(drawableRes)
fab.setMenu(menu)
fab.addOnStateChangeListener { isOpened: Boolean ->
if (isOpened) {
// It will be turned 45 degrees, which makes an x out of the +
mainFab.setImageResource(R.drawable.ic_action_add)
} else {
mainFab.setImageResource(drawableRes)
}
}
return fab
}
companion object {
val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
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"
val ADD_IDENTITY = 1
val MANAGE_IDENTITY = 2
const val ADD_IDENTITY = 1
const val MANAGE_IDENTITY = 2
private val ID_NODE_SWITCH: Long = 1
private const val ID_NODE_SWITCH: Long = 1
private var instance: WeakReference<MainActivity>? = null

View File

@ -4,6 +4,8 @@ import android.content.Intent
import android.os.Bundle
import android.support.v4.app.NavUtils
import android.view.MenuItem
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
/**
@ -33,13 +35,19 @@ class MessageDetailActivity : DetailActivity() {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM))
val fragment = MessageDetailFragment()
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()
.add(R.id.content, fragment)
.commit()
}
}

View File

@ -29,17 +29,17 @@ 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.Assets
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.Labels
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 com.mikepenz.iconics.view.IconicsImageView
import kotlinx.android.synthetic.main.fragment_message_detail.*
import java.util.*
@ -70,7 +70,11 @@ class MessageDetailFragment : Fragment() {
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -81,9 +85,16 @@ class MessageDetailFragment : Fragment() {
// Show the dummy content as text in a TextView.
item?.let { item ->
subject.text = item.subject
status.setImageResource(Assets.getStatusDrawable(item.status))
status.contentDescription = getString(Assets.getStatusString(item.status))
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()
@ -124,7 +135,11 @@ class MessageDetailFragment : Fragment() {
}
}
private fun showRelatedMessages(ctx: Context, rootView: View, @IdRes id: Int, messages: List<Plaintext>) {
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
@ -136,8 +151,10 @@ class MessageDetailFragment : Fragment() {
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.mark_unread, GoogleMaterial.Icon
.gmd_markunread
)
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
}
@ -187,9 +204,15 @@ class MessageDetailFragment : Fragment() {
return false
}
private class RelatedMessageAdapter internal constructor(private val ctx: Context, private val messages: List<Plaintext>) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
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 {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RelatedMessageAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
@ -206,7 +229,7 @@ class MessageDetailFragment : Fragment() {
val message = messages[position]
viewHolder.avatar.setImageDrawable(Identicon(message.from))
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status))
viewHolder.status.setImageResource(message.status.getDrawable())
viewHolder.sender.text = message.from.toString()
viewHolder.extract.text = prepareMessageExtract(message.text)
viewHolder.item = message
@ -236,45 +259,12 @@ class MessageDetailFragment : Fragment() {
}
}
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) : RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
private val labels = labels.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val contactView = inflater.inflate(R.layout.item_label, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val label = labels[position]
viewHolder.icon.icon?.color(Labels.getColor(label))
viewHolder.icon.icon?.icon(Labels.getIcon(label))
viewHolder.label.text = Labels.getText(label, ctx)
}
override fun getItemCount() = labels.size
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
var label = itemView.findViewById<TextView>(R.id.label)!!
}
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
val ARG_ITEM = "item"
const val ARG_ITEM = "item"
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
}

View File

@ -33,7 +33,6 @@ 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.FabUtils
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
@ -80,7 +79,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
&& firstVisibleItemPosition >= 0) {
&& firstVisibleItemPosition >= 0
) {
loadMoreItems()
}
}
@ -98,7 +98,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
isLoading = true
swipeableMessageAdapter?.let { messageAdapter ->
doAsync {
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
val messages = messageRepo.findMessages(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE
)
onUiThread {
messageAdapter.addAll(messages)
isLoading = false
@ -149,7 +153,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -193,7 +201,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) {
val item = adapter.getItem(position)
(activity as MainActivity).onItemSelected(item)
MainActivity.apply { onItemSelected(item) }
}
}
}
@ -213,8 +221,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
recycler_view.itemAnimator = animator
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(SimpleListDividerDecorator(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true))
recycler_view.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
)
)
// NOTE:
// The initialization order is very important! This order determines the priority of
@ -226,7 +237,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager
this.swipeableMessageAdapter = adapter
swipeableMessageAdapter = adapter
Singleton.updateMessageListAdapterInListener(adapter)
}
@ -235,12 +246,14 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
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)
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
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()
Toast.makeText(
activity, R.string.no_identity_warning,
Toast.LENGTH_LONG
).show()
} else {
when (itemId) {
1 -> {

View File

@ -17,42 +17,65 @@
package ch.dissem.apps.abit
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.*
import android.os.Build
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle
import android.preference.PreferenceManager
import android.os.IBinder
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v4.content.FileProvider.getUriForFile
import android.support.v7.preference.Preference
import android.support.v7.preference.Preference.OnPreferenceChangeListener
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceScreen
import android.support.v7.preference.SwitchPreferenceCompat
import android.view.View
import android.widget.Toast
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.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.NetworkUtils
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.support.v4.indeterminateProgressDialog
import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.uiThread
import java.util.*
/**
* @author Christian Basler
*/
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener,
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
val cleanup = findPreference("cleanup")
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
findPreference("export")?.onPreferenceClickListener = exportClickListener()
findPreference("import")?.onPreferenceClickListener = importClickListener()
findPreference("status").onPreferenceClickListener = statusClickListener()
findPreference("status")?.onPreferenceClickListener = statusClickListener()
connectivityChangeListener().let {
findPreference("wifi_only")?.onPreferenceChangeListener = it
findPreference("require_charging")?.onPreferenceChangeListener = it
}
val emulateConversations = findPreference("emulate_conversations") as? SwitchPreferenceCompat
val conversationInit = findPreference("emulate_conversations_initialize")
emulateConversations?.onPreferenceChangeListener = emulateConversationChangeListener(conversationInit)
conversationInit?.onPreferenceClickListener = conversationInitClickListener()
conversationInit?.isEnabled = emulateConversations?.isChecked ?: false
}
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
@ -73,7 +96,8 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available")
val ctx = activity?.applicationContext
?: throw IllegalStateException("Context not available")
cleanup.isEnabled = false
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
@ -157,11 +181,12 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
override fun onAttach(ctx: Context?) {
super.onAttach(ctx)
(ctx as? MainActivity)?.floatingActionButton?.hide()
PreferenceManager.getDefaultSharedPreferences(ctx)
.registerOnSharedPreferenceChangeListener(this)
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
ctx?.let {
if (it is MainActivity) {
it.floatingActionButton?.hide()
it.updateTitle(getString(R.string.settings))
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
@ -193,6 +218,86 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
}
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?) =
OnPreferenceChangeListener { _, newValue ->
conversationInit?.isEnabled = newValue as Boolean
true
}
private fun connectivityChangeListener() =
OnPreferenceChangeListener { _, _ ->
context?.let { ctx ->
if (Build.VERSION.SDK_INT >= LOLLIPOP && Preferences.isFullNodeActive(ctx)) {
NetworkUtils.scheduleNodeStart(ctx)
}
}
true
}
// The why-is-it-so-damn-hard-to-group-preferences section
override fun getCallbackFragment(): Fragment = this
override fun onPreferenceStartScreen(
preferenceFragmentCompat: PreferenceFragmentCompat,
preferenceScreen: PreferenceScreen
): Boolean {
fragmentManager?.beginTransaction()?.let { ft ->
val fragment = SettingsFragment()
fragment.arguments = Bundle().apply {
putString(PreferenceFragmentCompat.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

View File

@ -20,25 +20,22 @@ 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 android.widget.*
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
import java.util.*
/**
* An adapter for contacts. Can be filtered by alias or address.
*/
class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
class ContactAdapter(
ctx: Context,
private val originalData: List<BitmessageAddress>,
private val slim: Boolean = false
) :
BaseAdapter(), Filterable {
private val inflater = LayoutInflater.from(ctx)
private val originalData = Singleton.getAddressRepository(ctx).getContacts()
private var data: List<BitmessageAddress> = originalData
override fun getCount() = data.size
@ -49,23 +46,33 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val viewHolder = if (convertView == null) {
ViewHolder(inflater.inflate(R.layout.contact_row, parent, false))
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
viewHolder.address?.text = item.address
return viewHolder.view
}
override fun getFilter(): Filter = ContactFilter()
private inner class ViewHolder(val view: View) {
val avatar = view.findViewById<ImageView>(R.id.avatar)!!
val name = view.findViewById<TextView>(R.id.name)!!
val address = view.findViewById<TextView>(R.id.address)!!
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
@ -83,27 +90,36 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
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()
.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
}
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()
}
}
} ?: {
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
@ -125,4 +141,6 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
}
}
}
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
}

View File

@ -0,0 +1,158 @@
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.support.v4.app.Fragment
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.PopupMenu
import android.support.v7.widget.RecyclerView
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 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
class ConversationAdapter internal constructor(
ctx: Context,
private val parent: Fragment,
conversation: Conversation,
private val label: Label?
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
private val messageRepo = Singleton.getMessageRepository(ctx)
private var filteredMessages = conversation.messages.filter { label == null || it.labels.any { it == label } }
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)!!.also { view ->
view.setOnClickListener {
val popup = PopupMenu(itemView.context, view)
popup.menuInflater.inflate(R.menu.message, popup.menu)
popup.setOnMenuItemClickListener {
item?.let { item ->
when (it.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
}
popup.show()
}
}
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
linksClickable = true
setTextIsSelectable(true)
}
val labelAdapter = LabelAdapter(itemView.context, emptySet<Label>())
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
adapter = labelAdapter
layoutManager = GridLayoutManager(itemView.context, 2)
}
}
}

View File

@ -0,0 +1,60 @@
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.support.annotation.ColorInt
import android.support.v7.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
import org.jetbrains.anko.backgroundColor
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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
itemView.backgroundTintList = ColorStateList.valueOf(color)
} else {
itemView.backgroundColor = color
}
}
}
}

View File

@ -0,0 +1,275 @@
/*
* 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.content.Context
import android.graphics.Typeface
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.MultiIdenticon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
import java.util.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
class SwipeableConversationAdapter(ctx: Context) :
RecyclerView.Adapter<SwipeableConversationAdapter.ViewHolder>(),
SwipeableItemAdapter<SwipeableConversationAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Conversation>()
var eventListener: EventListener? = null
private val itemViewOnClickListener: View.OnClickListener
private val swipeableViewContainerOnClickListener: View.OnClickListener
private var label: Label? = null
private var selectedPosition = -1
private var activateOnItemClick: Boolean = false
private val labelUnknown = ctx.getString(R.string.unknown)
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
}
interface EventListener {
fun onItemDeleted(item: Conversation)
fun onItemArchived(item: Conversation)
fun onItemViewClicked(v: View?)
}
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
val count = v.findViewById<TextView>(R.id.count)!!
override fun getSwipeableContainerView() = container
}
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener =
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
fun add(item: Conversation) {
data.add(item)
notifyDataSetChanged()
}
fun addFirst(item: Conversation) {
val index = data.size
data.addFirst(item)
notifyItemInserted(index)
}
fun addAll(items: Collection<Conversation>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: Conversation) {
val index = data.indexOf(item)
data.removeAll { it.id == item.id }
notifyItemRemoved(index)
}
fun update(item: Conversation) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
private fun onItemViewClick(v: View) {
eventListener?.onItemViewClicked(v)
}
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
)
}
fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = data[position].id.leastSignificantBits
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)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
itemView.setOnClickListener(itemViewOnClickListener)
// (if the item is *not pinned*, click event comes to the container)
container.setOnClickListener(swipeableViewContainerOnClickListener)
// set data
avatar.setImageDrawable(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
}
}
}
override fun getItemCount() = data.size
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
} else {
REACTION_CAN_SWIPE_BOTH_H
}
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(
when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
}
)
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
when (result) {
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
else -> null
}
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
fun setSelectedPosition(selectedPosition: Int) {
val oldPosition = this.selectedPosition
this.selectedPosition = selectedPosition
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(
adapter: SwipeableConversationAdapter,
position: Int
) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableConversationAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemDeleted(item)
adapter?.remove(item)
}
override fun onCleanUp() {
adapter = null
}
}
private class SwipeRightResultAction internal constructor(
adapter: SwipeableConversationAdapter,
position: Int
) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableConversationAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemArchived(item)
adapter?.remove(item)
}
override fun onCleanUp() {
adapter = null
}
}
}

View File

@ -29,8 +29,9 @@ import android.widget.TextView
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.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.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
@ -48,7 +49,8 @@ import java.util.*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(),
SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Plaintext>()
var eventListener: EventListener? = null
@ -84,7 +86,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
swipeableViewContainerOnClickListener =
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
@ -134,7 +137,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
)
}
fun getItem(position: Int) = data[position]
@ -168,8 +172,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
// set data
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(Assets.getStatusDrawable(item.status))
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
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)
@ -194,16 +198,18 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
holder.itemView.setBackgroundResource(
when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
}
else -> R.drawable.bg_swipe_item_neutral
})
)
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
@ -222,7 +228,10 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
private class SwipeLeftResultAction internal constructor(
adapter: SwipeableMessageAdapter,
position: Int
) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
@ -235,7 +244,10 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
}
}
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
private class SwipeRightResultAction internal constructor(
adapter: SwipeableMessageAdapter,
position: Int
) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]

View File

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

View File

@ -19,8 +19,11 @@ 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
@ -33,14 +36,26 @@ import java.util.concurrent.Executors
* notifications should be combined.
*
*/
class MessageListener(ctx: Context) : BitmessageContext.Listener {
class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
override fun setContext(ctx: BitmessageContext) {
messageRepo = ctx.messages
conversationService = ConversationService(messageRepo)
}
private val unacknowledged = LinkedList<Plaintext>()
private 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 = Preferences.isEmulateConversations(ctx)
}
override fun receive(plaintext: Plaintext) {
pool.submit {
updateConversation(plaintext)
unacknowledged.addFirst(plaintext)
numberOfUnacknowledgedMessages++
if (unacknowledged.size > 5) {
@ -65,4 +80,17 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener {
numberOfUnacknowledgedMessages = 0
}
}
fun updateConversation(plaintext: Plaintext) {
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
conversationService.getSubject(listOf(plaintext))?.let { subject ->
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
messageRepo.save(plaintext)
}
}
}
companion object {
private var emulateConversations = false
}
}

View File

@ -19,8 +19,8 @@ package ch.dissem.apps.abit.listener
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ch.dissem.apps.abit.service.BitmessageService
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import org.jetbrains.anko.connectivityManager
@ -29,7 +29,7 @@ class WifiReceiver : BroadcastReceiver() {
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
val bmc = Singleton.getBitmessageContext(ctx)
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
ctx.startService(Intent(ctx, BitmessageService::class.java))
NetworkUtils.doStartBitmessageService(ctx)
}
}
}

View File

@ -17,8 +17,13 @@
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 android.support.annotation.ColorRes
import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.R
import org.jetbrains.anko.notificationManager
/**
@ -46,4 +51,30 @@ abstract class AbstractNotification(ctx: Context) {
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 {
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.notification
import android.content.Context
import android.support.v4.app.NotificationCompat
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Job
/**
* Ongoing notification while proof of work is in progress.
*/
class BatchNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
init {
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
notification = builder.build()
}
override val notificationId = ONGOING_NOTIFICATION_ID
fun update(job: Job): BatchNotification {
builder.setContentTitle(ctx.getString(job.description))
.setSmallIcon(job.icon)
.setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0)
notification = builder.build()
show()
return this
}
companion object {
const val ONGOING_NOTIFICATION_ID = 4
}
}

View File

@ -30,10 +30,14 @@ import ch.dissem.apps.abit.R
*/
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.error")
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))
@ -51,6 +55,6 @@ class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
override val notificationId = ERROR_NOTIFICATION_ID
companion object {
val ERROR_NOTIFICATION_ID = 4
const val ERROR_NOTIFICATION_ID = 4
}
}

View File

@ -34,18 +34,19 @@ import kotlin.concurrent.fixedRateTimer
*/
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.network")
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)
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setShowWhen(false)
.setContentIntent(pendingIntent)
.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")
@ -54,20 +55,31 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
builder.setOngoing(running)
val connections = BitmessageService.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))
info.append(
ctx.getString(
R.string.connection_info_1,
streamNumber
)
)
} else {
info.append(ctx.getString(R.string.connection_info_n,
streamNumber, nodeCount))
info.append(
ctx.getString(
R.string.connection_info_n,
streamNumber, nodeCount
)
)
}
info.append('\n')
}
@ -77,14 +89,18 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
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))
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))
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
@ -116,13 +132,15 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
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))
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 {
val NETWORK_NOTIFICATION_ID = 2
const val NETWORK_NOTIFICATION_ID = 2
}
}

View File

@ -17,6 +17,7 @@
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
@ -27,51 +28,60 @@ 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.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.bitmessage.entity.Plaintext
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_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.Drawables.toBitmap
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, CHANNEL_ID)
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(toBitmap(Identicon(plaintext.from), 192))
.setContentTitle(plaintext.from.toString())
.setContentText(plaintext.subject)
.setStyle(BigTextStyle().bigText(bigText))
.setContentInfo("Info")
.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))
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)
intent.putExtra(action, message)
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 {
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)
@ -82,19 +92,24 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
* * accessed it will be in a `synchronized(unacknowledged)
* * {}` block
*/
fun multiNotification(unacknowledged: Collection<Plaintext>, numberOfUnacknowledgedMessages: Int): NewMessageNotification {
val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
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))
.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)
sb.setSpan(
SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
.SPAN_INCLUSIVE_EXCLUSIVE
)
inboxStyle.addLine(sb)
}
}
@ -111,8 +126,7 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
companion object {
private val NEW_MESSAGE_NOTIFICATION_ID = 1
private const val NEW_MESSAGE_NOTIFICATION_ID = 1
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
private val CHANNEL_ID = "abit.message"
}
}

View File

@ -33,7 +33,7 @@ import kotlin.concurrent.fixedRateTimer
*/
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.pow")
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOngoing(true)
@ -46,6 +46,7 @@ class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
private var timer: Timer? = null
init {
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
update(0)
}
@ -67,10 +68,6 @@ class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
return this
}
companion object {
const val ONGOING_NOTIFICATION_ID = 3
}
fun start(item: ProofOfWorkService.PowItem) {
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
@ -101,4 +98,8 @@ class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
show()
}
}
companion object {
const val ONGOING_NOTIFICATION_ID = 3
}
}

View File

@ -68,15 +68,15 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
* Returns the contacts in the following order:
*
* * Subscribed addresses come first
* * Addresses with Aliases (alphabetically)
* * Addresses (alphabetically)
* * Addresses with aliases (alphabetically)
* * Addresses without aliases are omitted
*
*
* @return the ordered list of ids (address strings)
*/
fun getContactIds(): List<String> = findIds(
"private_key IS NULL OR chan = '1'",
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS IS NULL, $COLUMN_ALIAS, $COLUMN_ADDRESS"
"($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> {
@ -86,8 +86,7 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS)
val db = sql.readableDatabase
db.query(
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
orderBy
@ -106,8 +105,7 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
val db = sql.readableDatabase
db.query(
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null, null
).use { c ->
@ -154,8 +152,7 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
}
private fun exists(address: BitmessageAddress): Boolean {
val db = sql.readableDatabase
db.rawQuery(
sql.readableDatabase.rawQuery(
"SELECT COUNT(*) FROM Address WHERE address=?",
arrayOf(address.address)
).use { cursor ->
@ -165,49 +162,45 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
}
private fun update(address: BitmessageAddress) {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = getContentValues(address)
val update = db.update(TABLE_NAME, values, "address=?", arrayOf(address.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) {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = getContentValues(address)
values.put(COLUMN_ADDRESS, address.address)
values.put(COLUMN_VERSION, address.version)
values.put(COLUMN_CHAN, address.isChan)
val values = getContentValues(address).apply {
put(COLUMN_ADDRESS, address.address)
put(COLUMN_VERSION, address.version)
put(COLUMN_CHAN, address.isChan)
}
val insert = db.insert(TABLE_NAME, null, values)
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 {
val values = ContentValues()
address.alias?.let { values.put(COLUMN_ALIAS, it) }
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)
values.put(COLUMN_PUBLIC_KEY, out.toByteArray())
put(COLUMN_PUBLIC_KEY, out.toByteArray())
}
address.privateKey?.let { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
address.privateKey?.let { put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
if (address.isChan) {
values.put(COLUMN_CHAN, true)
put(COLUMN_CHAN, true)
}
values.put(COLUMN_SUBSCRIBED, address.isSubscribed)
return values
put(COLUMN_SUBSCRIBED, address.isSubscribed)
}
override fun remove(address: BitmessageAddress) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
sql.writableDatabase.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
}
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()

View File

@ -54,13 +54,13 @@ class AndroidInventory(private val sql: SqlHelper) : Inventory {
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
cache.put(stream, result)
cache[stream] = result
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"stream = $stream", null, null, null, null
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))
@ -84,10 +84,9 @@ class AndroidInventory(private val sql: SqlHelper) : Inventory {
// you will actually use after this query.
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"hash = X'$vector'", null, null, null, null
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)
@ -115,11 +114,10 @@ class AndroidInventory(private val sql: SqlHelper) : Inventory {
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
}
val db = sql.readableDatabase
val result = LinkedList<ObjectMessage>()
db.query(
TABLE_NAME, projection,
where.toString(), null, null, null, null
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))
@ -139,31 +137,29 @@ class AndroidInventory(private val sql: SqlHelper) : Inventory {
LOG.trace("Storing object {}", iv)
try {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_HASH, objectMessage.inventoryVector.hash)
values.put(COLUMN_STREAM, objectMessage.stream)
values.put(COLUMN_EXPIRES, objectMessage.expiresTime)
values.put(COLUMN_DATA, Encode.bytes(objectMessage))
values.put(COLUMN_TYPE, objectMessage.type)
values.put(COLUMN_VERSION, objectMessage.version)
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)
}
db.insertOrThrow(TABLE_NAME, null, values)
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
getCache(objectMessage.stream).put(iv, objectMessage.expiresTime)
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
val db = sql.writableDatabase
db.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
sql.writableDatabase.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
}

View File

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

View File

@ -19,7 +19,6 @@ package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.UuidUtils
@ -29,7 +28,6 @@ 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.AlreadyStoredException
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Strings.hex
@ -42,11 +40,19 @@ import java.util.*
*/
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
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)
}
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()
override fun countUnread(label: Label?) = when {
label === LABEL_ARCHIVE -> 0
@ -65,7 +71,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
).toInt()
}
override fun findConversations(label: Label?): List<UUID> {
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val where = when {
@ -76,8 +82,12 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
val result = LinkedList<UUID>()
sql.readableDatabase.query(
true,
TABLE_NAME, projection, where,
null, null, null, null, null
TABLE_NAME,
projection,
where,
null, 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))
@ -135,10 +145,24 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
// 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)
val projection = arrayOf(
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_ACK_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
)
val db = sql.readableDatabase
db.query(
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
@ -177,7 +201,8 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
labels = findLabels(id!!)
}
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
private fun findLabels(msgId: Any) =
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
override fun save(message: Plaintext) {
saveContactIfNecessary(message.from)
@ -206,23 +231,21 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
}
}
private fun getValues(message: Plaintext): ContentValues {
val values = ContentValues()
values.put(COLUMN_IV, message.inventoryVector?.hash)
values.put(COLUMN_TYPE, message.type.name)
values.put(COLUMN_SENDER, message.from.address)
values.put(COLUMN_RECIPIENT, message.to?.address)
values.put(COLUMN_DATA, Encode.bytes(message))
values.put(COLUMN_ACK_DATA, message.ackData)
values.put(COLUMN_SENT, message.sent)
values.put(COLUMN_RECEIVED, message.received)
values.put(COLUMN_STATUS, message.status.name)
values.put(COLUMN_INITIAL_HASH, message.initialHash)
values.put(COLUMN_TTL, message.ttl)
values.put(COLUMN_RETRIES, message.retries)
values.put(COLUMN_NEXT_TRY, message.nextTry)
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
return 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) {
@ -235,8 +258,40 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
}
override fun remove(message: Plaintext) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
}
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
val result = mutableListOf<Plaintext>()
val projection = arrayOf(
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_ACK_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
)
sql.readableDatabase.query(
TABLE_NAME, projection,
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
"$COLUMN_ID ASC",
"$limit"
).use { c ->
while (c.moveToNext()) {
result.add(getMessage(c))
}
}
return result
}
companion object {

View File

@ -5,7 +5,6 @@ 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.exception.ApplicationException
import ch.dissem.bitmessage.ports.NodeRegistry
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
import ch.dissem.bitmessage.utils.Collections
@ -120,19 +119,20 @@ class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
private fun insert(node: NetworkAddress) {
try {
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_STREAM, node.stream)
values.put(COLUMN_ADDRESS, node.IPv6)
values.put(COLUMN_PORT, node.port)
values.put(COLUMN_SERVICES, node.services)
values.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
}
)
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) {
@ -150,9 +150,10 @@ class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
}
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_SERVICES, node.services)
values.put(COLUMN_TIME, max(node.time, time))
val values = ContentValues().apply {
put(COLUMN_SERVICES, node.services)
put(COLUMN_TIME, max(node.time, time))
}
sql.writableDatabase.update(
TABLE_NAME,

View File

@ -48,28 +48,27 @@ class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepo
// 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)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"initial_hash=X'${hex(initialHash)}'",
null, null, null, null
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))
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)))
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)))
)
}
}
@ -82,10 +81,9 @@ class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepo
// you will actually use after this query.
val projection = arrayOf(COLUMN_INITIAL_HASH)
val db = sql.readableDatabase
val result = LinkedList<ByteArray>()
db.query(
TABLE_NAME, projection, null, null, null, null, null
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))
@ -97,20 +95,20 @@ class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepo
override fun putObject(item: ProofOfWorkRepository.Item) {
try {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
values.put(COLUMN_DATA, Encode.bytes(item.objectMessage))
values.put(COLUMN_VERSION, item.objectMessage.version)
values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
values.put(COLUMN_EXTRA_BYTES, item.extraBytes)
item.message?.let { message ->
values.put(COLUMN_EXPIRATION_TIME, item.expirationTime)
values.put(COLUMN_MESSAGE_ID, message.id as Long?)
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?)
}
}
db.insertOrThrow(TABLE_NAME, null, values)
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
@ -121,20 +119,19 @@ class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepo
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
override fun removeObject(initialHash: ByteArray) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
sql.writableDatabase.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
private val TABLE_NAME = "POW"
private val COLUMN_INITIAL_HASH = "initial_hash"
private val COLUMN_DATA = "data"
private val COLUMN_VERSION = "version"
private val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
private val COLUMN_EXTRA_BYTES = "extra_bytes"
private val COLUMN_EXPIRATION_TIME = "expiration_time"
private val COLUMN_MESSAGE_ID = "message_id"
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"
}
}

View File

@ -32,42 +32,42 @@ class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME,
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)
}
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
"Message", arrayOf("id"),
"conversation IS NULL", null, null, null, null
).use { c ->
while (c.moveToNext()) {
val id = c.getLong(0)
@ -76,8 +76,9 @@ class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME,
}
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
val values = ContentValues(1)
values.put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
val values = ContentValues(1).apply {
put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
}
db.update("Message", values, "id=?", arrayOf(id.toString()))
}
@ -89,7 +90,7 @@ class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME,
companion object {
// If you change the database schema, you must increment the database version.
private val DATABASE_VERSION = 7
val DATABASE_NAME = "jabit.db"
private const val DATABASE_VERSION = 7
const val DATABASE_NAME = "jabit.db"
}
}

View File

@ -0,0 +1,117 @@
package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.support.annotation.DrawableRes
import android.support.annotation.StringRes
import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.notification.BatchNotification
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
import org.jetbrains.anko.doAsync
import java.util.*
class BatchProcessorService : Service() {
private lateinit var notification: BatchNotification
override fun onCreate() {
notification = BatchNotification(this)
}
override fun onBind(intent: Intent) = BatchBinder(this)
class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() {
private val notification = service.notification
fun process(job: Job) = synchronized(queue) {
ContextCompat.startForegroundService(
service,
Intent(service, BatchProcessorService::class.java)
)
service.startForeground(
ONGOING_NOTIFICATION_ID,
notification.notification
)
if (!working) {
working = true
service.processQueue(job)
} else {
queue.add(job)
}
}
}
private fun processQueue(job: Job) {
doAsync {
var next: Job? = job
while (next != null) {
next.process(notification)
synchronized(queue) {
next = queue.poll()
if (next == null) {
working = false
stopForeground(true)
stopSelf()
}
}
}
}
}
companion object {
private var working = false
private val queue = LinkedList<Job>()
}
}
interface Job {
val icon: Int
@DrawableRes get
val description: Int
@StringRes get
val numberOfItems: Int
var numberOfProcessedItems: Int
/**
* Runs the job. This shouldn't happen in a separate thread, as this is handled by the service.
*/
fun process(notification: BatchNotification)
}
data class SimpleJob<T>(
override val numberOfItems: Int,
/**
* Provides the next batch of items, given the last item of the previous batch,
* or null for the first batch.
*/
private val provider: (T?) -> List<T>,
/**
* Processes an item.
*/
private val processor: (T) -> Unit,
override val icon: Int,
override val description: Int
) : Job {
override var numberOfProcessedItems: Int = 0
override fun process(notification: BatchNotification) {
notification.update(this)
var batch = provider.invoke(null)
while (batch.isNotEmpty()) {
Thread.yield()
batch.forEach {
processor.invoke(it)
Thread.yield()
}
numberOfProcessedItems += batch.size
notification.update(this)
batch = provider.invoke(batch.last())
}
}
}

View File

@ -22,12 +22,14 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.BatteryManager
import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.connectivityManager
import org.jetbrains.anko.doAsync
/**
* Define a Service that returns an IBinder for the
@ -39,9 +41,9 @@ class BitmessageService : Service() {
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
private lateinit var notification: NetworkNotification
private val connectivityReceiver: BroadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (bmc.isRunning() && connectivityManager.isActiveNetworkMetered){
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (bmc.isRunning() && !Preferences.isConnectionAllowed(this@BitmessageService)) {
bmc.shutdown()
}
}
@ -58,7 +60,13 @@ class BitmessageService : Service() {
}
override fun onCreate() {
registerReceiver(connectivityReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
registerReceiver(
connectivityReceiver,
IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
)
notification = NetworkNotification(this)
running = false
}
@ -84,14 +92,18 @@ class BitmessageService : Service() {
running = false
notification.showShutdown()
cleanupHandler.removeCallbacks(cleanupTask)
bmc.cleanup()
doAsync {
bmc.cleanup()
}
unregisterReceiver(connectivityReceiver)
stopSelf()
}
override fun onBind(intent: Intent) = null
companion object {
@Volatile private var running = false
@Volatile
private var running = false
val isRunning: Boolean
get() = running && Singleton.bitmessageContext?.isRunning() == true

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
import ch.dissem.apps.abit.util.PowStats
@ -44,9 +44,14 @@ class ProofOfWorkService : Service() {
private val notification = service.notification
fun process(item: PowItem) = synchronized(queue) {
service.startService(Intent(service, ProofOfWorkService::class.java))
service.startForeground(ONGOING_NOTIFICATION_ID,
notification.notification)
ContextCompat.startForegroundService(
service,
Intent(service, ProofOfWorkService::class.java)
)
service.startForeground(
ONGOING_NOTIFICATION_ID,
notification.notification
)
if (!calculating) {
calculating = true
service.calculateNonce(item)
@ -58,7 +63,11 @@ class ProofOfWorkService : Service() {
}
data class PowItem(val initialHash: ByteArray, val targetValue: ByteArray, val callback: ProofOfWorkEngine.Callback) {
data class PowItem(
val initialHash: ByteArray,
val targetValue: ByteArray,
val callback: ProofOfWorkEngine.Callback
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -81,29 +90,32 @@ class ProofOfWorkService : Service() {
private fun calculateNonce(item: PowItem) {
notification.start(item)
val startTime = System.currentTimeMillis()
engine.calculateNonce(item.initialHash, item.targetValue, object : ProofOfWorkEngine.Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
notification.finished()
val time = System.currentTimeMillis() - startTime
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
try {
item.callback.onNonceCalculated(initialHash, nonce)
} finally {
var next: PowItem? = null
synchronized(queue) {
next = queue.poll()
if (next == null) {
calculating = false
stopForeground(true)
stopSelf()
} else {
notification.update(queue.size).show()
engine.calculateNonce(
item.initialHash,
item.targetValue,
object : ProofOfWorkEngine.Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
notification.finished()
val time = System.currentTimeMillis() - startTime
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
try {
item.callback.onNonceCalculated(initialHash, nonce)
} finally {
var next: PowItem? = null
synchronized(queue) {
next = queue.poll()
if (next == null) {
calculating = false
stopForeground(true)
stopSelf()
} else {
notification.update(queue.size).show()
}
}
next?.let { calculateNonce(it) }
}
next?.let { calculateNonce(it) }
}
}
})
})
}
companion object {

View File

@ -32,6 +32,7 @@ import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.factory.BufferPool
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService
@ -50,36 +51,42 @@ object Singleton {
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
val labeler = DefaultLabeler().apply {
listener = { message, added, removed ->
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
currentLabel.value?.let { label ->
when {
label.type == Label.Type.TRASH
&& added.all { it.type == Label.Type.TRASH }
&& removed.any { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from trash
swipeableMessageAdapter.remove(message)
}
label.type == Label.Type.UNREAD
&& added.all { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message)
}
added.contains(label) -> {
// in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in
swipeableMessageAdapter.addFirst(message)
}
removed.contains(label) -> {
swipeableMessageAdapter.remove(message)
}
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
swipeableMessageAdapter.update(message)
MainActivity.apply {
runOnUiThread {
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
currentLabel.value?.let { label ->
when {
label.type == Label.Type.TRASH
&& added.all { it.type == Label.Type.TRASH }
&& removed.any { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from trash
swipeableMessageAdapter.remove(message)
}
label.type == Label.Type.UNREAD
&& added.all { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message)
}
label == AndroidLabelRepository.LABEL_ARCHIVE && !added.isEmpty() -> {
// work-around for messages in archive, which isn't an actual label but an absence of labels
swipeableMessageAdapter.remove(message)
}
added.contains(label) -> {
// in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in
swipeableMessageAdapter.addFirst(message)
}
removed.contains(label) -> {
swipeableMessageAdapter.remove(message)
}
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
swipeableMessageAdapter.update(message)
}
}
}
}
}
}
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
MainActivity.apply {
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
updateUnread()
}
}
@ -95,6 +102,7 @@ object Singleton {
fun getBitmessageContext(context: Context): BitmessageContext =
init({ bitmessageContext }, { bitmessageContext = it }) {
BufferPool.setLimit(4)
BitmessageContext.build {
TTL.pubkey = 2 * DAY
val ctx = context.applicationContext
@ -111,7 +119,7 @@ object Singleton {
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler()
networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx)
labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false

View File

@ -2,9 +2,9 @@ package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Intent
import android.os.Build
import android.support.annotation.RequiresApi
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
/**
@ -19,16 +19,14 @@ class StartupNodeOnWifiService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val bmc = Singleton.getBitmessageContext(this)
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
applicationContext.startService(Intent(this, BitmessageService::class.java))
NetworkUtils.doStartBitmessageService(applicationContext)
}
return true
}
override fun onStopJob(params: JobParameters?) = if (Preferences.isWifiOnly(this)) {
// Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
// depending on Android version.
Preferences.isFullNodeActive(this)
} else {
false
}
/**
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
* depending on Android version.
*/
override fun onStopJob(params: JobParameters?) = Preferences.isFullNodeActive(this)
}

View File

@ -18,7 +18,6 @@ package ch.dissem.apps.abit.synchronization
import android.app.Service
import android.content.Intent
import android.os.IBinder
/**
* Define a Service that returns an IBinder for the

View File

@ -43,28 +43,25 @@ object Assets {
} catch (e: IOException) {
throw RuntimeException(e)
}
}
@DrawableRes
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> 0
Plaintext.Status.DRAFT -> R.drawable.draft
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
Plaintext.Status.SENT -> R.drawable.sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
else -> 0
}
@StringRes
fun getStatusString(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> R.string.status_received
Plaintext.Status.DRAFT -> R.string.status_draft
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
Plaintext.Status.SENT -> R.string.status_sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
else -> 0
}
}
fun Plaintext.Status.getDrawable() = when (this) {
Plaintext.Status.RECEIVED -> 0
Plaintext.Status.DRAFT -> R.drawable.draft
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
Plaintext.Status.SENT -> R.drawable.sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
else -> 0
}
fun Plaintext.Status.getString() = when (this) {
Plaintext.Status.RECEIVED -> R.string.status_received
Plaintext.Status.DRAFT -> R.string.status_draft
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
Plaintext.Status.SENT -> R.string.status_sent
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
else -> 0
}

View File

@ -23,6 +23,8 @@ import java.util.regex.Pattern
*/
object Constants {
const val PREFERENCE_WIFI_ONLY = "wifi_only"
const val PREFERENCE_REQUIRE_CHARGING = "require_charging"
const val PREFERENCE_EMULATE_CONVERSATIONS = "emulate_conversations"
const val PREFERENCE_TRUSTED_NODE = "trusted_node"
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout"
const val PREFERENCE_SERVER_POW = "server_pow"

View File

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

View File

@ -1,31 +0,0 @@
package ch.dissem.apps.abit.util
import android.support.annotation.DrawableRes
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
/**
* Utilities to work with the common floating action button in the main activity
*/
object FabUtils {
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
fab.removeAllOnMenuItemClickListeners()
fab.show()
fab.closeMenu()
val mainFab = fab.mainFab
mainFab.setImageResource(drawableRes)
fab.setMenu(menu)
fab.addOnStateChangeListener { isOpened: Boolean ->
if (isOpened) {
// It will be turned 45 degrees, which makes an x out of the +
mainFab.setImageResource(R.drawable.ic_action_add)
} else {
mainFab.setImageResource(drawableRes)
}
}
return fab
}
}

View File

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

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
import ch.dissem.apps.abit.service.BitmessageService
@ -18,30 +19,41 @@ object NetworkUtils {
fun enableNode(ctx: Context, ask: Boolean = true) {
Preferences.setFullNodeActive(ctx, true)
if (Preferences.isWifiOnly(ctx)) {
if (Preferences.isConnectionAllowed(ctx)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scheduleNodeStart(ctx)
} else {
ctx.startService(Intent(ctx, BitmessageService::class.java))
MainActivity.updateNodeSwitch()
}
} else if (ask) {
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
ctx.startActivity(dialogIntent)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Preferences.isConnectionAllowed(ctx) || !ask) {
scheduleNodeStart(ctx)
} else {
askForConnection(ctx)
}
} else {
ctx.startService(Intent(ctx, BitmessageService::class.java))
MainActivity.updateNodeSwitch()
if (Preferences.isWifiOnly(ctx)) {
if (Preferences.isConnectionAllowed(ctx)) {
doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch()
} else if (ask) {
askForConnection(ctx)
}
} else {
doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch()
}
}
}
private fun askForConnection(ctx: Context) {
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
ctx.startActivity(dialogIntent)
}
fun doStartBitmessageService(ctx: Context) {
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
}
fun disableNode(ctx: Context) {
Preferences.setFullNodeActive(ctx, false)
ctx.stopService(Intent(ctx, BitmessageService::class.java))
@ -49,11 +61,17 @@ object NetworkUtils {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun scheduleNodeStart(ctx: Context) {
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
val builder = JobInfo.Builder(0, serviceComponent)
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
if (Preferences.isWifiOnly(ctx)) {
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
}
if (Preferences.requireCharging(ctx)) {
builder.setRequiresCharging(true)
}
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
builder.setPersisted(true)
jobScheduler.schedule(builder.build())
}
}

View File

@ -25,7 +25,7 @@ class Observable<T>(value: T) {
* To prevent memory leaks, the observer must be removed if it isn't used anymore.
*/
fun addObserver(key: Any, observer: (T) -> Unit) {
observers.put(key, observer)
observers[key] = observer
}
/**

View File

@ -39,12 +39,13 @@ import java.security.Security;
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
*/
@SuppressWarnings("WeakerAccess")
public final class PRNGFixes {
private static final int VERSION_CODE_JELLY_BEAN = 16;
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
getBuildFingerprintAndDeviceSerial();
getBuildFingerprintAndDeviceSerial();
/**
* Hidden constructor to prevent instantiation.
@ -70,7 +71,7 @@ public final class PRNGFixes {
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
@ -78,18 +79,18 @@ public final class PRNGFixes {
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class)
.invoke(null, (Object) generateSeed());
.getMethod("RAND_seed", byte[].class)
.invoke(null, (Object) generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
int bytesRead = (Integer) Class.forName(
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) {
throw new IOException(
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
}
} catch (Exception e) {
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
@ -104,7 +105,7 @@ public final class PRNGFixes {
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void installLinuxPRNGSecureRandom()
throws SecurityException {
throws SecurityException {
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
// No need to apply the fix
return;
@ -113,11 +114,11 @@ public final class PRNGFixes {
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders =
Security.getProviders("SecureRandom.SHA1PRNG");
Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
@ -126,10 +127,10 @@ public final class PRNGFixes {
// by the Linux PRNG-based SecureRandom implementation.
SecureRandom rng1 = new SecureRandom();
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng1.getProvider().getClass())) {
rng1.getProvider().getClass())) {
throw new SecurityException(
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
}
SecureRandom rng2;
@ -139,10 +140,10 @@ public final class PRNGFixes {
throw new SecurityException("SHA1PRNG not available", e);
}
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng2.getProvider().getClass())) {
rng2.getProvider().getClass())) {
throw new SecurityException(
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
}
}
@ -152,11 +153,11 @@ public final class PRNGFixes {
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
public LinuxPRNGSecureRandomProvider() {
LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG",
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need to
// prevent them from getting the default implementation whose output
@ -225,7 +226,7 @@ public final class PRNGFixes {
// On a small fraction of devices /dev/urandom is not writable.
// Log and ignore.
Log.w(PRNGFixes.class.getSimpleName(),
"Failed to mix seed into " + URANDOM_FILE);
"Failed to mix seed into " + URANDOM_FILE);
} finally {
mSeeded = true;
}
@ -249,7 +250,7 @@ public final class PRNGFixes {
}
} catch (IOException e) {
throw new SecurityException(
"Failed to read from " + URANDOM_FILE, e);
"Failed to read from " + URANDOM_FILE, e);
}
}
@ -269,10 +270,10 @@ public final class PRNGFixes {
// output being pulled into this process prematurely.
try {
sUrandomIn = new DataInputStream(
new FileInputStream(URANDOM_FILE));
new FileInputStream(URANDOM_FILE));
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for reading", e);
+ URANDOM_FILE + " for reading", e);
}
}
return sUrandomIn;
@ -297,7 +298,7 @@ public final class PRNGFixes {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(Process.myPid());
@ -341,4 +342,4 @@ public final class PRNGFixes {
throw new RuntimeException("UTF-8 encoding not supported");
}
}
}
}

View File

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

View File

@ -17,19 +17,27 @@
package ch.dissem.apps.abit.util
import android.content.Context
import android.preference.PreferenceManager
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.notification.ErrorNotification
import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS
import ch.dissem.apps.abit.util.Constants.PREFERENCE_FULL_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager
import org.jetbrains.anko.defaultSharedPreferences
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
import java.net.InetAddress
import android.os.BatteryManager
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
/**
* @author Christian Basler
@ -70,57 +78,63 @@ object Preferences {
return Integer.parseInt(portString)
} catch (e: NumberFormatException) {
ErrorNotification(ctx)
.setError(R.string.error_invalid_sync_port, portString)
.show()
.setError(R.string.error_invalid_sync_port, portString)
.show()
}
}
return 8444
}
fun getTimeoutInSeconds(ctx: Context): Long {
val preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT) ?: return 120
return preference.toLong()
fun getTimeoutInSeconds(ctx: Context): Long = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT)?.toLong() ?: 120
private fun getPreference(ctx: Context, name: String): String? = ctx.defaultSharedPreferences.getString(name, null)
fun isConnectionAllowed(ctx: Context) = isAllowedForWiFi(ctx) && isAllowedForCharging(ctx)
private fun isAllowedForWiFi(ctx: Context) = !isWifiOnly(ctx) || !ctx.connectivityManager.isActiveNetworkMetered
private fun isAllowedForCharging(ctx: Context) = !requireCharging(ctx) || isCharging(ctx)
private fun isCharging(ctx: Context) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ctx.batteryManager.isCharging
} else {
val intent = ctx.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
}
private fun getPreference(ctx: Context, name: String): String? {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getString(name, null)
}
fun isConnectionAllowed(ctx: Context) = !isWifiOnly(ctx) || !ctx.connectivityManager.isActiveNetworkMetered
fun isWifiOnly(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
}
fun isWifiOnly(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
fun setWifiOnly(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply()
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_WIFI_ONLY, status)
.apply()
}
fun isFullNodeActive(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_FULL_NODE, false)
fun requireCharging(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
fun setRequireCharging(ctx: Context, status: Boolean) {
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_REQUIRE_CHARGING, status)
.apply()
}
fun isEmulateConversations(ctx: Context) =
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
fun isFullNodeActive(ctx: Context) =
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_FULL_NODE, false)
fun setFullNodeActive(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply()
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_FULL_NODE, status)
.apply()
}
fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports")
fun requestAcknowledgements(ctx: Context): Boolean {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
}
fun setRequestAcknowledgements(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
preferences.edit().putBoolean(PREFERENCE_REQUEST_ACK, status).apply()
}
fun requestAcknowledgements(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
fun cleanupExportDirectory(ctx: Context) {
val exportDirectory = getExportDirectory(ctx)

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_active_state"/>
</item>
</layer-list>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_state"/>
</item>
</layer-list>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,11 +37,11 @@
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingBottom="0dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="0dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
tools:text="Name" />
@ -53,9 +53,9 @@
android:layout_alignBottom="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="marquee"
android:lines="1"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="BM-2cW0000000000000000000000000000000" />

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_margin="4dp"
android:src="@color/colorAccent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingBottom="0dp"
android:paddingEnd="4dp"
android:paddingStart="4dp"
android:paddingTop="0dp"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Name" />
</RelativeLayout>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_swipe_item_neutral">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_item_normal_state"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
tools:ignore="UselessParent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_margin="16dp"
android:src="@color/colorPrimaryDark"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/avatar"
android:layout_marginTop="-5dp"
android:layout_toEndOf="@id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingBottom="0dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="0dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
tools:text="Sender" />
<TextView
android:id="@+id/subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/count"
android:layout_below="@id/sender"
android:layout_toEndOf="@id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Subject" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/subject"
android:layout_toEndOf="@id/avatar"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="1"
android:paddingBottom="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Text" />
<ImageView
android:id="@+id/status"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_alignEnd="@id/avatar"
android:layout_marginBottom="-8dp"
android:layout_marginEnd="-8dp"
android:tint="@color/colorAccent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_notification_proof_of_work" />
<TextView
android:id="@+id/count"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignBottom="@id/subject"
android:layout_alignParentEnd="true"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/md_blue_grey_500"
tools:text="0" />
</RelativeLayout>
</FrameLayout>
</FrameLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -15,8 +14,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -34,13 +32,13 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_constraintLeft_creator="1"
tools:layout_constraintTop_creator="1"/>
tools:layout_constraintTop_creator="1" />
<android.support.design.widget.TextInputLayout
android:id="@+id/label_wrapper"
android:layout_marginTop="24dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/description">
@ -48,7 +46,8 @@
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label"/>
android:hint="@string/label"
android:inputType="text" />
</android.support.design.widget.TextInputLayout>
@ -64,7 +63,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase"
android:inputType="textMultiLine"/>
android:inputType="textMultiLine" />
</android.support.design.widget.TextInputLayout>
@ -82,7 +81,8 @@
android:ems="10"
android:hint="@string/number_of_identities"
android:inputType="number"
android:text="1"/>
android:text="1"
tools:ignore="HardcodedText" />
</android.support.design.widget.TextInputLayout>
@ -92,7 +92,7 @@
android:layout_height="wrap_content"
android:text="@string/shorter"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/number_of_identities_wrapper"/>
app:layout_constraintTop_toBottomOf="@id/number_of_identities_wrapper" />
<Button
android:id="@+id/ok"
@ -103,7 +103,7 @@
android:text="@string/ok"
android:textColor="@color/colorAccent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/shorter"/>
app:layout_constraintTop_toBottomOf="@id/shorter" />
<Button
android:id="@+id/dismiss"
@ -113,6 +113,6 @@
android:text="@string/cancel"
android:textColor="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/ok"
app:layout_constraintRight_toLeftOf="@id/ok"/>
app:layout_constraintRight_toLeftOf="@id/ok" />
</android.support.constraint.ConstraintLayout>

View File

@ -50,14 +50,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="SIMPLE"/>
android:text="@string/encoding_simple"/>
<RadioButton
android:id="@+id/extended"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="EXTENDED"/>
android:text="@string/encoding_extended"/>
</RadioGroup>

View File

@ -1,46 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp">
<AutoCompleteTextView
android:id="@+id/recipient_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/to"
android:inputType="textNoSuggestions"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/subject_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/subject"
android:inputType="textEmailSubject"
android:textAppearance="?android:attr/textAppearanceLarge"/>
</android.support.design.widget.TextInputLayout>
<EditText
android:id="@+id/body_input"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="start|top"
android:hint="@string/compose_body_hint"
android:inputType="textMultiLine|textCapSentences"
android:scrollbars="vertical"/>
android:orientation="vertical">
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@id/sender_input"
android:padding="4dp"
android:text="@string/from"
android:textColor="#9b9b9b"
android:textSize="12sp" />
<Spinner
android:id="@+id/sender_input"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp">
<AutoCompleteTextView
android:id="@+id/recipient_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/to"
android:inputType="textNoSuggestions"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/subject_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/subject"
android:inputType="textEmailSubject"
android:textAppearance="?android:attr/textAppearanceLarge" />
</android.support.design.widget.TextInputLayout>
<EditText
android:id="@+id/body_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start|top"
android:hint="@string/compose_body_hint"
android:inputType="textMultiLine|textCapSentences"
android:scrollbars="none"
tools:ignore="InefficientWeight" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<TextView
android:id="@+id/subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toStartOf="@+id/avatar"
android:elegantTextHeight="false"
android:enabled="false"
android:gravity="center_vertical"
android:padding="16dp"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="UnusedAttribute"
tools:text="Subject" />
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_margin="10dp"
android:src="@color/colorAccent"
tools:ignore="ContentDescription" />
<View
android:id="@+id/divider"
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_below="@id/subject"
android:background="@color/divider" />
<android.support.v7.widget.RecyclerView
android:id="@+id/messages"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/divider"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
tools:listitem="@layout/item_message_detail" />
</RelativeLayout>

View File

@ -2,27 +2,32 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_label"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:padding="2dp">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/icon"
android:layout_margin="1dp"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
app:ico_color="@android:color/black"
app:ico_icon="cmd-label" />
app:iiv_color="@color/colorPrimaryDarkText"
app:iiv_icon="cmd-label" />
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="24dp"
tools:text="Label"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/icon" />
android:layout_toEndOf="@+id/icon"
android:paddingEnd="24dp"
android:paddingStart="8dp"
android:textColor="@color/colorPrimaryDarkText"
tools:text="Label" />
</RelativeLayout>

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:fitsSystemWindows="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginTop="8dp"
android:src="@color/colorAccent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/sender"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toStartOf="@+id/status"
android:gravity="center_vertical"
android:paddingEnd="0dp"
android:paddingStart="8dp"
android:textStyle="bold"
tools:text="Sender" />
<TextView
android:id="@+id/recipient"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_alignBottom="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toStartOf="@+id/status"
android:gravity="center_vertical"
android:paddingEnd="0dp"
android:paddingStart="8dp"
tools:text="Recipient" />
<ImageView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/menu"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:tint="@color/colorAccent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_notification_proof_of_work" />
<ImageView
android:id="@+id/menu"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@string/context_menu"
android:padding="8dp"
android:src="@drawable/ic_menu" />
</RelativeLayout>
<LinearLayout
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:textIsSelectable="true"
tools:text="Message Body" />
<android.support.v7.widget.RecyclerView
android:id="@+id/labels"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="fill_parent"
android:layout_height="2dip"
android:background="@color/divider" />
</LinearLayout>

View File

@ -26,6 +26,7 @@
android:layout_height="match_parent"
android:background="@drawable/bg_item_normal_state"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
tools:ignore="UselessParent">

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -16,9 +15,9 @@
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="64dp">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="64dp">
<CheckBox
android:id="@+id/checkbox"
@ -31,21 +30,22 @@
android:paddingEnd="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Name"/>
tools:text="Name" />
<TextView
android:id="@+id/address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:ellipsize="marquee"
android:lines="1"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingStart="48dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="BM-2cW0000000000000000000000000000000"/>
tools:text="BM-2cW0000000000000000000000000000000" />
</RelativeLayout>

View File

@ -70,7 +70,7 @@
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
app:ico_color="@android:color/black"
app:ico_icon="cmd-rss"/>
app:iiv_color="@android:color/black"
app:iiv_icon="cmd-rss"/>
</RelativeLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/delete"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete"/>
<item
android:id="@+id/archive"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_action_archive"
android:title="@string/archive"/>
</menu>

View File

@ -18,7 +18,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/open_file"
android:title="@string/open_file"
android:icon="@drawable/ic_action_open_file"
app:showAsAction="ifRoom"/>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,135 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools"><string name="app_name">Abit</string>
<string name="about_app">عميل Bitmessage أندرويد</string>
<string name="title_message_detail">رسالة</string>
<string name="title_subscription_detail">اشتراك</string>
<string name="title_chan_detail">قناة</string>
<string name="title_identity_detail">هوية</string>
<string name="title_contact_detail">جهة إتصال</string>
<string name="bitmessage_full_node">عقدة Bitmessage</string>
<string name="settings">إعدادات</string>
<string name="wifi_only">Wi-Fi فقط</string>
<string name="wifi_only_summary">منع الإتصال من خلال بيانات الهاتف</string>
<string name="to">إلى</string>
<string name="subject">الموضوع</string>
<string name="manage_identity">إدارة الهويات</string>
<string name="add_identity">إضافة هوية</string>
<string name="add_identity_summary">إنشاء هوية جديدة</string>
<string name="create_identity_description">إنشاء هوية عشوائية جديدة</string>
<string name="import_identity_description">استيراد هوية موجودة من PyBitmessage او من تصدير</string>
<string name="add_deterministic_address">هوية حتمية</string>
<string name="add_deterministic_address_description">إنشاء أو إعادة إنشاء هوية حتمية</string>
<string name="add_chan">إضافة قناة</string>
<string name="add_chan_description">إنشاء أو الإنضمام إلى قناة</string>
<string name="title_activity_open_bitmessage_link">إستيراد جهة اتصال</string>
<string name="subscribe">إشتراك</string>
<string name="do_import">استيراد</string>
<string name="cancel">إلغاء</string>
<string name="n_new_messages" tools:ignore="PluralsCandidate">رسائل جديدة</string>
<string name="reply">رد</string>
<string name="delete">حذف</string>
<string name="empty_trash">أفرغ المهملات</string>
<string name="trusted_node">عقدة موثوقة</string>
<string name="trusted_node_summary">استخدام العقدة في التزامن</string>
<string name="write_message">كتابة رسالة</string>
<string name="full_node">عقدة كاملة</string>
<string name="send">إرسال</string>
<string name="connection_info_disconnected">غير متصل</string>
<string name="connection_info_pending">يتصل…</string>
<string name="proof_of_work_title">إثبات العمل</string>
<string name="proof_of_work_text_0">جاري إثبات العمل لإرسال الرسالة</string>
<string name="inbox">صندوق الوارد</string>
<string name="draft">مسودات</string>
<string name="sent">المرسل</string>
<string name="unread">غير مقروء</string>
<string name="trash">مُهملات</string>
<string name="label">تسمية</string>
<string name="connection_info_1">بث #%1$d : توصيل واحد</string>
<string name="connection_info_n" tools:ignore="PluralsCandidate">"بث #%1$d: %2$d توصيلات"</string>
<string name="broadcast">إذاعة</string>
<string name="mark_unread">حدد كمقروء</string>
<string name="archive">أرشيف</string>
<string name="stream_number">بث #%d</string>
<string name="sync_timeout">انتهاء مهلة التزامن</string>
<string name="sync_timeout_summary">مهلة الإتصال بالثواني</string>
<string name="proof_of_work_text_n" tools:ignore="PluralsCandidate">جاري إثبات العمل لإرسال الرسالة (%1$d في قائمة الانتظار)</string>
<string name="error_invalid_sync_port">إعدادات منفذ التزامن غير صالحة</string>
<string name="compose_body_hint">كتابة رسالة</string>
<string name="contacts_and_subscriptions">جهات اتصال</string>
<string name="subscribed">تم الاشتراك</string>
<string name="server_pow">خادم إثبات العمل</string>
<string name="server_pow_summary">عقدة موثوقة تقوم بإثبات العمل</string>
<string name="full_node_warning">تشغيل عقدة Bitmessage كاملة يستهلك الكثير من البيانات، مما قد يكون مكلفًا لبيانات الهاتف، هل أنت متأكد أنك تريد تشغيل عقدة كاملة؟</string>
<string name="about">عن Abit</string>
<string name="about_summary">التبعيات مفتوحة المصدر.</string>
<string name="title_activity_status">تصحيح الأخطاء</string>
<string name="status">تصحيح الأخطاء</string>
<string name="status_summary">معلومات تقنية</string>
<string name="alias_default_identity">أنا</string>
<string name="pubkey_available">مفتاح التشفير المعلن متاح</string>
<string name="pubkey_not_available">مفتاح التشفير المعلن غير متاح بعد</string>
<string name="alt_qr_code">شفرة QR</string>
<string name="add_identity_warning">حيازة عدة هويات يحتاج إلى المزيد من الموارد، إذا كنت متأكدًأ أنك تريد إضافة هويات، أختر تمامًا ما تريد عمله:</string>
<string name="share">نشر</string>
<string name="delete_identity_warning">هل أنت متأكد من أنك تريد حذف هذه الهوية؟ لن تستطيع استقبال أي رسائل مرسلة لهذا العنوان ولا يمكنك التراجع عن هذه العملية.</string>
<string name="delete_contact_warning">هل أنت متأكد أنك تريد حذف جهة الاتصال هذه؟</string>
<string name="scan_qr_code">مسح شفرة QR</string>
<string name="create_contact">إنشاء جهة إتصال</string>
<string name="full_node_description">لا يمكنك إرسال أو استقبال رسائل إلا إذا قمت بتشغيل عقدة كاملة. رجاء العلم أن ذلك يستخدم الكثير من الموارد وبيانات الإنترنت. يمكنك تعيين عقدة موثوقة في الإعدادات، ويمكنك إطلاق عقدتك الخاصة.</string>
<string name="address">عنوان Bitmessage</string>
<string name="error_illegal_address">قد يكون هناك خطأ بالكتابة</string>
<string name="export">تصدير</string>
<string name="confirm_export">هل أنت متأكد أنك تريد تصدير هويتك؟ سيحتوي التصدير على مفاتيح خاصة غير مشفرة.</string>
<string name="compose_message">إنشاء</string>
<string name="passphrase">جملة المرور</string>
<string name="help_out">ادعم التطوير</string>
<string name="help_out_summary">هل تريد المساعدة؟ ألق نظرة هنا</string>
<string name="help_out_link">https://dissem.github.io/Abit/helping-out</string>
<string name="toast_long_running_operation">قد يستغرق بضع دقائق</string>
<string name="toast_identity_created">تم إنشاء الهوية</string>
<string name="toast_identities_created">تم إنشاء الهويات</string>
<string name="toast_chan_created">تم إنشاء القناة</string>
<string name="deterministic_address_warning">تأكد من تذكر هذه الإعدادات جيدًا عندما تعيد إنشاء الهوية الختمية.</string>
<string name="number_of_identities">عدد الهويات المنشئة</string>
<string name="shorter">ابحث عن عنوان أقصر</string>
<string name="wif_string">محتويات keys.dat</string>
<string name="next">استمرار</string>
<string name="title_import_identity">استيراد هوية</string>
<string name="open_file">فتح ملف</string>
<string name="error_loading_data">خطأ في تحميل البيانات</string>
<string name="select_file_title">اختيار ملف</string>
<string name="select_identities_to_import">اختر الهويات التي تريد استيرادها:</string>
<string name="import_input_description">يمكنك لصق محتويات تصدير او ملف keys.dat</string>
<string name="full_node_stop">إيقاف العقدة</string>
<string name="full_node_restart">اعادة تشغيل العقدة</string>
<string name="use_mobile_network">استخدم شبكة بيانات الهاتف</string>
<string name="personal_message">رسالة</string>
<string name="no_identity_warning">أعد المحاولة عند وجود هوية متاحة.</string>
<string name="status_public_key">تم طلب المفتاح المعلن</string>
<string name="status_sent_acknowledged">تم تسليم الرسالة</string>
<string name="status_draft">مسودة</string>
<string name="status_sent">تم الارسال</string>
<string name="status_received">تم الاستقبال</string>
<string name="error_unsupported_encoding">ترميز غير مدعوم، استخدم الترميز البسيط.</string>
<string name="select_encoding_warning">الترميز الموسع هو نظام رسالة جديد غير منتشر الدعم بعد، لكن يحتوي مميزات مختلفة. للبقاء على النظام الأكثر انتشارًا، اختر الترميز البسيط.</string>
<string name="select_encoding_title">اختر ترميز الرسالة</string>
<string name="cleanup">تطهير</string>
<string name="cleanup_summary">حذف المدخلات المنتهية</string>
<string name="cleanup_notification_start">تم بدأ التنظيف</string>
<string name="cleanup_notification_end">تم الانتهاء من التنظيف</string>
<string name="wait_for_wifi">انتظر Wi-Fi</string>
<string name="error_msg_recipient_missing">حدد المرسل إليه</string>
<string name="export_data">تصدير</string>
<string name="export_data_summary">تصدير جميع الرسائل وجهات الاتصال (الهويات غير مضمنة)</string>
<string name="import_data">استيراد</string>
<string name="import_data_summary">استيراد الرسائل وجهات الاتصال (الهويات غير مضمنة)</string>
<string name="request_acknowledgements">اطلب التبليغ بوصول الرسائل</string>
<string name="request_acknowledgements_summary">التبليغ يسمح بالتأكد من وصول الرسالة، لكن يحتاج المزيد من الوقت</string>
<string name="got_it">فهمت</string>
<string name="select_encoding">اختيار الترميز</string>
<string name="from">من</string>
<string name="invalid_wif_file">لم يتم معالجة الملف</string>
<string name="outbox">صندوق المرسل</string>
<string name="broadcasts">الإذاعات</string>
</resources>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<string name="inbox">Posteingang</string>
<string name="draft">Entwürfe</string>
<string name="sent">Gesendet</string>
<string name="unread">Ungelesen</string>
<string name="trash">Papierkorb</string>
<string name="broadcasts">Broadcasts</string>
</resources>

View File

@ -104,15 +104,14 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
<string name="title_chan_detail">Chan</string>
<string name="title_contact_detail">Kontakt</string>
<string name="title_identity_detail">Identität</string>
<string name="outbox">Postausgang</string>
<string name="status_draft">Entwurf</string>
<string name="status_public_key">öffentlicher Schlüssel angefordert</string>
<string name="status_received">empfangen</string>
<string name="status_sent">gesendet</string>
<string name="status_sent_acknowledged">Empfang bestätigt</string>
<string name="error_unsupported_encoding">Codierung %s wird nicht unterstützt, es wird SIMPLE verwendet.</string>
<string name="error_unsupported_encoding">Codierung %s wird nicht unterstützt, es wird einfach verwendet.</string>
<string name="select_encoding_title">Kodierung auswählen</string>
<string name="select_encoding_warning">EXTENDED ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle SIMPLE um sicher zu gehen.</string>
<string name="select_encoding_warning">Erweitert ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle einfach um sicher zu gehen.</string>
<string name="cleanup">Aufräumen</string>
<string name="cleanup_notification_start">Aufräumarbeiten gestartet</string>
<string name="cleanup_notification_end">Aufräumarbeiten beendet</string>
@ -127,4 +126,17 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
<string name="import_data">Import</string>
<string name="import_data_summary">Nachrichten und Kontakte importieren (aber keine Identitäten)</string>
<string name="select_encoding">Kodierung auswählen</string>
<string name="from">Von</string>
<string name="invalid_wif_file">Die Datei kann nicht verarbeitet werden</string>
<string name="inbox">Posteingang</string>
<string name="outbox">Postausgang</string>
<string name="draft">Entwürfe</string>
<string name="sent">Gesendet</string>
<string name="unread">Ungelesen</string>
<string name="trash">Papierkorb</string>
<string name="broadcasts">Broadcasts</string>
<string name="encoding_simple">einfach</string>
<string name="encoding_extended">erweitert</string>
<string name="emulate_conversations">Konversation erraten</string>
<string name="emulate_conversations_summary">Benutze Betreff um zu erraten welche Nachrichten zusammengehören. Die Reihenfolge stimmt häufig nicht.</string>
</resources>

View File

@ -0,0 +1,137 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools"><string name="app_name">Abit</string>
<string name="about_app">Un client Bitmessage pour Android</string>
<string name="title_message_detail">Message</string>
<string name="title_subscription_detail">Abonnement</string>
<string name="title_chan_detail">Canal</string>
<string name="title_identity_detail">Identité</string>
<string name="title_contact_detail">Contact</string>
<string name="bitmessage_full_node">Nœud de bitmessage</string>
<string name="settings">Paramètres</string>
<string name="wifi_only">Seulement Wi-Fi</string>
<string name="wifi_only_summary">Ne vous connectez pas au réseau mobile</string>
<string name="to">A</string>
<string name="subject">Objet</string>
<string name="manage_identity">Gérer l\'identité</string>
<string name="add_identity">Ajouter une identité</string>
<string name="add_identity_summary">Créer une nouvelle identité</string>
<string name="create_identity_description">Créer une nouvelle identité aléatoire</string>
<string name="import_identity_description">Importation d\'une identité existante de PyBitmessage ou d\'une exportation</string>
<string name="add_deterministic_address">Identité déterministe</string>
<string name="add_deterministic_address_description">Créer ou recréer une identité déterministe</string>
<string name="add_chan">Ajouter un canal</string>
<string name="add_chan_description">Créer ou rejoindre un canal</string>
<string name="title_activity_open_bitmessage_link">Importation de contact</string>
<string name="connection_info_1">"Flux nº%1$d: une connexion"</string>
<string name="connection_info_n" tools:ignore="PluralsCandidate">"Flux nº%1$d: %2$d connexions"</string>
<string name="label">Étiquette</string>
<string name="subscribe">Inscrivez-vous</string>
<string name="do_import">Importation</string>
<string name="cancel">Annuler</string>
<string name="broadcast">Diffusion</string>
<string name="n_new_messages" tools:ignore="PluralsCandidate">%d messages nouveaux</string>
<string name="reply">Répondre</string>
<string name="delete">Supprimer</string>
<string name="mark_unread">Marquer non lu</string>
<string name="archive">Archive</string>
<string name="stream_number">"Flux nº%d"</string>
<string name="trusted_node">Nœud de confiance</string>
<string name="trusted_node_summary">Cette adresse est utilisée pour la synchronisation</string>
<string name="sync_timeout">Limitation du temps de synchronisation</string>
<string name="write_message">Écrire un message</string>
<string name="full_node">Nœud actif</string>
<string name="send">Transmission</string>
<string name="connection_info_disconnected">Déconnecté</string>
<string name="connection_info_pending">La connexion est établie…</string>
<string name="proof_of_work_title">Preuve de travail</string>
<string name="proof_of_work_text_0">Faire du travail pour envoyer la message</string>
<string name="proof_of_work_text_n" tools:ignore="PluralsCandidate">Faire du travail pour envoyer la message (%1$d en file d\'attente)</string>
<string name="error_invalid_sync_port">Port non valide dans les paramètres de synchronisation : %s</string>
<string name="compose_body_hint">Écrire un message</string>
<string name="contacts_and_subscriptions">Contacts</string>
<string name="subscribed">Souscrit</string>
<string name="full_node_warning">L\'exécution d\'un nœud Bitmessage actif consomme beaucoup de trafic, ce qui peut coûter cher sur un réseau mobile. Êtes-vous sûr de vouloir démarrer un nœud actif ?</string>
<string name="about">À propos d\'Abit</string>
<string name="about_summary">Les dépendances Open Source.</string>
<string name="title_activity_status">Déboguage</string>
<string name="status">Déboguage</string>
<string name="status_summary">Informations techniques</string>
<string name="alias_default_identity">Moi</string>
<string name="pubkey_available">Clé publique disponible</string>
<string name="pubkey_not_available">Clé publique pas encore disponible</string>
<string name="alt_qr_code">Code QR</string>
<string name="add_identity_warning">Avoir plus d\'identités exigera plus de ressources. Si vous êtes sûr de vouloir ajouter une identité, veuillez choisir la procédure :</string>
<string name="delete_identity_warning">Êtes-vous sûr de vouloir effacer cette identité ? Les messages envoyés à cette adresse ne peuvent plus être reçus et il n\'est pas possible d\'annuler cette action.</string>
<string name="delete_contact_warning">Êtes-vous sûr de vouloir supprimer ce contact?</string>
<string name="scan_qr_code">Scanner code QR</string>
<string name="create_contact">Créer un contact</string>
<string name="full_node_description">Vous ne pouvez pas recevoir ou envoyer de messages sans lancer un nœud actif. Mais sachez que cela utilise beaucoup de ressources et de trafic internet. Comme alternative, vous pouvez configurer un nœud de confiance dans les paramètres, mais à partir de maintenant vous devrez déployer votre propre nœud.</string>
<string name="address">Adresse Bitmessage</string>
<string name="error_illegal_address">C\'est peut-être une faute de frappe</string>
<string name="export">Exportation</string>
<string name="confirm_export">Voulez-vous vraiment exporter votre identité ? L\'exportation contiendra les clés privées non chiffrées.</string>
<string name="passphrase">Phrase de passe</string>
<string name="help_out">Soutenir le développement</string>
<string name="help_out_summary">Apprenez comment soutenir le développement d\'Abit</string>
<string name="toast_long_running_operation">Cela peut prendre quelques minutes</string>
<string name="toast_identity_created">Identité créée</string>
<string name="toast_identities_created">Les identités ont été créées</string>
<string name="toast_chan_created">Canal créé</string>
<string name="deterministic_address_warning">N\'oubliez pas ces paramètres et assurez-vous qu\'ils sont corrects lorsque vous recréez une adresse déterministe.</string>
<string name="number_of_identities">Nombre d\'identités à créer</string>
<string name="shorter">Recherche d\'adresses plus courtes</string>
<string name="wif_string">WIF / contenu de keys.dat</string>
<string name="next">Continuer</string>
<string name="title_import_identity">Importation d\'identité</string>
<string name="open_file">Ouvrir un fichier</string>
<string name="error_loading_data">Erreur lors du chargement des données</string>
<string name="select_file_title">Sélectionnez un fichier</string>
<string name="select_identities_to_import">Veuillez sélectionner les identités à importer :</string>
<string name="import_input_description">Vous pouvez simplement insérer le contenu d\'une exportation ou d\'un fichier \'keys.dat\'</string>
<string name="full_node_stop">arrêter le nœud</string>
<string name="full_node_restart">redémarrer le nœud</string>
<string name="use_mobile_network">Utiliser le réseau mobile</string>
<string name="personal_message">Message</string>
<string name="empty_trash">Vider les ordures</string>
<string name="sync_timeout_summary">Délai d\'expiration en secondes</string>
<string name="server_pow">POW sur le serveur</string>
<string name="server_pow_summary">Le nœud de confiance fait la preuve de travail</string>
<string name="share">Partager</string>
<string name="compose_message">Composer</string>
<string name="no_identity_warning">Veuillez réessayer dès qu\'une identité est disponible.</string>
<string name="status_public_key">clé publique demandée</string>
<string name="status_sent_acknowledged">Réception confirmée</string>
<string name="status_draft">brouillon</string>
<string name="status_sent">envoyé</string>
<string name="status_received">reçu</string>
<string name="error_unsupported_encoding">Encodage non supporté \'%s\', \'simple\' est utilisé.</string>
<string name="select_encoding_warning">\'Étendu\' est un nouveau format de message qui ne fonctionne pas encore partout, mais qui a des avantages intéressants. Sélectionnez \'simple\' pour être sûr.</string>
<string name="select_encoding_title">Sélectionner le codage des messages</string>
<string name="cleanup">Nettoyage</string>
<string name="cleanup_summary">Supprimer les entrées d\'inventaire obsolètes</string>
<string name="cleanup_notification_start">Démarrage du nettoyage</string>
<string name="cleanup_notification_end">Nettoyage terminé</string>
<string name="wait_for_wifi">Attendez la connexion Wi-Fi</string>
<string name="error_msg_recipient_missing">Veuillez indiquer un destinataire</string>
<string name="export_data">Exportation</string>
<string name="export_data_summary">Exporter tous les messages et contacts (mais pas les identités)</string>
<string name="import_data">Importation</string>
<string name="import_data_summary">Importer des messages et des contacts (mais pas des identités)</string>
<string name="request_acknowledgements">Demande d\'accusés de réception</string>
<string name="request_acknowledgements_summary">Les accusés de réception permettent de s\'assurer qu\'un message a bien été reçu, mais il faut plus de temps pour l\'envoyer</string>
<string name="got_it">J\'ai compris</string>
<string name="select_encoding">Choix du codage</string>
<string name="help_out_link">https://dissem.github.io/Abit/aider</string>
<string name="from">De</string>
<string name="invalid_wif_file">Le fichier ne peut pas être traité</string>
<string name="inbox">Boîte de réception</string>
<string name="outbox">Boîte d\'envoi</string>
<string name="draft">Brouillons</string>
<string name="sent">Envoyé</string>
<string name="unread">Non lues</string>
<string name="trash">Corbeille</string>
<string name="broadcasts">Diffusions</string>
<string name="encoding_simple">simple</string>
<string name="encoding_extended">étendu</string>
</resources>

View File

@ -15,6 +15,7 @@
-->
<resources>
<dimen name="action_bar_offset">66dp</dimen>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<string name="inbox">Inbox</string>
<string name="draft">Drafts</string>
<string name="sent">Sent</string>
<string name="unread">Unread</string>
<string name="trash">Trash</string>
<string name="broadcasts">Broadcasts</string>
</resources>

View File

@ -108,9 +108,8 @@ As an alternative you could configure a trusted node in the settings, but as of
<string name="status_draft">draft</string>
<string name="status_sent">sent</string>
<string name="status_received">received</string>
<string name="outbox">Outbox</string>
<string name="error_unsupported_encoding">Unsupported encoding %s, using SIMPLE instead.</string>
<string name="select_encoding_warning">EXTENDED is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select SIMPLE.</string>
<string name="error_unsupported_encoding">Unsupported encoding %s, using simple instead.</string>
<string name="select_encoding_warning">Extended is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select simple.</string>
<string name="select_encoding_title">Select Message Encoding</string>
<string name="cleanup">Cleanup</string>
<string name="cleanup_summary">Remove outdated inventory entries</string>
@ -126,4 +125,32 @@ As an alternative you could configure a trusted node in the settings, but as of
<string name="request_acknowledgements_summary">Acknowledges allow making sure a message was received, but require additional time to send</string>
<string name="got_it">Got it</string>
<string name="select_encoding">Select encoding</string>
<string name="from">From</string>
<string name="invalid_wif_file">The file could not be processed</string>
<string name="inbox">Inbox</string>
<string name="outbox">Outbox</string>
<string name="draft">Drafts</string>
<string name="sent">Sent</string>
<string name="unread">Unread</string>
<string name="trash">Trash</string>
<string name="broadcasts">Broadcasts</string>
<string name="encoding_simple">simple</string>
<string name="encoding_extended">extended</string>
<string name="context_menu">actions</string>
<string name="emulate_conversations">Guess conversations</string>
<string name="emulate_conversations_summary">Use subject to determine which messages belong together. The order will likely be wrong.</string>
<string name="emulate_conversations_initialize">Group existing messages by subject</string>
<string name="emulate_conversations_batch">Grouping existing messages by subject</string>
<string name="preference_group_user_experience">Behaviour</string>
<string name="preference_group_user_experience_summary">Change how messages are displayed</string>
<string name="preference_group_network_and_performance">Network &amp; Performance</string>
<string name="preference_group_network_and_performance_summary">Tweak network usage and protocol details</string>
<string name="preference_group_advanced">Advanced</string>
<string name="preference_group_advanced_summary"></string>
<string name="preference_group_experimental">Experimental</string>
<string name="preference_group_experimental_summary">Only change if you know what you\'re doing</string>
<string name="require_charging">Require charging</string>
<string name="require_charging_summary">Only connect when device is plugged in</string>
<string name="unknown">Unknown</string>
<string name="ok">OK</string>
</resources>

View File

@ -5,19 +5,7 @@
<item name="android:activatedBackgroundIndicator">@drawable/bg_item_activated</item>
<item name="android:textColor">@color/colorPrimaryText</item>
<item name="android:textColorSecondary">@color/colorSecondaryText</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>
<style name="CustomShowcaseTheme" parent="ShowcaseView">
<item name="sv_backgroundColor">#eeffc107</item>
<item name="sv_showcaseColor">#ffc107</item>
<item name="sv_buttonText">Hide</item>
<item name="sv_tintButtonColor">false</item>
<item name="sv_titleTextAppearance">@style/CustomTitle</item>
</style>
<style name="CustomTitle" parent="TextAppearance.ShowcaseView.Title">
<item name="android:textColor">@color/colorAccent</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<files-path name="exports" path="exports/"/>
</paths>

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