Compare commits
41 Commits
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 | |||
16f1dfa6f6 |
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,11 +14,14 @@ 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 23
|
||||
versionName "1.0-rc1"
|
||||
@ -51,11 +54,11 @@ 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"
|
||||
|
||||
@ -65,49 +68,50 @@ dependencies {
|
||||
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 {
|
||||
|
@ -6,6 +6,7 @@
|
||||
<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" />
|
||||
@ -174,20 +175,7 @@
|
||||
|
||||
<!-- Receive Wi-Fi connection state changes -->
|
||||
<receiver
|
||||
android:name=".listener.WifiReceiver"
|
||||
android:enabled="@bool/is_pre_api_21">
|
||||
<intent-filter>
|
||||
<!-- This is bad for battery life, but needed on older devices to check
|
||||
if WiFi is available. Let's be honest, the whole app is bad for
|
||||
battery life. -->
|
||||
<action
|
||||
android:name="android.net.conn.CONNECTIVITY_CHANGE"
|
||||
tools:ignore="BatteryLife" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".service.StartServiceReceiver"
|
||||
android:enabled="@bool/is_post_api_21">
|
||||
android:name=".service.StartServiceReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
@ -198,6 +186,10 @@
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.BatchProcessorService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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?) {
|
||||
|
@ -98,7 +98,8 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
|
||||
"RE:",
|
||||
ignoreCase = true
|
||||
)) {
|
||||
)
|
||||
) {
|
||||
""
|
||||
} else {
|
||||
"RE: "
|
||||
@ -107,7 +108,7 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
}
|
||||
replyIntent.putExtra(
|
||||
EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n" + item.text!!
|
||||
"\n\n------------------------------------------------------\n${item.text ?: ""}"
|
||||
)
|
||||
return replyIntent
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,3 +114,68 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
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
|
||||
}
|
||||
|
@ -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,34 +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
|
||||
).apply {
|
||||
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
|
||||
addRule(RelativeLayout.ALIGN_PARENT_LEFT)
|
||||
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
|
||||
setMargins(margin, margin, margin, margin)
|
||||
MaterialShowcaseView.Builder(this)
|
||||
.setMaskColour(R.color.colorPrimary)
|
||||
.setTitleText(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setDismissOnTouch(true)
|
||||
.setDismissText(R.string.got_it)
|
||||
.setShape(object : Shape {
|
||||
var w = 0
|
||||
var h = 0
|
||||
|
||||
override fun updateTarget(target: Target) {
|
||||
w = target.bounds.width()
|
||||
h = target.bounds.height()
|
||||
}
|
||||
|
||||
ShowcaseView.Builder(this)
|
||||
.withMaterialShowcase()
|
||||
.setStyle(R.style.CustomShowcaseTheme)
|
||||
.setContentTitle(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)
|
||||
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)
|
||||
}
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps)
|
||||
|
||||
override fun getWidth() = w
|
||||
})
|
||||
.setTarget(drawer.stickyFooter)
|
||||
.setDelay(1000)
|
||||
.show()
|
||||
}
|
||||
|
||||
SyncAdapter.startSync(this)
|
||||
}
|
||||
|
||||
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
|
||||
@ -299,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)
|
||||
@ -324,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) {
|
||||
@ -398,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)
|
||||
}
|
||||
|
||||
@ -438,11 +452,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
} else {
|
||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
||||
}
|
||||
runOnUiThread {
|
||||
drawer.updateItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from [ListSelectionListener]
|
||||
@ -454,6 +470,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
val fragment = when (item) {
|
||||
is Conversation -> {
|
||||
ConversationDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
ComposeMessageFragment().apply {
|
||||
@ -485,6 +508,11 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
val detailIntent = when (item) {
|
||||
is Conversation -> {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
Intent(this, ComposeMessageActivity::class.java).apply {
|
||||
@ -518,6 +546,25 @@ 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 {
|
||||
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
|
||||
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
|
||||
|
@ -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.*
|
||||
|
||||
@ -85,8 +85,8 @@ 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 {
|
||||
@ -229,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
|
||||
@ -259,40 +259,6 @@ class MessageDetailFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
private val labels = labels.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.color(Labels.getColor(label))
|
||||
viewHolder.icon.icon?.icon(Labels.getIcon(label))
|
||||
viewHolder.label.text = Labels.getText(label, ctx)
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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.Singleton
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import org.jetbrains.anko.connectivityManager
|
||||
|
||||
class WifiReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
|
||||
NetworkUtils.doStartBitmessageService(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
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)
|
||||
@ -55,10 +55,13 @@ 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))
|
||||
|
@ -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,18 +28,15 @@ 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) {
|
||||
|
||||
@ -53,7 +51,7 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
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))
|
||||
|
@ -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,7 +88,8 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
|
||||
}
|
||||
}
|
||||
|
||||
internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
internal fun findLabels(msgId: Any) =
|
||||
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
|
||||
companion object {
|
||||
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
|
||||
@ -97,11 +104,12 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
|
||||
internal fun getLabel(c: Cursor, context: Context): Label {
|
||||
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
|
||||
}
|
||||
|
@ -40,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(
|
||||
@ -63,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 {
|
||||
@ -74,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))
|
||||
@ -133,7 +145,22 @@ 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
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
@ -174,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)
|
||||
@ -233,6 +261,39 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
|
||||
val result = mutableListOf<Plaintext>()
|
||||
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
|
||||
"$COLUMN_ID ASC",
|
||||
"$limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "Message"
|
||||
private const val COLUMN_ID = "id"
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -28,6 +28,7 @@ import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NO
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
@ -60,7 +61,10 @@ class BitmessageService : Service() {
|
||||
override fun onCreate() {
|
||||
registerReceiver(
|
||||
connectivityReceiver,
|
||||
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
IntentFilter().apply {
|
||||
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||
}
|
||||
)
|
||||
notification = NetworkNotification(this)
|
||||
running = false
|
||||
@ -87,7 +91,9 @@ class BitmessageService : Service() {
|
||||
running = false
|
||||
notification.showShutdown()
|
||||
cleanupHandler.removeCallbacks(cleanupTask)
|
||||
doAsync {
|
||||
bmc.cleanup()
|
||||
}
|
||||
unregisterReceiver(connectivityReceiver)
|
||||
stopSelf()
|
||||
}
|
||||
|
@ -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
|
||||
@ -66,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
|
||||
@ -97,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
|
||||
@ -106,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
|
||||
|
@ -14,8 +14,8 @@ 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()) {
|
||||
@ -24,11 +24,10 @@ class StartupNodeOnWifiService : JobService() {
|
||||
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,
|
||||
|
@ -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
|
||||
@ -57,8 +54,7 @@ object Assets {
|
||||
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
|
||||
@ -67,4 +63,3 @@ object Assets {
|
||||
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 const 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
|
||||
}
|
||||
|
||||
@ -99,4 +101,3 @@ object Drawables {
|
||||
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)
|
||||
@ -27,7 +25,7 @@ object Labels {
|
||||
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
|
||||
@ -39,7 +37,6 @@ object Labels {
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getColor(label: Label) = if (label.type == null) {
|
||||
label.color
|
||||
} else 0xFF000000.toInt()
|
||||
}
|
||||
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
|
||||
color
|
||||
} else default
|
||||
|
@ -6,10 +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 android.support.v4.content.ContextCompat
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
|
||||
import ch.dissem.apps.abit.service.BitmessageService
|
||||
import ch.dissem.apps.abit.service.StartupNodeOnWifiService
|
||||
@ -19,27 +16,17 @@ 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 {
|
||||
doStartBitmessageService(ctx)
|
||||
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 {
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,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())
|
||||
}
|
||||
}
|
||||
|
@ -1,344 +0,0 @@
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
* <p/>
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*
|
||||
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
|
||||
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final int VERSION_CODE_JELLY_BEAN = 16;
|
||||
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/**
|
||||
* Hidden constructor to prevent instantiation.
|
||||
*/
|
||||
private PRNGFixes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, (Object) generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
@SuppressWarnings("JavaDoc")
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// On a small fraction of devices /dev/urandom is not writable.
|
||||
// Log and ignore.
|
||||
Log.w(PRNGFixes.class.getSimpleName(),
|
||||
"Failed to mix seed into " + URANDOM_FILE);
|
||||
} finally {
|
||||
mSeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() throws IOException {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ object PowStats {
|
||||
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L)
|
||||
}
|
||||
}
|
||||
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>
|
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>
|
@ -46,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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
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>
|
@ -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 |
@ -109,9 +109,9 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
|
||||
<string name="status_received">empfangen</string>
|
||||
<string name="status_sent">gesendet</string>
|
||||
<string name="status_sent_acknowledged">Empfang bestätigt</string>
|
||||
<string name="error_unsupported_encoding">Codierung ‘%s’ wird nicht unterstützt, es wird ‘SIMPLE’ verwendet.</string>
|
||||
<string name="error_unsupported_encoding">Codierung ‘%s’ wird nicht unterstützt, es wird ‘einfach’ verwendet.</string>
|
||||
<string name="select_encoding_title">Kodierung auswählen</string>
|
||||
<string name="select_encoding_warning">‘EXTENDED’ ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle ‘SIMPLE’ um sicher zu gehen.</string>
|
||||
<string name="select_encoding_warning">‘Erweitert’ ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle ‘einfach’ um sicher zu gehen.</string>
|
||||
<string name="cleanup">Aufräumen</string>
|
||||
<string name="cleanup_notification_start">Aufräumarbeiten gestartet</string>
|
||||
<string name="cleanup_notification_end">Aufräumarbeiten beendet</string>
|
||||
@ -135,4 +135,8 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
|
||||
<string name="unread">Ungelesen</string>
|
||||
<string name="trash">Papierkorb</string>
|
||||
<string name="broadcasts">Broadcasts</string>
|
||||
<string name="encoding_simple">einfach</string>
|
||||
<string name="encoding_extended">erweitert</string>
|
||||
<string name="emulate_conversations">Konversation erraten</string>
|
||||
<string name="emulate_conversations_summary">Benutze Betreff um zu erraten welche Nachrichten zusammengehören. Die Reihenfolge stimmt häufig nicht.</string>
|
||||
</resources>
|
||||
|
@ -105,8 +105,8 @@
|
||||
<string name="status_draft">brouillon</string>
|
||||
<string name="status_sent">envoyé</string>
|
||||
<string name="status_received">reçu</string>
|
||||
<string name="error_unsupported_encoding">Encodage non supporté \'%s\', \'SIMPLE\' est utilisé.</string>
|
||||
<string name="select_encoding_warning">\'EXTENDED\' est un nouveau format de message qui ne fonctionne pas encore partout, mais qui a des avantages intéressants. Sélectionnez \'SIMPLE\' pour être sûr.</string>
|
||||
<string name="error_unsupported_encoding">Encodage non supporté \'%s\', \'simple\' est utilisé.</string>
|
||||
<string name="select_encoding_warning">\'Étendu\' est un nouveau format de message qui ne fonctionne pas encore partout, mais qui a des avantages intéressants. Sélectionnez \'simple\' pour être sûr.</string>
|
||||
<string name="select_encoding_title">Sélectionner le codage des messages</string>
|
||||
<string name="cleanup">Nettoyage</string>
|
||||
<string name="cleanup_summary">Supprimer les entrées d\'inventaire obsolètes</string>
|
||||
@ -132,4 +132,6 @@
|
||||
<string name="unread">Non lues</string>
|
||||
<string name="trash">Corbeille</string>
|
||||
<string name="broadcasts">Diffusions</string>
|
||||
<string name="encoding_simple">simple</string>
|
||||
<string name="encoding_extended">étendu</string>
|
||||
</resources>
|
||||
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="is_pre_api_21">false</bool>
|
||||
<bool name="is_post_api_21">true</bool>
|
||||
</resources>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="is_pre_api_21">true</bool>
|
||||
<bool name="is_post_api_21">false</bool>
|
||||
</resources>
|
@ -15,6 +15,7 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<dimen name="action_bar_offset">66dp</dimen>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
@ -108,8 +108,8 @@ As an alternative you could configure a trusted node in the settings, but as of
|
||||
<string name="status_draft">draft</string>
|
||||
<string name="status_sent">sent</string>
|
||||
<string name="status_received">received</string>
|
||||
<string name="error_unsupported_encoding">Unsupported encoding ‘%s’, using ‘SIMPLE’ instead.</string>
|
||||
<string name="select_encoding_warning">‘EXTENDED’ is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select ‘SIMPLE’.</string>
|
||||
<string name="error_unsupported_encoding">Unsupported encoding ‘%s’, using ‘simple’ instead.</string>
|
||||
<string name="select_encoding_warning">‘Extended’ is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select ‘simple’.</string>
|
||||
<string name="select_encoding_title">Select Message Encoding</string>
|
||||
<string name="cleanup">Cleanup</string>
|
||||
<string name="cleanup_summary">Remove outdated inventory entries</string>
|
||||
@ -134,4 +134,23 @@ As an alternative you could configure a trusted node in the settings, but as of
|
||||
<string name="unread">Unread</string>
|
||||
<string name="trash">Trash</string>
|
||||
<string name="broadcasts">Broadcasts</string>
|
||||
<string name="encoding_simple">simple</string>
|
||||
<string name="encoding_extended">extended</string>
|
||||
<string name="context_menu">actions</string>
|
||||
<string name="emulate_conversations">Guess conversations</string>
|
||||
<string name="emulate_conversations_summary">Use subject to determine which messages belong together. The order will likely be wrong.</string>
|
||||
<string name="emulate_conversations_initialize">Group existing messages by subject</string>
|
||||
<string name="emulate_conversations_batch">Grouping existing messages by subject</string>
|
||||
<string name="preference_group_user_experience">Behaviour</string>
|
||||
<string name="preference_group_user_experience_summary">Change how messages are displayed</string>
|
||||
<string name="preference_group_network_and_performance">Network & Performance</string>
|
||||
<string name="preference_group_network_and_performance_summary">Tweak network usage and protocol details</string>
|
||||
<string name="preference_group_advanced">Advanced</string>
|
||||
<string name="preference_group_advanced_summary"></string>
|
||||
<string name="preference_group_experimental">Experimental</string>
|
||||
<string name="preference_group_experimental_summary">Only change if you know what you\'re doing</string>
|
||||
<string name="require_charging">Require charging</string>
|
||||
<string name="require_charging_summary">Only connect when device is plugged in</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="ok">OK</string>
|
||||
</resources>
|
||||
|
@ -8,18 +8,6 @@
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomShowcaseTheme" parent="ShowcaseView">
|
||||
<item name="sv_backgroundColor">#eeffc107</item>
|
||||
<item name="sv_showcaseColor">#ffc107</item>
|
||||
<item name="sv_buttonText">Hide</item>
|
||||
<item name="sv_tintButtonColor">false</item>
|
||||
<item name="sv_titleTextAppearance">@style/CustomTitle</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomTitle" parent="TextAppearance.ShowcaseView.Title">
|
||||
<item name="android:textColor">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
|
||||
<item name="windowNoTitle">false</item>
|
||||
</style>
|
||||
|
@ -1,58 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_ux"
|
||||
android:title="@string/preference_group_user_experience"
|
||||
android:summary="@string/preference_group_user_experience_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="emulate_conversations"
|
||||
android:summary="@string/emulate_conversations_summary"
|
||||
android:title="@string/emulate_conversations" />
|
||||
<Preference
|
||||
android:defaultValue="true"
|
||||
android:key="emulate_conversations_initialize"
|
||||
android:summary="@string/emulate_conversations_summary"
|
||||
android:title="@string/emulate_conversations_initialize" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_network_and_performance"
|
||||
android:title="@string/preference_group_network_and_performance"
|
||||
android:summary="@string/preference_group_network_and_performance_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="wifi_only"
|
||||
android:summary="@string/wifi_only_summary"
|
||||
android:title="@string/wifi_only" />
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="require_charging"
|
||||
android:summary="@string/require_charging_summary"
|
||||
android:title="@string/require_charging" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="request_acknowledgements"
|
||||
android:summary="@string/request_acknowledgements_summary"
|
||||
android:title="@string/request_acknowledgements" />
|
||||
<android.support.v7.preference.EditTextPreference
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_advanced"
|
||||
android:title="@string/preference_group_advanced"
|
||||
android:summary="@string/preference_group_advanced_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<Preference
|
||||
android:key="cleanup"
|
||||
android:summary="@string/cleanup_summary"
|
||||
android:title="@string/cleanup" />
|
||||
<Preference
|
||||
android:key="export"
|
||||
android:summary="@string/export_data_summary"
|
||||
android:title="@string/export_data" />
|
||||
<Preference
|
||||
android:key="import"
|
||||
android:summary="@string/import_data_summary"
|
||||
android:title="@string/import_data" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_experimental"
|
||||
android:title="@string/preference_group_experimental"
|
||||
android:summary="@string/preference_group_experimental_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="textUri"
|
||||
android:key="trusted_node"
|
||||
android:summary="@string/trusted_node_summary"
|
||||
android:title="@string/trusted_node" />
|
||||
<android.support.v7.preference.EditTextPreference
|
||||
<EditTextPreference
|
||||
android:defaultValue="120"
|
||||
android:inputType="number"
|
||||
android:key="sync_timeout"
|
||||
android:summary="@string/sync_timeout_summary"
|
||||
android:title="@string/sync_timeout" />
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:dependency="trusted_node"
|
||||
android:key="server_pow"
|
||||
android:summary="@string/server_pow_summary"
|
||||
android:title="@string/server_pow" />
|
||||
<android.support.v7.preference.Preference
|
||||
<Preference
|
||||
android:key="status"
|
||||
android:summary="@string/status_summary"
|
||||
android:title="@string/status" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<Preference
|
||||
android:key="about"
|
||||
android:summary="@string/about_summary"
|
||||
android:title="@string/about" />
|
||||
<android.support.v7.preference.Preference
|
||||
<Preference
|
||||
android:key="help_out"
|
||||
android:summary="@string/help_out_summary"
|
||||
android:title="@string/help_out">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="@string/help_out_link" />
|
||||
</android.support.v7.preference.Preference>
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="cleanup"
|
||||
android:summary="@string/cleanup_summary"
|
||||
android:title="@string/cleanup" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="export"
|
||||
android:summary="@string/export_data_summary"
|
||||
android:title="@string/export_data" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="import"
|
||||
android:summary="@string/import_data_summary"
|
||||
android:title="@string/import_data" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="status"
|
||||
android:summary="@string/status_summary"
|
||||
android:title="@string/status" />
|
||||
</android.support.v7.preference.PreferenceScreen>
|
||||
</Preference>
|
||||
</PreferenceScreen>
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package ch.dissem.bitmessage.repository
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import ch.dissem.apps.abit.repository.AndroidAddressRepository
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package ch.dissem.bitmessage.repository
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import ch.dissem.apps.abit.repository.AndroidInventory
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
|
@ -16,13 +16,12 @@
|
||||
|
||||
package ch.dissem.bitmessage.repository
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.LabelRepository
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -21,7 +21,7 @@ import ch.dissem.apps.abit.repository.AndroidAddressRepository
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
@ -69,7 +69,7 @@ class AndroidMessageRepositoryTest : TestBase() {
|
||||
val labelRepo = AndroidLabelRepository(sqlHelper, RuntimeEnvironment.application)
|
||||
repo = AndroidMessageRepository(sqlHelper)
|
||||
mockedInternalContext(
|
||||
cryptography = SpongyCryptography(),
|
||||
cryptography = BouncyCryptography(),
|
||||
addressRepository = addressRepo,
|
||||
labelRepository = labelRepo,
|
||||
messageRepository = repo,
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package ch.dissem.bitmessage.repository
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import ch.dissem.apps.abit.repository.AndroidNodeRegistry
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
|
@ -59,7 +59,6 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun setUp() {
|
||||
RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME)
|
||||
val sqlHelper = SqlHelper(RuntimeEnvironment.application)
|
||||
|
||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
||||
messageRepo = AndroidMessageRepository(sqlHelper)
|
||||
repo = AndroidProofOfWorkRepository(sqlHelper)
|
||||
@ -94,12 +93,14 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
messageRepo.save(plaintext)
|
||||
plaintext.ackMessage!!.let { ackMessage ->
|
||||
initialHash2 = cryptography().getInitialHash(ackMessage)
|
||||
repo.putObject(ProofOfWorkRepository.Item(
|
||||
repo.putObject(
|
||||
ProofOfWorkRepository.Item(
|
||||
objectMessage = ackMessage,
|
||||
nonceTrialsPerByte = 1000, extraBytes = 1000,
|
||||
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
|
||||
message = plaintext
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,13 +133,15 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
.build()
|
||||
messageRepo.save(plaintext)
|
||||
plaintext.ackMessage!!.let { ackMessage ->
|
||||
repo.putObject(ProofOfWorkRepository.Item(
|
||||
repo.putObject(
|
||||
ProofOfWorkRepository.Item(
|
||||
objectMessage = ackMessage,
|
||||
nonceTrialsPerByte = 1000,
|
||||
extraBytes = 1000,
|
||||
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
|
||||
message = plaintext
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
assertThat(repo.getItems().size, `is`(sizeBefore + 1))
|
||||
}
|
||||
@ -147,7 +150,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun `ensure item can be retrieved`() {
|
||||
val item = repo.getItem(initialHash1)
|
||||
assertThat(item, notNullValue())
|
||||
assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GetPubkey::class.java))
|
||||
assertThat<ObjectPayload>(
|
||||
item.objectMessage.payload,
|
||||
instanceOf<ObjectPayload>(GetPubkey::class.java)
|
||||
)
|
||||
assertThat(item.nonceTrialsPerByte, `is`(1000L))
|
||||
assertThat(item.extraBytes, `is`(1000L))
|
||||
}
|
||||
@ -156,7 +162,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun `ensure ack item can be retrieved`() {
|
||||
val item = repo.getItem(initialHash2)
|
||||
assertThat(item, notNullValue())
|
||||
assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GenericPayload::class.java))
|
||||
assertThat<ObjectPayload>(
|
||||
item.objectMessage.payload,
|
||||
instanceOf<ObjectPayload>(GenericPayload::class.java)
|
||||
)
|
||||
assertThat(item.nonceTrialsPerByte, `is`(1000L))
|
||||
assertThat(item.extraBytes, `is`(1000L))
|
||||
assertThat(item.expirationTime, not<Number>(0))
|
||||
|
@ -19,7 +19,7 @@ package ch.dissem.bitmessage.repository
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.Preferences
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
@ -41,7 +41,7 @@ open class TestBase {
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
mockedInternalContext(
|
||||
cryptography = SpongyCryptography(),
|
||||
cryptography = BouncyCryptography(),
|
||||
proofOfWorkEngine = MultiThreadedPOWEngine()
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.21'
|
||||
ext.kotlin_version = '1.2.41'
|
||||
ext.anko_version = '0.10.4'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
|
||||
@ -19,10 +19,10 @@ allprojects {
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
maven { url "https://jitpack.io" }
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Mon Oct 23 08:19:50 CEST 2017
|
||||
#Sat Mar 03 14:35:52 CET 2018
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
|
||||
|