Compare commits
80 Commits
1.0-beta18
...
develop
Author | SHA1 | Date | |
---|---|---|---|
6878f80a54 | |||
a01f116065 | |||
c6e29c056b | |||
8b89d81970 | |||
76317a2488 | |||
85f114a33d | |||
9e7f247763 | |||
ec4615b639 | |||
2ddd78dfe2 | |||
90bb538692 | |||
9cc07f73ae | |||
0b432b6a67 | |||
725ec60fd4 | |||
60c4a4d8a0 | |||
6585876b25 | |||
b1fd9d9ef9 | |||
e05d27bfbc | |||
be7a7f1af6 | |||
61e579c0d4 | |||
eee1be873a | |||
4c213d3e9c | |||
76cb5df998 | |||
3026ae8505 | |||
412180f443 | |||
78f9621afa | |||
85562efc0d | |||
a89f80f400 | |||
1426b786e8 | |||
6a311a0346 | |||
9b75a8c2ef | |||
4e5ba4401a | |||
f374748f71 | |||
49e77199b0 | |||
46e5bb7ece | |||
8004865e01 | |||
101913a531 | |||
40f8bc87a2 | |||
0d1cfff883 | |||
d7b7b11cdf | |||
d8d5f70b37 | |||
06e99ea0cd | |||
ad3af929d7 | |||
16f1dfa6f6 | |||
2bddd0f256 | |||
ab70e6df12 | |||
fb72356467 | |||
e3c7c4d557 | |||
938bfc206e | |||
b4b1d25f99 | |||
3509082d30 | |||
9e59187ae0 | |||
c1af65732a | |||
61e0a12a50 | |||
c254c1bacd | |||
4f1ef4407c | |||
46bbd59712 | |||
8d876719c4 | |||
ec645d70ce | |||
7f0d8828d1 | |||
d98b800249 | |||
c750e2004a | |||
9eefbad7d6 | |||
21abdbd720 | |||
39ad5e8baf | |||
1da0674857 | |||
f481914a65 | |||
99b2d1903e | |||
d611bd13bc | |||
66c8536a84 | |||
72213a53a5 | |||
8a668f4af2 | |||
6829614da0 | |||
1906a2e13c | |||
708529fc0a | |||
b5dbbeb46a | |||
b368c8251d | |||
6986d9a2df | |||
35249a0145 | |||
2a1aa736cc | |||
9f26ade617 |
46
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abit@dissem.ch. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/4/
|
16
CONTRIBUTING.md
Normal file
@ -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
@ -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.
|
@ -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
|
||||
minSdkVersion 21
|
||||
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 {
|
||||
|
@ -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,45 @@
|
||||
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">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".service.StartServiceReceiver" android:enabled="@bool/is_post_api_21">
|
||||
<receiver
|
||||
android:name=".service.StartServiceReceiver">
|
||||
<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 +196,7 @@
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity"/>
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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?) {
|
||||
|
@ -43,6 +43,7 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.content) == null) {
|
||||
// Display the fragment as the main content.
|
||||
val fragment = ComposeMessageFragment()
|
||||
fragment.arguments = intent.extras
|
||||
@ -51,8 +52,10 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
.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"),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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,12 +57,25 @@ 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
|
||||
retainInstance = true
|
||||
arguments?.apply {
|
||||
val draft = getSerializable(EXTRA_DRAFT) as Plaintext?
|
||||
if (draft != null) {
|
||||
this@ComposeMessageFragment.draft = draft
|
||||
identity = draft.from
|
||||
recipient = draft.to
|
||||
subject = draft.subject ?: ""
|
||||
content = draft.text ?: ""
|
||||
encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE
|
||||
parents.addAll(draft.parents)
|
||||
} else {
|
||||
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
|
||||
if (context != null && (id == null || id.privateKey == null)) {
|
||||
id = Singleton.getIdentity(context!!)
|
||||
}
|
||||
@ -66,46 +84,69 @@ class ComposeMessageFragment : Fragment() {
|
||||
} 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
|
||||
broadcast = getBoolean(EXTRA_BROADCAST, false)
|
||||
if (containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
|
||||
}
|
||||
if (arguments.containsKey(EXTRA_SUBJECT)) {
|
||||
subject = arguments.getString(EXTRA_SUBJECT)
|
||||
if (containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getString(EXTRA_SUBJECT)
|
||||
}
|
||||
if (arguments.containsKey(EXTRA_CONTENT)) {
|
||||
content = arguments.getString(EXTRA_CONTENT)
|
||||
if (containsKey(EXTRA_CONTENT)) {
|
||||
content = getString(EXTRA_CONTENT)
|
||||
}
|
||||
encoding = arguments.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
|
||||
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)
|
||||
|
||||
context?.let { ctx ->
|
||||
val identities = Singleton.getAddressRepository(ctx).getIdentities()
|
||||
sender_input.adapter = ContactAdapter(ctx, identities, true)
|
||||
val index = identities.indexOf(Singleton.getIdentity(ctx))
|
||||
if (index >= 0) {
|
||||
sender_input.setSelection(index)
|
||||
}
|
||||
|
||||
if (broadcast) {
|
||||
recipient_input.visibility = View.GONE
|
||||
} else {
|
||||
val adapter = ContactAdapter(context!!)
|
||||
val adapter = ContactAdapter(
|
||||
ctx,
|
||||
Singleton.getAddressRepository(ctx).getContacts()
|
||||
)
|
||||
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_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
|
||||
override fun onNothingSelected(parent: AdapterView<*>) =
|
||||
Unit // leave current selection
|
||||
}
|
||||
recipient?.let { recipient_input.setText(it.toString()) }
|
||||
}
|
||||
@ -121,6 +162,7 @@ class ComposeMessageFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.compose, menu)
|
||||
@ -146,18 +188,17 @@ class ComposeMessageFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
|
||||
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,14 +216,11 @@ 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)
|
||||
}
|
||||
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
sender?.let { builder.from(it) }
|
||||
if (!Preferences.requestAcknowledgements(ctx)) {
|
||||
builder.preventAck()
|
||||
}
|
||||
@ -192,11 +230,14 @@ class ComposeMessageFragment : Fragment() {
|
||||
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(
|
||||
@ -210,7 +251,42 @@ class ComposeMessageFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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,20 +68,24 @@ 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
|
||||
@ -77,7 +95,7 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -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,7 +39,11 @@ class ImportIdentitiesFragment : Fragment() {
|
||||
private lateinit var adapter: AddressSelectorAdapter
|
||||
private lateinit var importer: WifImporter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -48,17 +52,29 @@ class ImportIdentitiesFragment : Fragment() {
|
||||
val wifData = arguments.getString(WIF_DATA)
|
||||
val bmc = Singleton.getBitmessageContext(activity)
|
||||
|
||||
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,
|
||||
val layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false)
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,31 +151,36 @@ 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)
|
||||
.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()
|
||||
}
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
SyncAdapter.startSync(this)
|
||||
}
|
||||
|
||||
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
|
||||
@ -192,16 +202,20 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
|
||||
private fun createDrawer(toolbar: Toolbar) {
|
||||
val profiles = ArrayList<IProfile<*>>()
|
||||
profiles.add(ProfileSettingDrawerItem()
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.withIcon(
|
||||
IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons))
|
||||
.colorRes(R.color.icons)
|
||||
)
|
||||
.withIdentifier(ADD_IDENTITY.toLong())
|
||||
)
|
||||
profiles.add(ProfileSettingDrawerItem()
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY.toLong())
|
||||
@ -212,25 +226,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()
|
||||
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()
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts))
|
||||
drawerItems.add(PrimaryDrawerItem()
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts)
|
||||
)
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
)
|
||||
|
||||
nodeSwitch = SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
@ -281,6 +306,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,9 +332,15 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
val tag = item.tag
|
||||
if (tag is Label) {
|
||||
currentLabel.value = tag
|
||||
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
|
||||
if (itemList !is ConversationListFragment) {
|
||||
changeList(ConversationListFragment())
|
||||
}
|
||||
} else {
|
||||
if (itemList !is MessageListFragment) {
|
||||
changeList(MessageListFragment())
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (item is Nameable<*>) {
|
||||
when (item.name.textRes) {
|
||||
@ -343,7 +375,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 +400,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 +412,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,11 +452,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
} else {
|
||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
||||
}
|
||||
runOnUiThread {
|
||||
drawer.updateItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from [ListSelectionListener]
|
||||
@ -434,14 +469,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 +508,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 +546,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
|
||||
|
||||
|
@ -4,6 +4,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
|
||||
/**
|
||||
@ -33,9 +34,15 @@ class MessageDetailActivity : DetailActivity() {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// 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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 -> {
|
||||
|
@ -17,42 +17,63 @@
|
||||
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.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 +94,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,30 +179,20 @@ 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) {
|
||||
when (key) {
|
||||
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
|
||||
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) {
|
||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
||||
val ctx = context ?: throw IllegalStateException("No context available")
|
||||
if (node != null) {
|
||||
SyncAdapter.startSync(ctx)
|
||||
} else {
|
||||
SyncAdapter.stopSync(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
|
||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
||||
if (node != null) {
|
||||
@ -193,6 +205,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 (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
|
||||
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import ch.dissem.apps.abit.util.PRNGFixes
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidCryptography : SpongyCryptography() {
|
||||
init {
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -88,7 +95,9 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
} else {
|
||||
val words = alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val words =
|
||||
alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
|
||||
for (word in words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
@ -105,6 +114,13 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
if (newValues.isEmpty()) {
|
||||
try {
|
||||
newValues.add(BitmessageAddress(prefix.toString()))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues
|
||||
results.count = newValues.size
|
||||
} else {
|
||||
@ -125,4 +141,6 @@ class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
|
||||
}
|
||||
|
@ -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())
|
||||
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
|
||||
adapter = labelAdapter
|
||||
layoutManager = GridLayoutManager(itemView.context, 2)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import 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
|
||||
|
||||
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
var labels = labels.toList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.icon(label.getIcon())
|
||||
viewHolder.label.text = label.getText(ctx)
|
||||
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
|
||||
fun setBackground(@ColorInt color: Int) {
|
||||
itemView.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,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
|
||||
}
|
||||
}
|
||||
}
|
@ -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,7 +198,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
|
||||
holder.itemView.setBackgroundResource(when (type) {
|
||||
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) {
|
||||
@ -203,7 +208,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
R.drawable.bg_swipe_item_right
|
||||
}
|
||||
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]
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
@ -37,9 +36,7 @@ class FullNodeDialogActivity : Activity() {
|
||||
finish()
|
||||
}
|
||||
dismiss.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
NetworkUtils.scheduleNodeStart(applicationContext)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.service.BitmessageService
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import org.jetbrains.anko.connectivityManager
|
||||
|
||||
class WifiReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
|
||||
ctx.startService(Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +34,15 @@ 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)
|
||||
.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
|
||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
@ -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,
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT))
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
} else {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true)
|
||||
builder.addAction(R.drawable.ic_notification_node_start,
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_start,
|
||||
ctx.getString(R.string.full_node_restart),
|
||||
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT))
|
||||
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,
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT))
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NETWORK_NOTIFICATION_ID = 2
|
||||
const val NETWORK_NOTIFICATION_ID = 2
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
.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,8 +92,11 @@ 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))
|
||||
@ -93,8 +106,10 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -54,11 +54,11 @@ 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(
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = $stream", null, null, null, null
|
||||
).use { c ->
|
||||
@ -84,8 +84,7 @@ 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(
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'$vector'", null, null, null, null
|
||||
).use { c ->
|
||||
@ -115,9 +114,8 @@ 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(
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(), null, null, null, null
|
||||
).use { c ->
|
||||
@ -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 } }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,12 +40,20 @@ import java.util.*
|
||||
*/
|
||||
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
|
||||
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
|
||||
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
|
||||
label == null -> DatabaseUtils.queryNumEntries(
|
||||
@ -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 {
|
||||
|
@ -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,12 +119,12 @@ 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,
|
||||
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
|
||||
@ -133,6 +132,7 @@ class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
|
||||
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,
|
||||
|
@ -48,8 +48,7 @@ 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(
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'${hex(initialHash)}'",
|
||||
null, null, null, null
|
||||
@ -82,9 +81,8 @@ 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(
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection, null, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
@ -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)
|
||||
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 ->
|
||||
values.put(COLUMN_EXPIRATION_TIME, item.expirationTime)
|
||||
values.put(COLUMN_MESSAGE_ID, message.id as Long?)
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -25,9 +25,10 @@ import android.net.ConnectivityManager
|
||||
import android.os.Handler
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
import org.jetbrains.anko.connectivityManager
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
@ -39,9 +40,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 +59,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 +91,18 @@ class BitmessageService : Service() {
|
||||
running = false
|
||||
notification.showShutdown()
|
||||
cleanupHandler.removeCallbacks(cleanupTask)
|
||||
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
|
||||
|
@ -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,7 +90,10 @@ class ProofOfWorkService : Service() {
|
||||
private fun calculateNonce(item: PowItem) {
|
||||
notification.start(item)
|
||||
val startTime = System.currentTimeMillis()
|
||||
engine.calculateNonce(item.initialHash, item.targetValue, object : ProofOfWorkEngine.Callback {
|
||||
engine.calculateNonce(
|
||||
item.initialHash,
|
||||
item.targetValue,
|
||||
object : ProofOfWorkEngine.Callback {
|
||||
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
|
||||
notification.finished()
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
|
@ -20,7 +20,6 @@ import android.content.Context
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.adapter.AndroidCryptography
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
|
||||
import ch.dissem.apps.abit.listener.MessageListener
|
||||
@ -29,9 +28,11 @@ import ch.dissem.apps.abit.repository.*
|
||||
import ch.dissem.apps.abit.util.Constants
|
||||
import ch.dissem.apps.abit.util.Observable
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.factory.BufferPool
|
||||
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
|
||||
import ch.dissem.bitmessage.ports.DefaultLabeler
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
@ -50,6 +51,8 @@ object Singleton {
|
||||
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
|
||||
val labeler = DefaultLabeler().apply {
|
||||
listener = { message, added, removed ->
|
||||
MainActivity.apply {
|
||||
runOnUiThread {
|
||||
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
|
||||
currentLabel.value?.let { label ->
|
||||
when {
|
||||
@ -64,6 +67,10 @@ object Singleton {
|
||||
// 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
|
||||
@ -78,8 +85,8 @@ object Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
|
||||
MainActivity.apply {
|
||||
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
|
||||
@ -104,14 +112,14 @@ object Singleton {
|
||||
ServerPowEngine(ctx),
|
||||
ServicePowEngine(ctx)
|
||||
)
|
||||
cryptography = AndroidCryptography()
|
||||
cryptography = SpongyCryptography()
|
||||
nodeRegistry = AndroidNodeRegistry(sqlHelper)
|
||||
inventory = AndroidInventory(sqlHelper)
|
||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
@ -14,21 +14,20 @@ import ch.dissem.apps.abit.util.Preferences
|
||||
*
|
||||
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class StartupNodeOnWifiService : JobService() {
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
val bmc = Singleton.getBitmessageContext(this)
|
||||
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
|
||||
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)
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
|
||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
|
||||
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
|
||||
@ -75,8 +76,8 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
|
||||
}
|
||||
val trustedNode = Preferences.getTrustedNode(context)
|
||||
if (trustedNode == null) {
|
||||
LOG.info("Trusted node not available, disabling synchronization")
|
||||
stopSync(context)
|
||||
// As Abit tends to get killed by the system, let's leverage the sync mechanism to start it again:
|
||||
NetworkUtils.scheduleNodeStart(context)
|
||||
return
|
||||
}
|
||||
LOG.info("Synchronization started")
|
||||
@ -104,7 +105,8 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
|
||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||
LOG.info("Looking for completed POW")
|
||||
|
||||
val privateKey = identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
|
||||
val privateKey =
|
||||
identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
|
||||
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
|
||||
val reader = ProofOfWorkRequest.Reader(identity)
|
||||
val powRepo = Singleton.getProofOfWorkRepository(context)
|
||||
@ -113,7 +115,8 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
|
||||
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
|
||||
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
|
||||
val cryptoMsg = CryptoCustomMessage(
|
||||
ProofOfWorkRequest(identity, initialHash, CALCULATE, target))
|
||||
ProofOfWorkRequest(identity, initialHash, CALCULATE, target)
|
||||
)
|
||||
cryptoMsg.signAndEncrypt(identity, signingKey)
|
||||
val response = bmc.send(
|
||||
trustedNode,
|
||||
|
@ -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
|
||||
|
@ -17,8 +17,6 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.DrawableRes
|
||||
import android.support.annotation.StringRes
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import java.io.IOException
|
||||
@ -43,11 +41,10 @@ object Assets {
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
|
||||
fun Plaintext.Status.getDrawable() = when (this) {
|
||||
Plaintext.Status.RECEIVED -> 0
|
||||
Plaintext.Status.DRAFT -> R.drawable.draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
|
||||
@ -55,10 +52,9 @@ object Assets {
|
||||
Plaintext.Status.SENT -> R.drawable.sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
fun getStatusString(status: Plaintext.Status) = when (status) {
|
||||
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
|
||||
@ -66,5 +62,4 @@ object Assets {
|
||||
Plaintext.Status.SENT -> R.string.status_sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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,47 +43,48 @@ 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 {
|
||||
fun Drawable.toBitmap(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)
|
||||
setBounds(0, 0, canvas.width, canvas.height)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
||||
fun qrCode(address: BitmessageAddress?): Bitmap? {
|
||||
if (address == null) {
|
||||
return null
|
||||
}
|
||||
fun BitmessageAddress.qrCode(): Bitmap? {
|
||||
val link = StringBuilder()
|
||||
link.append(Constants.BITMESSAGE_URL_SCHEMA)
|
||||
link.append(address.address)
|
||||
if (address.alias != null) {
|
||||
link.append("?label=").append(address.alias)
|
||||
link.append(address)
|
||||
if (alias != null) {
|
||||
link.append("?label=").append(alias)
|
||||
}
|
||||
address.pubkey?.apply {
|
||||
link.append(if (address.alias == null) '?' else '&')
|
||||
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))
|
||||
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)
|
||||
result = MultiFormatWriter().encode(
|
||||
link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
|
||||
)
|
||||
} catch (e: WriterException) {
|
||||
LOG.error(e.message, e)
|
||||
Drawables.LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
|
||||
@ -98,5 +100,4 @@ object Drawables {
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.support.annotation.DrawableRes
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
|
||||
/**
|
||||
* Utilities to work with the common floating action button in the main activity
|
||||
*/
|
||||
object FabUtils {
|
||||
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
return fab
|
||||
}
|
||||
}
|
@ -2,21 +2,19 @@ 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) {
|
||||
fun Label.getText(ctx: Context): String = type?.getText(toString(), ctx) ?: toString()
|
||||
|
||||
fun Label.Type.getText(alternative: String?, ctx: Context) = when (this) {
|
||||
Label.Type.INBOX -> ctx.getString(R.string.inbox)
|
||||
Label.Type.DRAFT -> ctx.getString(R.string.draft)
|
||||
Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
|
||||
@ -25,9 +23,9 @@ object Labels {
|
||||
Label.Type.TRASH -> ctx.getString(R.string.trash)
|
||||
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
|
||||
else -> alternative
|
||||
}
|
||||
}
|
||||
|
||||
fun getIcon(label: Label): IIcon = when (label.type) {
|
||||
fun Label.getIcon(): IIcon = when (type) {
|
||||
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
|
||||
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
|
||||
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
|
||||
@ -36,10 +34,9 @@ object Labels {
|
||||
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
|
||||
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
|
||||
else -> CommunityMaterial.Icon.cmd_label
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getColor(label: Label) = if (label.type == null) {
|
||||
label.color
|
||||
} else 0xFF000000.toInt()
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
|
||||
color
|
||||
} else default
|
||||
|
@ -6,9 +6,7 @@ import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import android.support.v4.content.ContextCompat
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
|
||||
import ch.dissem.apps.abit.service.BitmessageService
|
||||
import ch.dissem.apps.abit.service.StartupNodeOnWifiService
|
||||
@ -18,28 +16,22 @@ 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) {
|
||||
|
||||
if (Preferences.isConnectionAllowed(ctx) || !ask) {
|
||||
scheduleNodeStart(ctx)
|
||||
} else {
|
||||
ctx.startService(Intent(ctx, BitmessageService::class.java))
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
} else if (ask) {
|
||||
// Ask for connection
|
||||
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
|
||||
if (ctx !is Activity) {
|
||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
}
|
||||
ctx.startActivity(dialogIntent)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scheduleNodeStart(ctx)
|
||||
}
|
||||
} else {
|
||||
ctx.startService(Intent(ctx, BitmessageService::class.java))
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
|
||||
fun doStartBitmessageService(ctx: Context) {
|
||||
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
|
||||
fun disableNode(ctx: Context) {
|
||||
@ -47,13 +39,18 @@ object NetworkUtils {
|
||||
ctx.stopService(Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
|
||||
@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)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,344 +0,0 @@
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
* <p/>
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*
|
||||
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
|
||||
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final int VERSION_CODE_JELLY_BEAN = 16;
|
||||
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/**
|
||||
* Hidden constructor to prevent instantiation.
|
||||
*/
|
||||
private PRNGFixes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, (Object) generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
public LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
@SuppressWarnings("JavaDoc")
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// On a small fraction of devices /dev/urandom is not writable.
|
||||
// Log and ignore.
|
||||
Log.w(PRNGFixes.class.getSimpleName(),
|
||||
"Failed to mix seed into " + URANDOM_FILE);
|
||||
} finally {
|
||||
mSeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() throws IOException {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ object PowStats {
|
||||
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
@ -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
|
||||
@ -77,50 +85,56 @@ object Preferences {
|
||||
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)
|
||||
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_trash"/>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_trash"
|
||||
android:gravity="right|center_vertical"
|
||||
android:right="16dp"/>
|
||||
</layer-list>
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_archive"/>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_archive"
|
||||
android:gravity="left|center_vertical"
|
||||
android:left="16dp"/>
|
||||
</layer-list>
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#ffffff">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:drawable="@android:color/white"/>
|
||||
|
||||
</ripple>
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_item_swiping_active_state"/>
|
||||
</item>
|
||||
</layer-list>
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_item_swiping_state"/>
|
||||
</item>
|
||||
</layer-list>
|
6
app/src/main/res/drawable/bg_label.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#000000"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
@ -18,9 +18,8 @@
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_trash"/>
|
||||
</item>
|
||||
<item android:right="16dp">
|
||||
<bitmap
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_trash"
|
||||
android:gravity="right|center_vertical"
|
||||
android:src="@drawable/ic_item_swipe_trash"/>
|
||||
</item>
|
||||
android:right="16dp"/>
|
||||
</layer-list>
|
||||
|
@ -18,9 +18,8 @@
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_archive"/>
|
||||
</item>
|
||||
<item android:left="16dp">
|
||||
<bitmap
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_archive"
|
||||
android:gravity="left|center_vertical"
|
||||
android:src="@drawable/ic_item_swipe_archive"/>
|
||||
</item>
|
||||
android:left="16dp"/>
|
||||
</layer-list>
|
||||
|
9
app/src/main/res/drawable/ic_menu.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_notification_batch.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15.9,18.45C17.25,18.45 18.35,17.35 18.35,16C18.35,14.65 17.25,13.55 15.9,13.55C14.54,13.55 13.45,14.65 13.45,16C13.45,17.35 14.54,18.45 15.9,18.45M21.1,16.68L22.58,17.84C22.71,17.95 22.75,18.13 22.66,18.29L21.26,20.71C21.17,20.86 21,20.92 20.83,20.86L19.09,20.16C18.73,20.44 18.33,20.67 17.91,20.85L17.64,22.7C17.62,22.87 17.47,23 17.3,23H14.5C14.32,23 14.18,22.87 14.15,22.7L13.89,20.85C13.46,20.67 13.07,20.44 12.71,20.16L10.96,20.86C10.81,20.92 10.62,20.86 10.54,20.71L9.14,18.29C9.05,18.13 9.09,17.95 9.22,17.84L10.7,16.68L10.65,16L10.7,15.31L9.22,14.16C9.09,14.05 9.05,13.86 9.14,13.71L10.54,11.29C10.62,11.13 10.81,11.07 10.96,11.13L12.71,11.84C13.07,11.56 13.46,11.32 13.89,11.15L14.15,9.29C14.18,9.13 14.32,9 14.5,9H17.3C17.47,9 17.62,9.13 17.64,9.29L17.91,11.15C18.33,11.32 18.73,11.56 19.09,11.84L20.83,11.13C21,11.07 21.17,11.13 21.26,11.29L22.66,13.71C22.75,13.86 22.71,14.05 22.58,14.16L21.1,15.31L21.15,16L21.1,16.68M6.69,8.07C7.56,8.07 8.26,7.37 8.26,6.5C8.26,5.63 7.56,4.92 6.69,4.92A1.58,1.58 0 0,0 5.11,6.5C5.11,7.37 5.82,8.07 6.69,8.07M10.03,6.94L11,7.68C11.07,7.75 11.09,7.87 11.03,7.97L10.13,9.53C10.08,9.63 9.96,9.67 9.86,9.63L8.74,9.18L8,9.62L7.81,10.81C7.79,10.92 7.7,11 7.59,11H5.79C5.67,11 5.58,10.92 5.56,10.81L5.4,9.62L4.64,9.18L3.5,9.63C3.41,9.67 3.3,9.63 3.24,9.53L2.34,7.97C2.28,7.87 2.31,7.75 2.39,7.68L3.34,6.94L3.31,6.5L3.34,6.06L2.39,5.32C2.31,5.25 2.28,5.13 2.34,5.03L3.24,3.47C3.3,3.37 3.41,3.33 3.5,3.37L4.63,3.82L5.4,3.38L5.56,2.19C5.58,2.08 5.67,2 5.79,2H7.59C7.7,2 7.79,2.08 7.81,2.19L8,3.38L8.74,3.82L9.86,3.37C9.96,3.33 10.08,3.37 10.13,3.47L11.03,5.03C11.09,5.13 11.07,5.25 11,5.32L10.03,6.06L10.06,6.5L10.03,6.94Z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10H6.71C7.37,7.69 9.5,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M7.73,10L15.73,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10M3,5.27L5.75,8C2.56,8.15 0,10.77 0,14A6,6 0 0,0 6,20H17.73L19.73,22L21,20.73L4.27,4M19.35,10.03C18.67,6.59 15.64,4 12,4C10.5,4 9.15,4.43 8,5.17L9.45,6.63C10.21,6.23 11.08,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15C22,16.13 21.36,17.11 20.44,17.62L21.89,19.07C23.16,18.16 24,16.68 24,15C24,12.36 21.95,10.22 19.35,10.03Z" />
|
||||
</vector>
|
@ -1,14 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#ccffffff" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_focused="true">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke android:color="@android:color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
</selector>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#ffffff">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:drawable="@android:color/white"/>
|
||||
|
||||
</ripple>
|
@ -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" />
|
||||
|
||||
|
47
app/src/main/res/layout/contact_row_slim.xml
Normal 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>
|
125
app/src/main/res/layout/conversation_row.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,9 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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"
|
||||
@ -15,7 +35,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/to"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:maxLines="1"/>
|
||||
android:maxLines="1" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
@ -29,18 +49,20 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/subject"
|
||||
android:inputType="textEmailSubject"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
android:textAppearance="?android:attr/textAppearanceLarge" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/body_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
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="vertical"/>
|
||||
android:scrollbars="none"
|
||||
tools:ignore="InefficientWeight" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
53
app/src/main/res/layout/fragment_conversation_detail.xml
Normal 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>
|
@ -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>
|
||||
|
105
app/src/main/res/layout/item_message_detail.xml
Normal 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>
|
@ -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">
|
||||
|
||||
|
@ -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");
|
||||
@ -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>
|
||||
|
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/material_showcase_button_bg"
|
||||
android:text="@string/got_it"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold">
|
||||
</Button>
|
@ -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>
|
||||
|
15
app/src/main/res/menu/conversation.xml
Normal 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>
|
@ -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"/>
|
||||
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 14 KiB |