Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78871374fb | |||
| 6c56d7ae4a |
@@ -1,9 +0,0 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: androidsdk/android-28
|
||||
commands:
|
||||
- ./gradlew assemble
|
||||
- ./gradlew check
|
||||
+8
-171
@@ -1,165 +1,18 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Android ###
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
/*/build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
### Android Patch ###
|
||||
gen-external-apklibs
|
||||
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
|
||||
### Eclipse ###
|
||||
*.pydevproject
|
||||
.metadata
|
||||
.gradle
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
|
||||
### OSX ###
|
||||
# Mac Specific
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
@@ -168,24 +21,8 @@ Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Jekyll Specific
|
||||
_site/
|
||||
|
||||
|
||||
### Windows ###
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
# Ruby
|
||||
Gemfile.lock
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
layout: default
|
||||
title: 404
|
||||
permalink: /404.html
|
||||
---
|
||||
|
||||
<div class="container-fluid index error">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12 content-panel articles">
|
||||
<h1 class="header author-header">404</h1>
|
||||
|
||||
<div class="error-text">
|
||||
Sorry but this page doesn't seem to exist.
|
||||
</div>
|
||||
|
||||
<div class="error-text">
|
||||
<a href="{{ site.baseurl }}/">Home</a> |
|
||||
<a href="{{ site.baseurl }}/posts/">All Posts</a> |
|
||||
<a href="{{ site.baseurl }}/search/">Search</a>
|
||||
</div>
|
||||
|
||||
{% include social_links.html %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content-panel related">
|
||||
{% for post in site.posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Suggested Article</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
# 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/
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,201 +1,28 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Copyright (C) 2014 Jacob Tomlinson
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
The contents of this website, which consists of the files in
|
||||
* _config.yml
|
||||
* _data
|
||||
* _drafts
|
||||
* _posts
|
||||
|
||||
1. Definitions.
|
||||
are copyrighted and sole property of its author Jacob Tomlinson. It may not be
|
||||
used, modified, syndicated or distributed without expressed permission from
|
||||
the author.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
However the website theme built using jekyll is Open Source under the following
|
||||
GPLv3 license.
|
||||
|
||||
"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.
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
"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.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
@@ -1,14 +1,56 @@
|
||||
# Abit
|
||||
# Carte Noire
|
||||
|
||||
A Bitmessage client for Android.
|
||||
A simple Jekyll theme for blogging. Not named after the coffee.
|
||||
|
||||
**Please be aware that due to the protocol, sending messages might suck your
|
||||
battery dry. Also, it causes a lot of traffic and may use up your data plan
|
||||
rather quickly. Use at your own peril.**
|
||||

|
||||
|
||||
Abit uses the [Jabit Bitmessage library](https://github.com/Dissem/Jabit).
|
||||
### Article
|
||||

|
||||
|
||||
## Requirements
|
||||
You'll need at least Android 4.4 KitKat. Due to the Proof of Work that comes
|
||||
with the protocol you might want as fast a processor and as many cores as
|
||||
you can get.
|
||||
### Disqus Comments
|
||||

|
||||
|
||||
### Posts grouped by year
|
||||

|
||||
|
||||
### JavaScript Search
|
||||

|
||||
|
||||
### Menu by mmenu
|
||||

|
||||
|
||||
## Contact
|
||||
If you wish to contact me regarding this theme please raise an issue on GitHub,
|
||||
tweet me [@_jacobtomlinson](http://www.twitter.com/_jacobtomlinson) or email me
|
||||
[jacob@jacobtomlinson.co.uk](mailto:jacob@jacobtomlinson.co.uk).
|
||||
|
||||
## Contribution
|
||||
Pull requests are very welcome.
|
||||
|
||||
## Theme
|
||||
This jekyll theme has been created from scratch. Ideas and inspiration are taken
|
||||
from other places but the code is my own.
|
||||
|
||||
## Tools and Libraries
|
||||
The following tools and libraries are used in this theme
|
||||
|
||||
### JavaScript
|
||||
* [jQuery](http://jquery.com/)
|
||||
* [MMenu](http://mmenu.frebsite.nl/)
|
||||
* [HighlightJS](https://highlightjs.org/)
|
||||
* [Simple Jekyll Search](https://github.com/christian-fei/Simple-Jekyll-Search)
|
||||
|
||||
### CSS
|
||||
* [Bootstrap](http://getbootstrap.com/)
|
||||
* [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
|
||||
|
||||
### Social
|
||||
* [AddThis](http://www.addthis.com/)
|
||||
* [Disqus](https://disqus.com/)
|
||||
|
||||
### Other
|
||||
* [Real Favicon Generator](http://realfavicongenerator.net/)
|
||||
* [Google Analytics](http://www.google.com/analytics/)
|
||||
|
||||
## License
|
||||
The jekyll theme, HTML, CSS and JavaScript is licensed under GPLv3 (unless stated otherwise in the file).
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely need to edit after that.
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site settings
|
||||
title: Abit
|
||||
email: chrigu.meyer@gmail.com
|
||||
description: > # this means to ignore newlines until "baseurl:"
|
||||
Write an awesome description for your new site here. You can edit this
|
||||
line in _config.yml. It will appear in your document head meta (for
|
||||
Google search results) and in your feed.xml site description.
|
||||
baseurl: "/Abit" # the subpath of your site, e.g. /blog
|
||||
url: "https://dissem.github.io" # the base hostname & protocol for your site
|
||||
title_description: "A Bitmessage client for Android™"
|
||||
title_image: "ic_launcher-web.png"
|
||||
#google_analytics: "UA-20365477-4"
|
||||
#disqus_account: cartenoire
|
||||
github_repository: https://github.com/Dissem/Abit
|
||||
|
||||
|
||||
# Social usernames/URLs
|
||||
#twitter_username: jekyllrb
|
||||
github_username: Dissem
|
||||
|
||||
# Footer
|
||||
footer_left: "Made with <i class=\"fa fa-heart\"></i> by <a href=\"https://twitter.com/Dissem\">Christian Basler</a>"
|
||||
footer_right: "</> on <a href=\"https://github.com/Dissem/Abit\">Github</a> <i class=\"fa fa-github-alt\"></i>"
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
|
||||
kramdown:
|
||||
input: GFM
|
||||
syntax_highlighter: rouge
|
||||
|
||||
permalink: pretty
|
||||
exclude: [vendor]
|
||||
@@ -0,0 +1,2 @@
|
||||
gravatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=500&d=mm"
|
||||
jekyll: "https://i.imgur.com/aRQcGSi.png"
|
||||
@@ -0,0 +1,86 @@
|
||||
<div class="footer clearfix">
|
||||
<div class="col-md-6">
|
||||
{{ site.footer_left }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ site.footer_right }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
|
||||
<script src="{{ site.baseurl }}/js/jquery.mmenu.min.all.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$("#my-menu").mmenu().on( "closed.mm", function() {
|
||||
$(".menu-button").show();
|
||||
});
|
||||
$(".menu-button").click(function() {
|
||||
$(".menu-button").hide();
|
||||
$("#my-menu").trigger("open.mm");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if page.make-smaller-titles %}
|
||||
<script type="text/javascript">
|
||||
|
||||
function setFontSize() {
|
||||
|
||||
var title, dateOfTitle, fontSizeOfTitle, listOfA, listOfSmall, listOfArticlesDiv, divWidth;
|
||||
|
||||
listOfArticlesDiv = document.getElementsByClassName("articles");
|
||||
|
||||
for (i = 0; i < listOfArticlesDiv.length; i++) {
|
||||
|
||||
listOfA = document.getElementsByClassName("articles")[i].getElementsByTagName("a");
|
||||
listOfSmall = document.getElementsByClassName("articles")[i].getElementsByTagName("small");
|
||||
|
||||
divWidth = document.getElementsByClassName("articles")[i].offsetWidth;
|
||||
|
||||
for (k = 0; k < listOfSmall.length; k++) {
|
||||
|
||||
title = $(listOfA[k]);
|
||||
dateOfTitle = $(listOfSmall[k]);
|
||||
|
||||
fontSizeOfTitle = startingFontSize;
|
||||
title.css("font-size", fontSizeOfTitle);
|
||||
|
||||
while (title.width() + dateOfTitle.width() >= divWidth)
|
||||
title.css("font-size", fontSizeOfTitle -= 0.5);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStartFontSize() {
|
||||
try {
|
||||
startingFontSize = parseInt($(document.getElementsByClassName("articles")[0].getElementsByTagName("a")[0]).css("font-size"));
|
||||
setFontSize();
|
||||
window.addEventListener('resize', setFontSize, true);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
$(document).ready(getStartFontSize);
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if site.google_analytics %}
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '{{ site.google_analytics }}']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
(function () {
|
||||
var ga = document.createElement('script');
|
||||
ga.type = 'text/javascript';
|
||||
ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,45 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
|
||||
<meta name="description" content="{{ site.description }}">
|
||||
|
||||
<link rel="profile" href="https://gmpg.org/xfn/11" />
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/style.css" />
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/jquery.mmenu.all.css" />
|
||||
<link rel="stylesheet" href="{{ site.baseurl }}/css/idea.css">
|
||||
|
||||
<!-- Favicons generated at http://realfavicongenerator.net/ -->
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ site.baseurl }}/favicons/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ site.baseurl }}/favicons/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ site.baseurl }}/favicons/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ site.baseurl }}/favicons/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ site.baseurl }}/favicons/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ site.baseurl }}/favicons/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ site.baseurl }}/favicons/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ site.baseurl }}/favicons/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ site.baseurl }}/favicons/apple-touch-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/android-chrome-192x192.png" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-96x96.png" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="manifest" href="{{ site.baseurl }}/favicons/manifest.json">
|
||||
<link rel="shortcut icon" href="{{ site.baseurl }}/favicons/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-TileImage" content="{{ site.baseurl }}/favicons/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="{{ site.baseurl }}/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid={{ site.addthis_id }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if page.content contains '<script type="math/tex' %}
|
||||
<!-- MathJax for LaTeX -->
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{% endif %}
|
||||
|
||||
</head>
|
||||
@@ -0,0 +1,14 @@
|
||||
<nav id="my-menu">
|
||||
<div>
|
||||
<p>{{ site.title }}</p>
|
||||
|
||||
<ul class="pages">
|
||||
<li><a href="{{ site.baseurl }}/"><i class="fa fa-home"></i> Home</a></li>
|
||||
<li><a href="{{ site.baseurl }}/posts/"><i class="fa fa-archive"></i> All Posts</a></li>
|
||||
<li><a href="{{ site.baseurl }}/search/"><i class="fa fa-search"></i> Search</a></li>
|
||||
</ul>
|
||||
|
||||
{% include social_links.html %}
|
||||
</div>
|
||||
</nav>
|
||||
<div class="menu-button" href="#menu"><i class="fa fa-bars"></i></div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<p class="links">
|
||||
{% if site.twitter_username %}<a href="https://www.twitter.com/{{ site.twitter_username }}" target="_new"><i class="fa fa-twitter"></i></a>{% endif %}
|
||||
{% if site.linkedin_link %}<a href="{{ site.linkedin_link }}" target="_new"><i class="fa fa-linkedin"></i></a>{% endif %}
|
||||
{% if site.google_plus_link %}<a href="{{ site.google_plus_link }}" target="_new"><i class="fa fa-google-plus"></i></a>{% endif %}
|
||||
{% if site.github_username %}<a href="https://github.com/{{ site.github_username }}" target="_new"><i class="fa fa-github-alt"></i></a>{% endif %}
|
||||
{% if site.stackoverflow_link %}<a href="{{ site.stackoverflow_link }}" target="_new"><i class="fa fa-stack-overflow"></i></a>{% endif %}
|
||||
<a href="{{ site.baseurl }}/feed.xml" target="_new"><i class="fa fa-rss"></i></a>
|
||||
</p>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
{% include head.html %}
|
||||
|
||||
<body>
|
||||
|
||||
{% include header.html %}
|
||||
|
||||
<div class="page-content">
|
||||
<div class="wrap">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include footer.html %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{{ content }}
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<div class="container-fluid single">
|
||||
<div class="row">
|
||||
|
||||
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
|
||||
{% if site.data.thumbnail[page.thumbnail] %}
|
||||
<div class="thumb">
|
||||
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
|
||||
</div>
|
||||
{% elsif page.thumbnail %}
|
||||
<div class="thumb">
|
||||
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="header" itemprop="name">{{ page.title }}</h1>
|
||||
|
||||
<div class="content-panel content">
|
||||
|
||||
{% if page.series %}
|
||||
This post is part of the series '{{ page.series }}':
|
||||
<ol class="series">
|
||||
{% for apost in site.posts reversed %}
|
||||
{% if page.series == apost.series %}
|
||||
<li>
|
||||
{% if page.title == apost.title %}
|
||||
{% assign nextpost = true %}
|
||||
{{ apost.title }}
|
||||
{% else %}
|
||||
{% if nextpost == true %}
|
||||
{% assign seriesnext = apost %}
|
||||
{% endif %}
|
||||
{% assign nextpost = false %}
|
||||
<a href="{{ apost.url }}">{{ apost.title }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
<span itemprop="articleBody">{{ content }}</span>
|
||||
|
||||
{% if page.series %}
|
||||
{% if seriesnext %}
|
||||
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<div class="share">
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<div class="addthis_sharing_toolbox"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page.tags and page.tags.size > 0 %}
|
||||
<div class="tags">
|
||||
<small>
|
||||
<i class="fa fa-tags"></i>
|
||||
{{ page.tags | join: ', ' }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.twitter_username and site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
|
||||
</div>
|
||||
{% elsif site.twitter_username %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
|
||||
</div>
|
||||
{% elsif site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article in the comment section below!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<div class="content-panel comments">
|
||||
<div id="disqus_thread">
|
||||
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
|
||||
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.related_posts.length > 0 %}
|
||||
<div class="content-panel related clearfix">
|
||||
{% for post in site.related_posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<hr />
|
||||
<div class="previous previous-next">
|
||||
{% if page.previous %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="next previous-next">
|
||||
{% if page.next %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<script type="text/javascript">
|
||||
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
|
||||
function disqus_config() { this.experiment.enable_scroll_container = true; }
|
||||
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
|
||||
/* * * DON'T EDIT BELOW THIS LINE * * */
|
||||
(function() {
|
||||
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
|
||||
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
|
||||
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
{% if page.minutes %}
|
||||
{% assign minutes = page.minutes %}
|
||||
{% else %}
|
||||
{% assign minutes = content | number_of_words | divided_by: 180 %}
|
||||
{% if minutes == 0 %}{% assign minutes = 1 %}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid single">
|
||||
<div class="row">
|
||||
|
||||
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
|
||||
{% if site.data.thumbnail[page.thumbnail] %}
|
||||
<div class="thumb">
|
||||
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
|
||||
</div>
|
||||
{% elsif page.thumbnail %}
|
||||
<div class="thumb">
|
||||
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="header" itemprop="name">{{ page.title }}</h1>
|
||||
|
||||
<div class="author">
|
||||
<small><i>
|
||||
{% if page.author %}
|
||||
by
|
||||
<span itemprop="author">
|
||||
{% if site.google_plus_link %}
|
||||
<a rel="author" href="{{ site.google_plus_link }}">
|
||||
{% endif %}
|
||||
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
|
||||
<span itemprop="name">{{ page.author }}</span>
|
||||
</span>
|
||||
{% if site.google_plus_link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
on <span itemprop="datePublished" content="2014-08-28">{{ page.date | date: "%B %-d, %Y" }}</span>
|
||||
{% if page.categories != empty %} under {% for category in page.categories limit:1 %}{{ category }}{% endfor %}{% endif %}
|
||||
</i></small>
|
||||
</div>
|
||||
|
||||
<div class="read-time">
|
||||
<small>
|
||||
{{ minutes }} minute read
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="content-panel content">
|
||||
|
||||
{% if page.series %}
|
||||
This post is part of the series '{{ page.series }}':
|
||||
<ol class="series">
|
||||
{% for apost in site.posts reversed %}
|
||||
{% if page.series == apost.series %}
|
||||
<li>
|
||||
{% if page.title == apost.title %}
|
||||
{% assign nextpost = true %}
|
||||
{{ apost.title }}
|
||||
{% else %}
|
||||
{% if nextpost == true %}
|
||||
{% assign seriesnext = apost %}
|
||||
{% endif %}
|
||||
{% assign nextpost = false %}
|
||||
<a href="{{ apost.url }}">{{ apost.title }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
<span itemprop="articleBody">{{ content }}</span>
|
||||
|
||||
{% if page.series %}
|
||||
{% if seriesnext %}
|
||||
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<div class="share">
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<div class="addthis_sharing_toolbox"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page.tags and page.tags.size > 0 %}
|
||||
<div class="tags">
|
||||
<small>
|
||||
<i class="fa fa-tags"></i>
|
||||
{{ page.tags | join: ', ' }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.twitter_username and site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
|
||||
</div>
|
||||
{% elsif site.twitter_username %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
|
||||
</div>
|
||||
{% elsif site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article in the comment section below!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<div class="content-panel comments">
|
||||
<div id="disqus_thread">
|
||||
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
|
||||
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.related_posts.length > 0 %}
|
||||
<div class="content-panel related clearfix">
|
||||
{% for post in site.related_posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<hr />
|
||||
<div class="previous previous-next">
|
||||
{% if page.previous %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="next previous-next">
|
||||
{% if page.next %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<script type="text/javascript">
|
||||
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
|
||||
function disqus_config() { this.experiment.enable_scroll_container = true; }
|
||||
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
|
||||
/* * * DON'T EDIT BELOW THIS LINE * * */
|
||||
(function() {
|
||||
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
|
||||
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
|
||||
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: post
|
||||
title: Using thumbnails
|
||||
date: 2014-06-08 12:32:18
|
||||
summary: Using thumbnails in your Carte Noire articles.
|
||||
categories: jekyll
|
||||
thumbnail: jekyll
|
||||
tags:
|
||||
- thumbnails
|
||||
- carte noire
|
||||
---
|
||||
|
||||
Carte Noire is designed to start each article with an all-white image as a
|
||||
thumbnail. These are created by adding a `thumbnail` parameter to the article's
|
||||
[YAML frontmatter][1]. This thumbnail parameter is processed in one of two ways,
|
||||
images specified in `_data/thumbnails.yml` or using [Font Awesome][2].
|
||||
|
||||
## Images
|
||||
|
||||
To use your own custom images as a thumbnail you must upload them to a web available
|
||||
location (I use [Imgur][3]) and then you need to add the url to `_data/thumbnail.yml`
|
||||
with an associated keyword.
|
||||
|
||||
```
|
||||
jekyll: "http://i.imgur.com/aRQcGSi.png"
|
||||
```
|
||||
|
||||
You then add a `thumbnail` option to the article's frontmatter and provide the keyword
|
||||
for that thumbnail.
|
||||
|
||||
```
|
||||
thumbnail: jekyll
|
||||
```
|
||||
|
||||
This allows you to re-use thumbnails across multiple articles without having to
|
||||
specify the url each time.
|
||||
|
||||
## Font Awesome
|
||||
|
||||
If jekyll can't find a corresponding image in your `thumbnail.yml` file then it
|
||||
will assume you want to use a Font Awesome icon instead. You can find the full
|
||||
list of Font Awesome icons [here][4].
|
||||
|
||||
So for example if your article is about android and you want to use the [android icon][5]
|
||||
from font awesome you can just specify the following in your frontmatter.
|
||||
|
||||
```
|
||||
thumbnail: android
|
||||
```
|
||||
|
||||
Then in the future if you decide you want to use your own android icon you can just
|
||||
add it to `_data/thumbnails.yml` which will override it for all articles using
|
||||
the android thumbnail.
|
||||
|
||||
[1]: http://jekyllrb.com/docs/frontmatter/
|
||||
[2]: http://fortawesome.github.io/Font-Awesome/
|
||||
[3]: http://imgur.com/
|
||||
[4]: http://fortawesome.github.io/Font-Awesome/icons/
|
||||
[5]: http://fortawesome.github.io/Font-Awesome/icon/android/
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: post
|
||||
title: So, What is Jekyll?
|
||||
date: 2014-06-09 12:32:18
|
||||
summary: Transform your plain text into static websites and blogs. Simple, static, and blog-aware.
|
||||
categories: jekyll
|
||||
thumbnail: jekyll
|
||||
tags:
|
||||
- about
|
||||
- jekyll
|
||||
---
|
||||
|
||||
Jekyll is a tool for transforming your plain text into static websites and
|
||||
blogs. It is simple, static, and blog-aware. Jekyll uses the
|
||||
[Liquid](http://docs.shopify.com/themes/liquid-basics) templating
|
||||
language and has builtin [Markdown](http://daringfireball.net/projects/markdown/)
|
||||
and [Textile](http://en.wikipedia.org/wiki/Textile_(markup_language)) support.
|
||||
|
||||
It also ties in nicely to [Github Pages](https://pages.github.com/).
|
||||
|
||||
Learn more about Jekyll on their [website](http://jekyllrb.com/).
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
layout: post
|
||||
title: Carte Noire in Action
|
||||
date: 2014-06-10 12:31:19
|
||||
summary: See what the different elements looks like.
|
||||
categories: jekyll
|
||||
thumbnail: cogs
|
||||
tags:
|
||||
- demo
|
||||
- action
|
||||
- carte
|
||||
- noire
|
||||
---
|
||||
|
||||
**Note** - This article is a derivative of ["See pixyll in action"][1], taken from the lovely jekyll theme [pixyll][4].
|
||||
|
||||
All links are easy to [locate and discern](#), yet don't detract from the harmony
|
||||
of a paragraph. The _same_ goes for italics and __bold__ elements. Even the the strikeout
|
||||
works if <del>for some reason you need to update your post</del>. For consistency's sake,
|
||||
<ins>The same goes for insertions</ins>, of course.
|
||||
|
||||
### Code, with syntax highlighting
|
||||
|
||||
Code blocks use the [peppermint][2] theme.
|
||||
|
||||
{% highlight ruby %}
|
||||
class Awesome < ActiveRecord::Base
|
||||
include EvenMoreAwesome
|
||||
|
||||
validates_presence_of :something
|
||||
validates :email, email_format: true
|
||||
|
||||
def initialize(email, name = nil)
|
||||
self.email = email
|
||||
self.name = name
|
||||
end
|
||||
end
|
||||
{% endhighlight %}
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<title>Title</title>
|
||||
|
||||
<style>body {width: 500px;}</style>
|
||||
|
||||
<script type="application/javascript">
|
||||
function $init() {return true;}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<p checked class="title" id='title'>Title</p>
|
||||
<!-- here goes the rest of the page -->
|
||||
</body>
|
||||
```
|
||||
|
||||
# Headings!
|
||||
|
||||
They're responsive, and well-proportioned (in `padding`, `line-height`, `margin`, and `font-size`).
|
||||
|
||||
##### They draw the perfect amount of attention
|
||||
|
||||
This allows your content to have the proper informational and contextual hierarchy. Yay.
|
||||
|
||||
### There are lists, too
|
||||
|
||||
* Apples
|
||||
* Oranges
|
||||
* Potatoes
|
||||
* Milk
|
||||
|
||||
1. Mow the lawn
|
||||
2. Feed the dog
|
||||
3. Dance
|
||||
|
||||
### Images look great, too
|
||||
|
||||

|
||||
|
||||
|
||||
### Stylish blockquotes included
|
||||
|
||||
You can use the markdown quote syntax, `>` for simple quotes.
|
||||
|
||||
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse quis porta mauris.
|
||||
|
||||
### LaTeX support
|
||||
|
||||
The default math delimiters are \$\$. Hence `$$ E = m \cdot c^2 $$` yields $$ E = m \cdot c^2 $$
|
||||
|
||||
And here's something more fancy:
|
||||
|
||||
$$ \zeta(s) = \frac{1}{\Gamma(s)} \int \limits_0^\infty x^{s-1} \sum_{n=1}^\infty e^{-nx} \mathrm{d}x = \frac{1}{\Gamma(s)} \int \limits_0^\infty \frac{x^{s-1}}{e^x - 1} \mathrm{d}x $$
|
||||
|
||||
|
||||
### There's more being added all the time
|
||||
|
||||
Checkout the [Github repository][3] to request,
|
||||
or add, features.
|
||||
|
||||
Happy writing.
|
||||
|
||||
[1]: http://pixyll.com/jekyll/pixyll/2014/06/10/see-pixyll-in-action/
|
||||
[2]: https://noahfrederick.com/log/lion-terminal-theme-peppermint/
|
||||
[3]: https://github.com/jacobtomlinson/carte-noire
|
||||
[4]: http://pixyll.com/
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
layout: post
|
||||
title: Welcome to Carte Noire
|
||||
date: 2015-03-23 15:31:19
|
||||
author: Jacob Tomlinson
|
||||
summary: Carte Noire is a dark blog theme for Jekyll focusing on a clear reading experience.
|
||||
categories: jekyll
|
||||
thumbnail: heart
|
||||
tags:
|
||||
- welcome
|
||||
- to
|
||||
- carte
|
||||
- noire
|
||||
---
|
||||
|
||||
Welcome to Carte Noire.
|
||||
|
||||
Carte Noire began as a new theme for [my personal blog][1], but has now taken
|
||||
on a life of its own as a free theme for Jekyll.
|
||||
|
||||
The theme has been designed with simplicity and readability in mind. It makes
|
||||
use of third party services such as Disqus ad AddThis to ensure the blog has
|
||||
all the features you would expect from a dynamic application such as Wordpress
|
||||
but with the hosting and maintenance simplicity of Jekyll.
|
||||
|
||||
Please use/copy/share Carte Noire!
|
||||
|
||||
[1]: http://www.jacobtomlinson.co.uk/
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,124 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'idea'
|
||||
|
||||
ext {
|
||||
appName = "Abit"
|
||||
}
|
||||
if (project.hasProperty("project.configs")
|
||||
&& new File(project.property("project.configs") + appName + ".gradle").exists()) {
|
||||
apply from: project.property("project.configs") + appName + ".gradle"
|
||||
}
|
||||
|
||||
//noinspection GroovyMissingReturnStatement
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 23
|
||||
versionName "1.0-rc1"
|
||||
multiDexEnabled true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/core.kotlin_module'
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ext.jabitVersion = '2.0.4'
|
||||
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
|
||||
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-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.0.0'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.0.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.0.0'
|
||||
|
||||
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
testImplementation "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
|
||||
|
||||
implementation 'org.slf4j:slf4j-android:1.7.25'
|
||||
|
||||
implementation 'com.mikepenz:materialize:1.2.0-rc01@aar'
|
||||
implementation('com.mikepenz:materialdrawer:6.1.0-rc01.2@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation('com.mikepenz:aboutlibraries:6.2.0-rc01@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation "com.mikepenz:iconics-core:3.1.0-rc01@aar"
|
||||
implementation "com.mikepenz:iconics-views:3.1.0-rc01@aar"
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
||||
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
|
||||
|
||||
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
||||
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
|
||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
|
||||
implementation 'com.github.angads25:filepicker:1.1.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
|
||||
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.19.0'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:1.3'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation 'org.robolectric:robolectric:3.7.1'
|
||||
testImplementation "org.robolectric:shadows-multidex:3.7.1"
|
||||
|
||||
androidTestImplementation "androidx.multidex:multidex:2.0.0"
|
||||
}
|
||||
|
||||
idea.module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
Vendored
-17
@@ -1,17 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/chris/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="ch.dissem.apps.abit">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MessageDetailActivity"
|
||||
android:label="@string/title_message_detail"
|
||||
android:parentActivityName=".MainActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AddressDetailActivity"
|
||||
android:label="@string/title_subscription_detail"
|
||||
android:parentActivityName=".MainActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".dialog.FullNodeDialogActivity"
|
||||
android:label="@string/full_node"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog" />
|
||||
<activity
|
||||
android:name=".ComposeMessageActivity"
|
||||
android:label="@string/compose_message"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".CreateAddressActivity"
|
||||
android:label="@string/title_activity_open_bitmessage_link"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ImportIdentityActivity"
|
||||
android:label="@string/title_import_identity"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data
|
||||
android:host="*"
|
||||
android:mimeType="*/*"
|
||||
android:pathPattern=".*\\.dat"
|
||||
android:scheme="file" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.ProofOfWorkService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Exports -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="ch.dissem.apps.abit.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".service.BitmessageIntentService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Receive Wi-Fi connection state changes -->
|
||||
<receiver
|
||||
android:name=".service.StartServiceReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".service.NodeStartupService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.CleanupService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.BatchProcessorService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,8 +0,0 @@
|
||||
CREATE TABLE Inventory (
|
||||
hash BINARY(32) NOT NULL PRIMARY KEY,
|
||||
stream INTEGER NOT NULL,
|
||||
expires INTEGER NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
CREATE TABLE Address (
|
||||
address VARCHAR(40) NOT NULL PRIMARY KEY,
|
||||
version INTEGER NOT NULL,
|
||||
alias VARCHAR(255),
|
||||
public_key BLOB,
|
||||
private_key BLOB,
|
||||
subscribed BIT DEFAULT '0'
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
CREATE TABLE Message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
iv BINARY(32) UNIQUE,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
sender VARCHAR(40) NOT NULL,
|
||||
recipient VARCHAR(40),
|
||||
data BLOB NOT NULL,
|
||||
sent INTEGER,
|
||||
received INTEGER,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
initial_hash BINARY(64) UNIQUE,
|
||||
|
||||
FOREIGN KEY (sender) REFERENCES Address (address),
|
||||
FOREIGN KEY (recipient) REFERENCES Address (address)
|
||||
);
|
||||
|
||||
CREATE TABLE Label (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(20),
|
||||
color INT NOT NULL DEFAULT 4278190080, -- FF000000
|
||||
ord INTEGER,
|
||||
|
||||
CONSTRAINT UC_label UNIQUE (label),
|
||||
CONSTRAINT UC_order UNIQUE (ord)
|
||||
);
|
||||
|
||||
CREATE TABLE Message_Label (
|
||||
message_id INTEGER NOT NULL,
|
||||
label_id INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (message_id, label_id),
|
||||
FOREIGN KEY (message_id) REFERENCES Message (id),
|
||||
FOREIGN KEY (label_id) REFERENCES Label (id)
|
||||
);
|
||||
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Broadcast', 'BROADCAST', 50);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100);
|
||||
@@ -1,7 +0,0 @@
|
||||
-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration
|
||||
-- wasn't really necessary yet.
|
||||
--
|
||||
-- This file is here to reduce confusion regarding to the original migration files.
|
||||
|
||||
--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64);
|
||||
--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash);
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE POW (
|
||||
initial_hash BINARY(64) PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
version BIGINT NOT NULL,
|
||||
nonce_trials_per_byte BIGINT NOT NULL,
|
||||
extra_bytes BIGINT NOT NULL
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
|
||||
ALTER TABLE POW ADD COLUMN message_id BIGINT;
|
||||
@@ -1,4 +0,0 @@
|
||||
ALTER TABLE Message ADD COLUMN ack_data BINARY(32);
|
||||
ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Message ADD COLUMN next_try BIGINT;
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE TABLE Node (
|
||||
stream BIGINT NOT NULL,
|
||||
address BINARY(32) NOT NULL,
|
||||
port INT NOT NULL,
|
||||
services BIGINT NOT NULL,
|
||||
time BIGINT NOT NULL,
|
||||
PRIMARY KEY (stream, address, port)
|
||||
);
|
||||
CREATE INDEX idx_time on Node(time);
|
||||
@@ -1 +0,0 @@
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);
|
||||
@@ -1,11 +0,0 @@
|
||||
ALTER TABLE Message ADD COLUMN conversation BINARY[16];
|
||||
|
||||
CREATE TABLE Message_Parent (
|
||||
parent BINARY(64) NOT NULL,
|
||||
child BINARY(64) NOT NULL,
|
||||
pos INT NOT NULL,
|
||||
conversation BINARY[16] NOT NULL,
|
||||
|
||||
PRIMARY KEY (parent, child),
|
||||
FOREIGN KEY (child) REFERENCES Message (iv)
|
||||
);
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.ListFragment
|
||||
import android.view.View
|
||||
import android.widget.ListView
|
||||
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class AbstractItemListFragment<in L, T> : ListFragment(), ListHolder<L> {
|
||||
/**
|
||||
* The fragment's current callback object, which is notified of list item
|
||||
* clicks.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T>
|
||||
/**
|
||||
* The current activated item position. Only used on tablets.
|
||||
*/
|
||||
private var activatedPosition = ListView.INVALID_POSITION
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (context is ListSelectionListener<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = context as ListSelectionListener<T>
|
||||
} else {
|
||||
throw IllegalStateException("Activity must implement fragment's callbacks.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
// Reset the active callbacks interface to the dummy implementation.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = DummyCallback as ListSelectionListener<T>
|
||||
}
|
||||
|
||||
override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) {
|
||||
super.onListItemClick(listView, view, position, id)
|
||||
|
||||
// Notify the active callbacks interface (the activity, if the
|
||||
// fragment is attached to one) that an item has been selected.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(listView.getItemAtPosition(position) as? T)?.let {
|
||||
callbacks.onItemSelected(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (activatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
|
||||
if (isVisible) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActivatedPosition(position: Int) {
|
||||
if (position == ListView.INVALID_POSITION) {
|
||||
listView.setItemChecked(activatedPosition, false)
|
||||
} else {
|
||||
listView.setItemChecked(position, true)
|
||||
}
|
||||
|
||||
activatedPosition = position
|
||||
}
|
||||
|
||||
override fun showPreviousList() = false
|
||||
|
||||
/**
|
||||
* A dummy implementation of the [ListSelectionListener] interface that does
|
||||
* nothing. Used only when this fragment is not attached to an activity.
|
||||
*/
|
||||
internal object DummyCallback : ListSelectionListener<Any> {
|
||||
override fun onItemSelected(item: Any) = Unit // NO OP
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
*/
|
||||
internal const val STATE_ACTIVATED_POSITION = "activated_position"
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Subscription detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a [MainActivity].
|
||||
*
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a [AddressDetailFragment].
|
||||
*/
|
||||
class AddressDetailActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
|
||||
intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM))
|
||||
val fragment = AddressDetailFragment()
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.wif.WifExporter
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_address_detail.*
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class AddressDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: BitmessageAddress? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.address, menu)
|
||||
|
||||
val ctx = activity!!
|
||||
Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail)
|
||||
Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share)
|
||||
Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val item = item ?: return false
|
||||
val ctx = activity ?: return false
|
||||
when (menuItem.itemId) {
|
||||
R.id.write_message -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
val intent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
val warning = if (item.privateKey != null)
|
||||
R.string.delete_identity_warning
|
||||
else
|
||||
R.string.delete_contact_warning
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(warning)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
Singleton.getAddressRepository(ctx).remove(item)
|
||||
MainActivity.apply {
|
||||
if (item.privateKey != null) {
|
||||
removeIdentityEntry(item)
|
||||
}
|
||||
}
|
||||
this.item = null
|
||||
ctx.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.export -> {
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(R.string.confirm_export)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"$item$EXPORT_POSTFIX"
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
WifExporter(Singleton.getBitmessageContext(ctx)).apply {
|
||||
addIdentity(item)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.share -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, item.address)
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
|
||||
= inflater.inflate(R.layout.fragment_address_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
activity?.let { activity ->
|
||||
when {
|
||||
item.isChan -> activity.setTitle(R.string.title_chan_detail)
|
||||
item.privateKey != null -> activity.setTitle(R.string.title_identity_detail)
|
||||
item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail)
|
||||
else -> activity.setTitle(R.string.title_contact_detail)
|
||||
}
|
||||
}
|
||||
|
||||
avatar.setImageDrawable(Identicon(item))
|
||||
name.setText(item.toString())
|
||||
name.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
item.alias = s.toString()
|
||||
}
|
||||
})
|
||||
address.text = item.address
|
||||
address.isSelected = true
|
||||
stream_number.text = getString(R.string.stream_number, item.stream)
|
||||
if (item.privateKey == null) {
|
||||
active.isChecked = item.isSubscribed
|
||||
active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked }
|
||||
|
||||
if (item.pubkey == null) {
|
||||
pubkey_available.alpha = 0.3f
|
||||
pubkey_available_desc.setText(R.string.pubkey_not_available)
|
||||
}
|
||||
} else {
|
||||
active.visibility = View.GONE
|
||||
pubkey_available.visibility = View.GONE
|
||||
pubkey_available_desc.visibility = View.GONE
|
||||
}
|
||||
|
||||
// QR code
|
||||
qr_code.setImageBitmap(item.qrCode())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
item?.let { item ->
|
||||
Singleton.getAddressRepository(context!!).save(item)
|
||||
if (item.privateKey != null) {
|
||||
MainActivity.apply { updateIdentityEntry(item) }
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
const val EXPORT_POSTFIX = ".keys.dat"
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Fragment that shows a list of all contacts, the ones we subscribed to first.
|
||||
*/
|
||||
class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() {
|
||||
private lateinit var adapter: ArrayAdapter<BitmessageAddress>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = object : ArrayAdapter<BitmessageAddress>(
|
||||
activity!!,
|
||||
R.layout.subscription_row,
|
||||
R.id.name,
|
||||
LinkedList()
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val result: View
|
||||
val v: ViewHolder
|
||||
if (convertView == null) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.subscription_row, parent, false)
|
||||
v = ViewHolder(
|
||||
ctx = context,
|
||||
avatar = view.findViewById(R.id.avatar),
|
||||
name = view.findViewById(R.id.name),
|
||||
streamNumber = view.findViewById(R.id.stream_number),
|
||||
subscribed = view.findViewById(R.id.subscribed)
|
||||
)
|
||||
view.tag = v
|
||||
result = view
|
||||
} else {
|
||||
v = convertView.tag as ViewHolder
|
||||
result = convertView
|
||||
}
|
||||
getItem(position)?.let { item ->
|
||||
v.avatar.setImageDrawable(Identicon(item))
|
||||
v.name.text = item.toString()
|
||||
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
|
||||
v.subscribed.visibility =
|
||||
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
listAdapter = adapter
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
initFab(activity as MainActivity)
|
||||
reloadList()
|
||||
}
|
||||
|
||||
override fun reloadList() {
|
||||
adapter.clear()
|
||||
context?.let { context ->
|
||||
val addressRepo = Singleton.getAddressRepository(context)
|
||||
doAsync {
|
||||
addressRepo.getContactIds()
|
||||
.map { addressRepo.getAddress(it) }
|
||||
.forEach { address -> uiThread { adapter.add(address) } }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFab(activity: MainActivity) {
|
||||
activity.updateTitle(getString(R.string.contacts_and_subscriptions))
|
||||
val menu = FabSpeedDialMenu(activity)
|
||||
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
|
||||
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
|
||||
activity.initFab(R.drawable.ic_action_add_contact, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
when (itemId) {
|
||||
// FIXME
|
||||
// 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
|
||||
// .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
// .initiateScan()
|
||||
2 -> {
|
||||
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_address_list, container, false)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (data != null && data.hasExtra("SCAN_RESULT")) {
|
||||
val uri = Uri.parse(data.getStringExtra("SCAN_RESULT"))
|
||||
val intent = Intent(activity, CreateAddressActivity::class.java)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Void) = reloadList()
|
||||
|
||||
private data class ViewHolder(
|
||||
val ctx: Context,
|
||||
val avatar: ImageView,
|
||||
val name: TextView,
|
||||
val streamNumber: TextView,
|
||||
val subscribed: View
|
||||
)
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setHomeAsUpIndicator(R.drawable.ic_action_close)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.content) == null) {
|
||||
// Display the fragment as the main content.
|
||||
val fragment = ComposeMessageFragment()
|
||||
fragment.arguments = intent.extras
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DRAFT = "ch.dissem.abit.Message.DRAFT"
|
||||
const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"
|
||||
const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"
|
||||
const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"
|
||||
const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"
|
||||
const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"
|
||||
const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"
|
||||
const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"
|
||||
|
||||
fun launchReplyTo(fragment: Fragment, item: Plaintext) =
|
||||
fragment.startActivity(
|
||||
getReplyIntent(
|
||||
ctx = fragment.activity
|
||||
?: throw IllegalStateException("Fragment not attached to an activity"),
|
||||
item = item
|
||||
)
|
||||
)
|
||||
|
||||
fun launchReplyTo(activity: Activity, item: Plaintext) =
|
||||
activity.startActivity(getReplyIntent(activity, item))
|
||||
|
||||
private fun getReplyIntent(ctx: Context, item: Plaintext): Intent {
|
||||
val replyIntent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
val receivingIdentity = item.to
|
||||
if (receivingIdentity?.isChan == true) {
|
||||
// reply to chan, not to the sender of the message
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity)
|
||||
// I hate when people send as chan, so it won't be the default behaviour.
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx))
|
||||
} else {
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, item.from)
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity)
|
||||
}
|
||||
// if the original message was sent using extended encoding, use it as well
|
||||
// so features like threading can be supported
|
||||
if (item.encoding == EXTENDED) {
|
||||
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED)
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_PARENT, item)
|
||||
item.subject?.let { subject ->
|
||||
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
|
||||
"RE:",
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
""
|
||||
} else {
|
||||
"RE: "
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject)
|
||||
}
|
||||
replyIntent.putExtra(
|
||||
EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n${item.text ?: ""}"
|
||||
)
|
||||
return replyIntent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_DRAFT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
|
||||
import ch.dissem.apps.abit.adapter.ContactAdapter
|
||||
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message
|
||||
import kotlinx.android.synthetic.main.fragment_compose_message.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageFragment : Fragment() {
|
||||
private lateinit var identity: BitmessageAddress
|
||||
private var recipient: BitmessageAddress? = null
|
||||
private var subject: String = ""
|
||||
private var content: String = ""
|
||||
|
||||
private var broadcast: Boolean = false
|
||||
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
|
||||
private val parents = mutableListOf<InventoryVector>()
|
||||
|
||||
private var draft: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
arguments?.apply {
|
||||
val draft = getSerializable(EXTRA_DRAFT) as Plaintext?
|
||||
if (draft != null) {
|
||||
this@ComposeMessageFragment.draft = draft
|
||||
identity = draft.from
|
||||
recipient = draft.to
|
||||
subject = draft.subject ?: ""
|
||||
content = draft.text ?: ""
|
||||
encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE
|
||||
parents.addAll(draft.parents)
|
||||
} else {
|
||||
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
|
||||
if (context != null && id?.privateKey == null) {
|
||||
id = Singleton.getIdentity(context!!)
|
||||
}
|
||||
if (id?.privateKey != null) {
|
||||
identity = id
|
||||
} else {
|
||||
throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
}
|
||||
broadcast = getBoolean(EXTRA_BROADCAST, false)
|
||||
if (containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
|
||||
}
|
||||
if (containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getString(EXTRA_SUBJECT) ?: throw IllegalStateException("EXTRA_SUBJECT expected")
|
||||
}
|
||||
if (containsKey(EXTRA_CONTENT)) {
|
||||
content = getString(EXTRA_CONTENT) ?: throw IllegalStateException("EXTRA_CONTENT expected")
|
||||
}
|
||||
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
|
||||
|
||||
if (containsKey(EXTRA_PARENT)) {
|
||||
val parent = getSerializable(EXTRA_PARENT) as Plaintext
|
||||
parent.inventoryVector?.let { parents.add(it) }
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(R.layout.fragment_compose_message, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { ctx ->
|
||||
val identities = Singleton.getAddressRepository(ctx).getIdentities()
|
||||
sender_input.adapter = ContactAdapter(ctx, identities, true)
|
||||
val index = identities.indexOf(Singleton.getIdentity(ctx))
|
||||
if (index >= 0) {
|
||||
sender_input.setSelection(index)
|
||||
}
|
||||
|
||||
if (broadcast) {
|
||||
recipient_input.visibility = View.GONE
|
||||
} else {
|
||||
val adapter = ContactAdapter(
|
||||
ctx,
|
||||
Singleton.getAddressRepository(ctx).getContacts()
|
||||
)
|
||||
recipient_input.setAdapter(adapter)
|
||||
recipient_input.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _, _, pos, _ -> recipient = adapter.getItem(pos) }
|
||||
|
||||
recipient_input.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
recipient = adapter.getItem(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) =
|
||||
Unit // leave current selection
|
||||
}
|
||||
recipient?.let { recipient_input.setText(it.toString()) }
|
||||
}
|
||||
subject_input.setText(subject)
|
||||
body_input.setText(content)
|
||||
|
||||
when {
|
||||
recipient == null -> recipient_input.requestFocus()
|
||||
subject.isEmpty() -> subject_input.requestFocus()
|
||||
else -> {
|
||||
body_input.requestFocus()
|
||||
body_input.setSelection(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.compose, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.send -> {
|
||||
send()
|
||||
return true
|
||||
}
|
||||
R.id.select_encoding -> {
|
||||
val encodingDialog = SelectEncodingDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(EXTRA_ENCODING, encoding)
|
||||
encodingDialog.arguments = args
|
||||
encodingDialog.setTargetFragment(this, 0)
|
||||
encodingDialog.show(fragmentManager, "select encoding dialog")
|
||||
return true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) =
|
||||
if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
|
||||
encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun build(ctx: Context): Plaintext {
|
||||
val builder: Plaintext.Builder
|
||||
if (broadcast) {
|
||||
builder = Plaintext.Builder(BROADCAST)
|
||||
} else {
|
||||
val inputString = recipient_input.text.toString()
|
||||
if (recipient == null || recipient?.toString() != inputString) {
|
||||
try {
|
||||
recipient = BitmessageAddress(inputString)
|
||||
} catch (e: Exception) {
|
||||
val contacts = Singleton.getAddressRepository(ctx).getContacts()
|
||||
for (contact in contacts) {
|
||||
if (inputString.equals(contact.alias, ignoreCase = true)) {
|
||||
recipient = contact
|
||||
if (inputString == contact.alias)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
builder = Plaintext.Builder(MSG)
|
||||
.to(recipient)
|
||||
}
|
||||
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
sender?.let { builder.from(it) }
|
||||
if (!ctx.preferences.requestAcknowledgements) {
|
||||
builder.preventAck()
|
||||
}
|
||||
when (encoding) {
|
||||
Plaintext.Encoding.SIMPLE -> builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
Plaintext.Encoding.EXTENDED -> builder.message(
|
||||
ExtendedEncoding(
|
||||
Message(
|
||||
subject = subject_input.text.toString(),
|
||||
body = body_input.text.toString(),
|
||||
parents = parents,
|
||||
files = emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
ctx.getString(R.string.error_unsupported_encoding, encoding),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
draft?.id?.let { builder.id(it) }
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (draft?.labels?.any { it.type == Label.Type.DRAFT } != false) {
|
||||
context?.let { ctx ->
|
||||
draft = build(ctx).also { msg ->
|
||||
Singleton.labeler.markAsDraft(msg)
|
||||
Singleton.getMessageRepository(ctx).save(msg)
|
||||
}
|
||||
Toast.makeText(ctx, "Message saved as draft", Toast.LENGTH_LONG).show()
|
||||
} ?: throw IllegalStateException("Context is not available")
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
identity = sender_input.selectedItem as BitmessageAddress
|
||||
// recipient is set when one is selected
|
||||
subject = subject_input.text?.toString() ?: ""
|
||||
content = body_input.text?.toString() ?: ""
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
if (recipient == null) {
|
||||
Toast.makeText(ctx, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
build(ctx).let { message ->
|
||||
draft = message
|
||||
Singleton.getBitmessageContext(ctx).send(message)
|
||||
}
|
||||
ctx.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import ch.dissem.apps.abit.adapter.ConversationAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class ConversationDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var itemId: UUID? = null
|
||||
private var item: Conversation? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM_ID)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
itemId = arguments.getSerializable(ARG_ITEM_ID) as UUID
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_conversation_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
item = itemId?.let { Singleton.getConversationService(ctx).getConversation(it) }
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||
messages.adapter = ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel)
|
||||
messages.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.conversation, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.delete -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.remove(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.archive(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM_ID = "item_id"
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.adapter.EventListener
|
||||
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
|
||||
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||
import org.jetbrains.anko.*
|
||||
import java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class ConversationListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
|
||||
|
||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private var deleteAllMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableConversationAdapter?.activateOnItemClick = activateOnItemClick
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableConversationAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val conversationIds = messageRepo.findConversations(
|
||||
currentLabel.value,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE,
|
||||
context?.preferences?.separateIdentities == true
|
||||
)
|
||||
conversationIds.forEach { conversationId ->
|
||||
val conversation = conversationService.getConversation(conversationId)
|
||||
uiThread {
|
||||
messageAdapter.add(conversation)
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
isLastPage = conversationIds.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
conversationService = Singleton.getConversationService(activity)
|
||||
|
||||
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun reloadList() = doUpdateList(currentLabel.value)
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableConversationAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableConversationAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
|
||||
deleteAllMenuItem?.isVisible = false
|
||||
|
||||
MainActivity.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val context = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
val listener = object : EventListener {
|
||||
override fun onItemDeleted(position: Int) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
swipeableConversationAdapter?.removeAt(position)
|
||||
|
||||
}
|
||||
|
||||
override fun onItemArchived(position: Int) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.archive(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
swipeableConversationAdapter?.removeAt(position)
|
||||
}
|
||||
|
||||
override fun onItemSelected(position: Int) {
|
||||
swipeableConversationAdapter?.selectedPosition = position
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
swipeableConversationAdapter?.getItem(position)?.let { item ->
|
||||
MainActivity.apply { onItemSelected(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recycler_view.layoutManager = layoutManager
|
||||
swipeableConversationAdapter = SwipeableConversationAdapter(context).apply {
|
||||
activateOnItemClick = this@ConversationListFragment.activateOnItemClick
|
||||
eventListener = listener
|
||||
}
|
||||
recycler_view.adapter = swipeableConversationAdapter
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
val dirs = when (currentLabel.value?.type) {
|
||||
Label.Type.TRASH -> ItemTouchHelper.LEFT
|
||||
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
|
||||
}
|
||||
|
||||
val swipeHandler = SwipeToDeleteCallback(context, dirs, listener)
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||
itemTouchHelper.attachToRecyclerView(recycler_view)
|
||||
|
||||
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
swipeableConversationAdapter = null
|
||||
layoutManager = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
deleteAllMenuItem = menu.findItem(R.id.delete_all)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete_all -> {
|
||||
currentLabel.value?.let { label ->
|
||||
context?.apply {
|
||||
alert(
|
||||
R.string.delete_all_messages_in_list,
|
||||
R.string.delete_all_messages_in_list_ask
|
||||
) {
|
||||
positiveButton(R.string.delete) {
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
cancelButton { }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAllMessages(label: Label) {
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.onNext(label)
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.onNext(backStack.pop())
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Base64
|
||||
import android.util.Base64.URL_SAFE
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V2Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class CreateAddressActivity : AppCompatActivity() {
|
||||
private var pubkeyBytes: ByteArray? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val uri = intent.data
|
||||
if (uri != null)
|
||||
setContentView(R.layout.activity_open_bitmessage_link)
|
||||
else
|
||||
setContentView(R.layout.activity_create_bitmessage_address)
|
||||
|
||||
val address = findViewById<TextView>(R.id.address)
|
||||
val label = findViewById<EditText>(R.id.label)
|
||||
val subscribe = findViewById<Switch>(R.id.subscribe)
|
||||
|
||||
if (uri != null) {
|
||||
val addressText = getAddress(uri)
|
||||
val parameters = getParameters(uri)
|
||||
for (parameter in parameters) {
|
||||
val matcher = KEY_VALUE_PATTERN.matcher(parameter)
|
||||
if (matcher.find()) {
|
||||
val key = matcher.group(1).toLowerCase()
|
||||
val value = matcher.group(2)
|
||||
when (key) {
|
||||
"label" -> label.setText(value.trim { it <= ' ' })
|
||||
"action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true)
|
||||
"pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE)
|
||||
else -> LOG.debug("Unknown attribute: $key=$value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address.text = addressText
|
||||
}
|
||||
|
||||
val cancel = findViewById<Button>(R.id.cancel)
|
||||
cancel.setOnClickListener {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) }
|
||||
}
|
||||
|
||||
|
||||
private fun onOK(address: TextView, label: EditText, subscribe: Switch) {
|
||||
val addressText = address.text.toString().trim { it <= ' ' }
|
||||
try {
|
||||
val bmAddress = BitmessageAddress(addressText)
|
||||
bmAddress.alias = label.text.toString()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(applicationContext)
|
||||
bmc.addContact(bmAddress)
|
||||
if (subscribe.isChecked) {
|
||||
bmc.addSubscribtion(bmAddress)
|
||||
}
|
||||
pubkeyBytes?.let { pubkeyBytes ->
|
||||
try {
|
||||
val pubkeyStream = ByteArrayInputStream(pubkeyBytes)
|
||||
val stream = bmAddress.stream
|
||||
when (bmAddress.version.toInt()) {
|
||||
2 -> V2Pubkey.read(pubkeyStream, stream)
|
||||
3 -> V3Pubkey.read(pubkeyStream, stream)
|
||||
4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream))
|
||||
else -> null
|
||||
}?.let { bmAddress.pubkey = it }
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} catch (e: RuntimeException) {
|
||||
address.error = getString(R.string.error_illegal_address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAddress(uri: Uri): String {
|
||||
val result = StringBuilder()
|
||||
val schemeSpecificPart = uri.schemeSpecificPart
|
||||
if (!schemeSpecificPart.startsWith("BM-")) {
|
||||
result.append("BM-")
|
||||
}
|
||||
when {
|
||||
schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')))
|
||||
schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')))
|
||||
else -> result.append(schemeSpecificPart)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun getParameters(uri: Uri): Array<String> {
|
||||
val index = uri.schemeSpecificPart.indexOf('?')
|
||||
return if (index >= 0) {
|
||||
uri.schemeSpecificPart
|
||||
.substring(index + 1)
|
||||
.split("&".toRegex())
|
||||
.dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
} else {
|
||||
emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java)
|
||||
|
||||
private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$")
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.MenuItem
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class DetailActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.scrolling_toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
// Show the Up button in the action bar.
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import android.text.TextPaint
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import org.jetbrains.anko.collections.forEachWithIndex
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val hash = input.ripe
|
||||
private val isChan = input.isChan
|
||||
private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply {
|
||||
for (row in 0 until SIZE) {
|
||||
if (!isChan || row < 5 || row > 6) {
|
||||
for (column in 0..CENTER_COLUMN) {
|
||||
if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) {
|
||||
this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0
|
||||
this[row][SIZE - column - 1] = this[row][column]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val color = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val background = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val textPaint = TextPaint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = 0xFF607D8B.toInt()
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
draw(canvas, 0f, 0f, width, height)
|
||||
}
|
||||
|
||||
internal fun draw(canvas: Canvas, offsetX: Float, offsetY: Float, width: Float, height: Float) {
|
||||
var x: Float
|
||||
var y: Float
|
||||
val cellWidth = width / SIZE.toFloat()
|
||||
val cellHeight = height / SIZE.toFloat()
|
||||
paint.color = background
|
||||
canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
|
||||
paint.color = color
|
||||
for (row in 0 until SIZE) {
|
||||
for (column in 0 until SIZE) {
|
||||
if (fields[row][column]) {
|
||||
x = offsetX + cellWidth * column
|
||||
y = offsetY + cellHeight * row
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isChan) {
|
||||
textPaint.textSize = 2 * cellHeight
|
||||
canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
paint.colorFilter = cf
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
|
||||
companion object {
|
||||
private const val SIZE = 9
|
||||
private const val CENTER_COLUMN = 5
|
||||
}
|
||||
}
|
||||
|
||||
class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt private val backgroundColor: Int = 0xFFAEC2CC.toInt()) :
|
||||
Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = backgroundColor
|
||||
}
|
||||
|
||||
private val identicons = input.asSequence().sortedBy { it.isChan }.map { Identicon(it) }.take(4).toList()
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width().toFloat()
|
||||
val height = bounds.height().toFloat()
|
||||
|
||||
when (identicons.size) {
|
||||
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
1 -> identicons.first().draw(canvas, 0f, 0f, width, height)
|
||||
2 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val w = width / 2
|
||||
val h = height / 2
|
||||
var x = 0f
|
||||
val y = height / 4
|
||||
identicons.forEach {
|
||||
it.draw(canvas, x, y, w, h)
|
||||
x += w
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
val scale = 2f / (1f + 2f * sqrt(3f))
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
|
||||
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
|
||||
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
|
||||
}
|
||||
4 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val scale = 1f / (1f + sqrt(2f))
|
||||
val borderScale = 0.5f - scale
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
val x = width * borderScale
|
||||
val y = height * borderScale
|
||||
identicons.forEachWithIndex { i, identicon ->
|
||||
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
identicons.forEach { it.alpha = alpha }
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
identicons.forEach { it.colorFilter = colorFilter }
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.wif.WifImporter
|
||||
import org.ini4j.InvalidFileFormatException
|
||||
import org.jetbrains.anko.longToast
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentitiesFragment : Fragment() {
|
||||
private lateinit var adapter: AddressSelectorAdapter
|
||||
private lateinit var importer: WifImporter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val ctx = activity ?: throw IllegalStateException("No activity available")
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val wifData = arguments?.getString(WIF_DATA) ?: throw IllegalStateException("No WIF data")
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
|
||||
try {
|
||||
importer = WifImporter(bmc, wifData)
|
||||
} catch (e: InvalidFileFormatException) {
|
||||
ctx.longToast(R.string.invalid_wif_file)
|
||||
ctx.finish()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = AddressSelectorAdapter(importer.getIdentities())
|
||||
val layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
RecyclerView.VERTICAL,
|
||||
false
|
||||
)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.HORIZONTAL))
|
||||
|
||||
view.findViewById<Button>(R.id.finish).setOnClickListener {
|
||||
importer.importAll(adapter.selected)
|
||||
MainActivity.apply {
|
||||
for (selected in adapter.selected) {
|
||||
addIdentityEntry(selected)
|
||||
}
|
||||
}
|
||||
ctx.finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WIF_DATA = "wif_data"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.transaction
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentityActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
|
||||
|
||||
if (wifData == null) {
|
||||
supportFragmentManager.transaction {
|
||||
replace(R.id.content, InputWifFragment())
|
||||
}
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
|
||||
|
||||
val fragment = ImportIdentitiesFragment().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
supportFragmentManager.transaction {
|
||||
replace(R.id.content, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.transaction
|
||||
import com.github.angads25.filepicker.model.DialogConfigs
|
||||
import com.github.angads25.filepicker.model.DialogProperties
|
||||
import com.github.angads25.filepicker.view.FilePickerDialog
|
||||
import kotlinx.android.synthetic.main.fragment_import_input.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class InputWifFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_import_input, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
next.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString())
|
||||
|
||||
val fragment = ImportIdentitiesFragment().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
fragmentManager?.transaction {
|
||||
replace(R.id.content, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
|
||||
inflater.inflate(R.menu.import_input_data, menu)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val properties = DialogProperties()
|
||||
properties.selection_mode = DialogConfigs.SINGLE_MODE
|
||||
properties.selection_type = DialogConfigs.FILE_SELECT
|
||||
properties.root = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.error_dir = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.extensions = null
|
||||
val dialog = FilePickerDialog(activity, properties)
|
||||
dialog.setTitle(getString(R.string.select_file_title))
|
||||
dialog.setDialogSelectionListener { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
try {
|
||||
FileInputStream(files[0]).use { inputStream ->
|
||||
val data = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
|
||||
var length: Int = inputStream.read(buffer)
|
||||
|
||||
while (length != -1) {
|
||||
data.write(buffer, 0, length)
|
||||
length = inputStream.read(buffer)
|
||||
}
|
||||
wif_input.setText(data.toByteArray().toString())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.error_loading_data,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,30 +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
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
interface ListHolder<in L> {
|
||||
fun reloadList()
|
||||
|
||||
fun updateList(label: L)
|
||||
|
||||
fun setActivateOnItemClick(activateOnItemClick: Boolean)
|
||||
|
||||
fun showPreviousList(): Boolean
|
||||
}
|
||||
@@ -1,585 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.transaction
|
||||
import ch.dissem.apps.abit.drawer.ProfileImageListener
|
||||
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.getColor
|
||||
import ch.dissem.apps.abit.util.getIcon
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.AccountHeaderBuilder
|
||||
import com.mikepenz.materialdrawer.Drawer
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Nameable
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView
|
||||
import uk.co.deanwild.materialshowcaseview.shape.Shape
|
||||
import uk.co.deanwild.materialshowcaseview.target.Target
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a list of Messages. This activity
|
||||
* has different presentations for handset and tablet-size devices. On
|
||||
* handsets, the activity presents a list of items, which when touched,
|
||||
* lead to a [MessageDetailActivity] representing
|
||||
* item details. On tablets, the activity presents the list of items and
|
||||
* item details side-by-side using two vertical panes.
|
||||
*
|
||||
*
|
||||
* The activity makes heavy use of fragments. The list of items is a
|
||||
* [MessageListFragment] and the item details
|
||||
* (if present) is a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* This activity also implements the required
|
||||
* [ListSelectionListener] interface
|
||||
* to listen for item selections.
|
||||
*
|
||||
*/
|
||||
class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
|
||||
private var active: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
*/
|
||||
var hasDetailPane: Boolean = false
|
||||
private set
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private lateinit var accountHeader: AccountHeader
|
||||
|
||||
private lateinit var drawer: Drawer
|
||||
private lateinit var nodeSwitch: SwitchDrawerItem
|
||||
|
||||
val floatingActionButton: FabSpeedDial?
|
||||
get() = findViewById(R.id.fab)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
instance = WeakReference(this)
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
messageRepo = Singleton.getMessageRepository(this)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
fab.hide()
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val listFragment = ConversationListFragment()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.commit()
|
||||
|
||||
if (findViewById<View>(R.id.message_detail_container) != null) {
|
||||
// The detail container view will be present only in the
|
||||
// large-screen layouts (res/values-large and
|
||||
// res/values-sw600dp). If this view is present, then the
|
||||
// activity should be in two-pane mode.
|
||||
hasDetailPane = true
|
||||
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
|
||||
createDrawer(toolbar)
|
||||
|
||||
// handle intents
|
||||
val intent = intent
|
||||
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE))
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
|
||||
val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
}
|
||||
|
||||
if (drawer.isDrawerOpen) {
|
||||
MaterialShowcaseView.Builder(this)
|
||||
.setMaskColour(R.color.colorPrimary)
|
||||
.setTitleText(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setDismissOnTouch(true)
|
||||
.setDismissText(R.string.got_it)
|
||||
.setShape(object : Shape {
|
||||
var w = 0
|
||||
var h = 0
|
||||
|
||||
override fun updateTarget(target: Target) {
|
||||
w = target.bounds.width()
|
||||
h = target.bounds.height()
|
||||
}
|
||||
|
||||
override fun getHeight() = h
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
|
||||
val r = h.toFloat() / 2
|
||||
canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
|
||||
}
|
||||
|
||||
override fun getWidth() = w
|
||||
})
|
||||
.setTarget(drawer.stickyFooter)
|
||||
.setDelay(1000)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
|
||||
if (active) {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.item_list, listFragment)
|
||||
supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let {
|
||||
transaction.remove(it)
|
||||
}
|
||||
transaction.addToBackStack(null).commit()
|
||||
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDrawer(toolbar: Toolbar) {
|
||||
val profiles = ArrayList<IProfile<*>>()
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(
|
||||
IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons)
|
||||
)
|
||||
.withIdentifier(ADD_IDENTITY.toLong())
|
||||
)
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY.toLong())
|
||||
)
|
||||
// Create the AccountHeader
|
||||
accountHeader = AccountHeaderBuilder()
|
||||
.withActivity(this)
|
||||
.withHeaderBackground(R.drawable.header)
|
||||
.withProfiles(profiles)
|
||||
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
|
||||
.withOnAccountHeaderListener(
|
||||
ProfileSelectionListener(
|
||||
this@MainActivity,
|
||||
supportFragmentManager
|
||||
)
|
||||
)
|
||||
.build()
|
||||
if (profiles.size > 2) { // There's always the add and manage identity items
|
||||
accountHeader.setActiveProfile(profiles[0], true)
|
||||
}
|
||||
|
||||
val drawerItems = ArrayList<IDrawerItem<*, *>>()
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withIdentifier(LABEL_ARCHIVE.id as Long)
|
||||
.withName(R.string.archive)
|
||||
.withTag(LABEL_ARCHIVE)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_archive)
|
||||
)
|
||||
drawerItems.add(DividerDrawerItem())
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts)
|
||||
)
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
)
|
||||
|
||||
nodeSwitch = SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
.withName(R.string.online)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
||||
.withChecked(preferences.online)
|
||||
.withOnCheckedChangeListener { _, _, isChecked ->
|
||||
preferences.online = isChecked
|
||||
if (isChecked) {
|
||||
network.enableNode(true)
|
||||
}
|
||||
}
|
||||
|
||||
drawer = DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(accountHeader)
|
||||
.withDrawerItems(drawerItems)
|
||||
.addStickyDrawerItems(nodeSwitch)
|
||||
.withOnDrawerItemClickListener(DrawerItemClickListener())
|
||||
.withShowDrawerOnFirstLaunch(true)
|
||||
.build()
|
||||
|
||||
loadDrawerItemsAsynchronously()
|
||||
}
|
||||
|
||||
private fun loadDrawerItemsAsynchronously() {
|
||||
doAsync {
|
||||
val identities = bmc.addresses.getIdentities()
|
||||
if (identities.isEmpty()) {
|
||||
// Create an initial identity
|
||||
Singleton.getIdentity(this@MainActivity)
|
||||
}
|
||||
|
||||
uiThread {
|
||||
for (identity in identities) {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAsync {
|
||||
val labels = bmc.labels.getLabels()
|
||||
|
||||
uiThread {
|
||||
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
|
||||
currentLabel.onNext(intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label)
|
||||
} else if (currentLabel.value == null) {
|
||||
currentLabel.onNext(labels[0])
|
||||
}
|
||||
|
||||
for (label in labels) {
|
||||
addLabelEntry(label)
|
||||
}
|
||||
currentLabel.value?.let { label ->
|
||||
drawer.setSelection(label.id as Long)
|
||||
}
|
||||
updateUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
|
||||
override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean {
|
||||
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
val tag = item.tag
|
||||
if (tag is Label) {
|
||||
currentLabel.onNext(tag)
|
||||
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
|
||||
if (itemList !is ConversationListFragment) {
|
||||
changeList(ConversationListFragment())
|
||||
}
|
||||
} else {
|
||||
if (itemList !is MessageListFragment) {
|
||||
changeList(MessageListFragment())
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (item is Nameable<*>) {
|
||||
when (item.name.textRes) {
|
||||
R.string.contacts_and_subscriptions -> {
|
||||
if (itemList is AddressListFragment) {
|
||||
itemList.reloadList()
|
||||
} else {
|
||||
changeList(AddressListFragment())
|
||||
}
|
||||
return false
|
||||
}
|
||||
R.string.settings -> {
|
||||
supportFragmentManager?.transaction {
|
||||
replace(R.id.item_list, SettingsFragment())
|
||||
addToBackStack(null)
|
||||
}
|
||||
return false
|
||||
}
|
||||
R.string.full_node -> return true
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
network.enableNode(false)
|
||||
updateUnread()
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
subscription = currentLabel.subscribe { label ->
|
||||
if (label.id is Long) {
|
||||
drawer.setSelection(label.id as Long, false)
|
||||
}
|
||||
}
|
||||
active = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
active = false
|
||||
}
|
||||
|
||||
fun addIdentityEntry(identity: BitmessageAddress) {
|
||||
val newProfile = ProfileDrawerItem()
|
||||
.withIcon(Identicon(identity))
|
||||
.withName(identity.toString())
|
||||
.withNameShown(true)
|
||||
.withEmail(identity.address)
|
||||
.withTag(identity)
|
||||
if (accountHeader.profiles != null) {
|
||||
// we know that there are 2 setting elements.
|
||||
// Set the new profile above them ;)
|
||||
accountHeader.addProfile(
|
||||
newProfile, accountHeader.profiles.size - 2
|
||||
)
|
||||
} else {
|
||||
accountHeader.addProfiles(newProfile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLabelEntry(label: Label) {
|
||||
val item = PrimaryDrawerItem()
|
||||
.withIdentifier(label.id as Long)
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(label.getIcon())
|
||||
.withIconColor(label.getColor(0xFF000000.toInt()))
|
||||
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
|
||||
}
|
||||
|
||||
fun updateIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
profile
|
||||
.withName(identity.toString())
|
||||
.withTag(identity)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
accountHeader.removeProfile(profile)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnread() {
|
||||
for (item in drawer.drawerItems) {
|
||||
if (item.tag is Label) {
|
||||
val label = item.tag as Label
|
||||
if (label !== LABEL_ARCHIVE) {
|
||||
val unread = messageRepo.countUnread(label, preferences.separateIdentities)
|
||||
if (unread > 0) {
|
||||
(item as PrimaryDrawerItem).withBadge(unread.toString())
|
||||
} else {
|
||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
||||
}
|
||||
runOnUiThread {
|
||||
drawer.updateItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from [ListSelectionListener]
|
||||
* indicating that the item with the given ID was selected.
|
||||
*/
|
||||
override fun onItemSelected(item: Serializable) {
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, show the detail view in this activity by
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
val fragment = when (item) {
|
||||
is Conversation -> {
|
||||
ConversationDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
ComposeMessageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessageDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> {
|
||||
AddressDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
val detailIntent = when (item) {
|
||||
is Conversation -> {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
Intent(this, ComposeMessageActivity::class.java).apply {
|
||||
putExtra(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
} else {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java).apply {
|
||||
putExtra(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDetailView(fragment: Fragment) {
|
||||
if (hasDetailPane) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(title: CharSequence) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
|
||||
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.hide()
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
return fab
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
|
||||
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
|
||||
const val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
|
||||
const val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
|
||||
|
||||
const val ADD_IDENTITY = 1
|
||||
const val MANAGE_IDENTITY = 2
|
||||
|
||||
private const val ID_NODE_SWITCH: Long = 1
|
||||
|
||||
private var instance: WeakReference<MainActivity>? = null
|
||||
|
||||
/**
|
||||
* Runs the given code in the main activity context, if it currently exists. Otherwise,
|
||||
* it's ignored.
|
||||
*/
|
||||
fun apply(run: MainActivity.() -> Unit) {
|
||||
instance?.get()?.let { run.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NavUtils
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Message detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a [MainActivity].
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a [MessageDetailFragment].
|
||||
*/
|
||||
class MessageDetailActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
val itemId = intent.getSerializableExtra(ConversationDetailFragment.ARG_ITEM_ID)
|
||||
arguments.putSerializable(ConversationDetailFragment.ARG_ITEM_ID, itemId)
|
||||
val fragment = if (item is Plaintext) {
|
||||
MessageDetailFragment()
|
||||
} else {
|
||||
ConversationDetailFragment()
|
||||
}
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.util.Linkify
|
||||
import android.text.util.Linkify.WEB_URLS
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.adapter.LabelAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.apps.abit.util.getString
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_message_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class MessageDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
item = arguments.getSerializable(ARG_ITEM) as Plaintext
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
status.setImageResource(item.status.getDrawable())
|
||||
status.contentDescription = getString(item.status.getString())
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(item.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
sender.text = item.from.toString()
|
||||
item.to?.let { to ->
|
||||
recipient.text = to.toString()
|
||||
} ?: {
|
||||
if (item.type == Plaintext.Type.BROADCAST) {
|
||||
recipient.setText(R.string.broadcast)
|
||||
}
|
||||
}.invoke()
|
||||
val labelAdapter = LabelAdapter(ctx, item.labels)
|
||||
labels.adapter = labelAdapter
|
||||
labels.layoutManager = GridLayoutManager(activity, 2)
|
||||
|
||||
text.text = item.text
|
||||
|
||||
Linkify.addLinks(text, WEB_URLS)
|
||||
Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
text.linksClickable = true
|
||||
text.setTextIsSelectable(true)
|
||||
|
||||
val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
if (item.isUnread()) {
|
||||
Singleton.labeler.markAsRead(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
messageRepo.save(item)
|
||||
}
|
||||
val parents = ArrayList<Plaintext>(item.parents.size)
|
||||
for (parentIV in item.parents) {
|
||||
val parent = messageRepo.getMessage(parentIV)
|
||||
if (parent != null) {
|
||||
parents.add(parent)
|
||||
}
|
||||
}
|
||||
showRelatedMessages(ctx, view, R.id.parents, parents)
|
||||
showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRelatedMessages(
|
||||
ctx: Context,
|
||||
rootView: View, @IdRes id: Int,
|
||||
messages: List<Plaintext>
|
||||
) {
|
||||
val recyclerView = rootView.findViewById<RecyclerView>(id)
|
||||
val adapter = RelatedMessageAdapter(ctx, messages)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply)
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(
|
||||
activity, menu, R.id.mark_unread, GoogleMaterial.Icon
|
||||
.gmd_markunread
|
||||
)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
if (item.isUnread() && activity is MainActivity) {
|
||||
(activity as MainActivity).updateUnread()
|
||||
}
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private class RelatedMessageAdapter internal constructor(
|
||||
private val ctx: Context,
|
||||
private val messages: List<Plaintext>
|
||||
) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RelatedMessageAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = messages[position]
|
||||
|
||||
viewHolder.avatar.setImageDrawable(Identicon(message.from))
|
||||
viewHolder.status.setImageResource(message.status.getDrawable())
|
||||
viewHolder.sender.text = message.from.toString()
|
||||
viewHolder.extract.text = prepareMessageExtract(message.text)
|
||||
viewHolder.item = message
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
override fun getItemCount() = messages.size
|
||||
|
||||
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
internal val avatar = itemView.findViewById<ImageView>(R.id.avatar)
|
||||
internal val status = itemView.findViewById<ImageView>(R.id.status)
|
||||
internal val sender = itemView.findViewById<TextView>(R.id.sender)
|
||||
internal val extract = itemView.findViewById<TextView>(R.id.text)
|
||||
internal var item: Plaintext? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (ctx is MainActivity) {
|
||||
item?.let { ctx.onItemSelected(it) }
|
||||
} else {
|
||||
val detailIntent = Intent(ctx, MessageDetailActivity::class.java)
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
ctx.startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
|
||||
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.adapter.EventListener
|
||||
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||
import org.jetbrains.anko.*
|
||||
import java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
private var subscription: Disposable? = null
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
|
||||
|
||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - PAGE_SIZE
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private var deleteAllMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableMessageAdapter?.activateOnItemClick = activateOnItemClick
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableMessageAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val label = currentLabel.value
|
||||
val messages = messageRepo.findMessages(
|
||||
label,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE,
|
||||
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
|
||||
)
|
||||
uiThread {
|
||||
messageAdapter.addAll(messages)
|
||||
isLoading = false
|
||||
isLastPage = messages.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
|
||||
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
subscription?.dispose()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun reloadList() = doUpdateList(currentLabel.value)
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
// If the menu item isn't available yet, we should wait - the method will be called again once it's
|
||||
// initialized.
|
||||
emptyTrashMenuItem?.let { menuItem ->
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableMessageAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableMessageAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
menuItem.isVisible = label.type == Label.Type.TRASH
|
||||
MainActivity.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val context = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recycler_view.layoutManager = layoutManager
|
||||
swipeableMessageAdapter = SwipeableMessageAdapter(context).apply {
|
||||
activateOnItemClick = this@MessageListFragment.activateOnItemClick
|
||||
}
|
||||
recycler_view.adapter = swipeableMessageAdapter // requires *wrapped* swipeableMessageAdapter
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
val dirs = when (currentLabel.value?.type) {
|
||||
Label.Type.TRASH -> ItemTouchHelper.LEFT
|
||||
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
|
||||
}
|
||||
|
||||
val swipeHandler = SwipeToDeleteCallback(context, dirs, object : EventListener {
|
||||
override fun onItemDeleted(position: Int) {
|
||||
context.toast("Deleted")
|
||||
}
|
||||
|
||||
override fun onItemArchived(position: Int) {
|
||||
context.toast("Archived")
|
||||
}
|
||||
|
||||
override fun onItemSelected(position: Int) {
|
||||
context.toast("Selected")
|
||||
}
|
||||
})
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(swipeHandler)
|
||||
itemTouchHelper.attachToRecyclerView(recycler_view)
|
||||
|
||||
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
swipeableMessageAdapter = null
|
||||
layoutManager = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
deleteAllMenuItem = menu.findItem(R.id.delete_all)
|
||||
// currentLabel.value?.let { doUpdateList(it) }
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete_all -> {
|
||||
currentLabel.value?.let { label ->
|
||||
context?.apply {
|
||||
alert(
|
||||
R.string.delete_all_messages_in_list,
|
||||
R.string.delete_all_messages_in_list_ask
|
||||
) {
|
||||
positiveButton(R.string.delete) {
|
||||
deleteAllMessages(label)
|
||||
}
|
||||
cancelButton { }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAllMessages(label: Label) {
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.onNext(label)
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.onNext(backStack.pop())
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider.getUriForFile
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import ch.dissem.apps.abit.service.BatchProcessorService
|
||||
import ch.dissem.apps.abit.service.SimpleJob
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Exports
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.indeterminateProgressDialog
|
||||
import org.jetbrains.anko.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
|
||||
findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
|
||||
findPreference("export")?.onPreferenceClickListener = exportClickListener()
|
||||
findPreference("import")?.onPreferenceClickListener = importClickListener()
|
||||
findPreference("status")?.onPreferenceClickListener = statusClickListener()
|
||||
|
||||
connectivityChangeListener().let {
|
||||
findPreference("wifi_only")?.onPreferenceChangeListener = it
|
||||
findPreference("require_charging")?.onPreferenceChangeListener = it
|
||||
}
|
||||
|
||||
val emulateConversations = findPreference("emulate_conversations") as? SwitchPreferenceCompat
|
||||
val conversationInit = findPreference("emulate_conversations_initialize")
|
||||
|
||||
emulateConversations?.onPreferenceChangeListener = emulateConversationChangeListener(conversationInit)
|
||||
conversationInit?.onPreferenceClickListener = conversationInitClickListener()
|
||||
conversationInit?.isEnabled = emulateConversations?.isChecked ?: false
|
||||
}
|
||||
|
||||
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
|
||||
(activity as? MainActivity)?.let { activity ->
|
||||
val libsBuilder = LibsBuilder()
|
||||
.withActivityTitle(activity.getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutDescription(getString(R.string.about_app))
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(libsBuilder.supportFragment())
|
||||
} else {
|
||||
libsBuilder.start(activity)
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
cleanup.isEnabled = false
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
|
||||
|
||||
doAsync {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
bmc.internals.nodeRegistry.clear()
|
||||
bmc.cleanup()
|
||||
ctx.preferences.cleanupExportDirectory()
|
||||
|
||||
uiThread {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
R.string.cleanup_notification_end,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
cleanup.isEnabled = true
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun exportClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity ?: throw IllegalStateException("No context available")
|
||||
|
||||
ctx.indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
|
||||
doAsync {
|
||||
val exportDirectory = ctx.preferences.exportDirectory
|
||||
exportDirectory.mkdirs()
|
||||
val file = Exports.exportData(exportDirectory, ctx)
|
||||
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
|
||||
val intent = Intent(android.content.Intent.ACTION_SEND)
|
||||
intent.type = "application/zip"
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun importClickListener() = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "application/zip"
|
||||
|
||||
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun statusClickListener() = Preference.OnPreferenceClickListener {
|
||||
val activity = activity as MainActivity
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(StatusFragment())
|
||||
} else {
|
||||
activity.startActivity<StatusActivity>()
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val ctx = activity ?: throw IllegalStateException("No context available")
|
||||
when (requestCode) {
|
||||
WRITE_EXPORT_REQUEST_CODE -> ctx.preferences.cleanupExportDirectory()
|
||||
READ_IMPORT_REQUEST_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||
ctx.indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
|
||||
doAsync {
|
||||
Exports.importData(data.data!!, ctx)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(ctx: Context?) {
|
||||
super.onAttach(ctx)
|
||||
ctx?.let {
|
||||
if (it is MainActivity) {
|
||||
it.floatingActionButton?.hide()
|
||||
it.updateTitle(getString(R.string.settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
if (service is BatchProcessorService.BatchBinder) {
|
||||
val messageRepo = Singleton.getMessageRepository(service.service)
|
||||
val conversationService = Singleton.getConversationService(service.service)
|
||||
|
||||
service.process(
|
||||
SimpleJob<Plaintext>(
|
||||
messageRepo.count(),
|
||||
{ messageRepo.findNextLegacyMessages(it) },
|
||||
{ msg ->
|
||||
if (msg.encoding == Plaintext.Encoding.SIMPLE) {
|
||||
conversationService.getSubject(listOf(msg))?.let { subject ->
|
||||
msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(msg)
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
},
|
||||
R.drawable.ic_notification_batch,
|
||||
R.string.emulate_conversations_batch
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun conversationInitClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
ctx.bindService(Intent(ctx, BatchProcessorService::class.java), connection, Context.BIND_AUTO_CREATE)
|
||||
true
|
||||
}
|
||||
|
||||
private fun emulateConversationChangeListener(conversationInit: Preference?) =
|
||||
Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
conversationInit?.isEnabled = newValue as Boolean
|
||||
true
|
||||
}
|
||||
|
||||
private fun connectivityChangeListener() =
|
||||
Preference.OnPreferenceChangeListener { _, _ ->
|
||||
activity?.network?.scheduleNodeStart()
|
||||
true
|
||||
}
|
||||
|
||||
// The why-is-it-so-damn-hard-to-group-preferences section
|
||||
// FIXME: maybe this is once again necessary, maybe not. Test!
|
||||
// override fun getCallbackFragment(): Fragment = this
|
||||
//
|
||||
// override fun onPreferenceStartScreen(
|
||||
// preferenceFragmentCompat: PreferenceFragment,
|
||||
// preferenceScreen: PreferenceScreen
|
||||
// ): Boolean {
|
||||
// fragmentManager?.beginTransaction()?.let { ft ->
|
||||
// val fragment = SettingsFragment()
|
||||
// fragment.arguments = Bundle().apply {
|
||||
// putString(PreferenceFragment.ARG_PREFERENCE_ROOT, preferenceScreen.key)
|
||||
// }
|
||||
// ft.add(R.id.item_list, fragment, preferenceScreen.key)
|
||||
// ft.addToBackStack(preferenceScreen.key)
|
||||
// ft.commit()
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated(view, savedInstanceState)
|
||||
// context?.let { ctx -> view.setBackgroundColor(ContextCompat.getColor(ctx, R.color.contentBackground)) }
|
||||
// }
|
||||
// End of the why-is-it-so-damn-hard-to-group-preferences section
|
||||
// Afterthought: here it looks so simple: https://developer.android.com/guide/topics/ui/settings.html
|
||||
// Remind me, why do we need to use PreferenceFragmentCompat?
|
||||
|
||||
companion object {
|
||||
const val WRITE_EXPORT_REQUEST_CODE = 1
|
||||
const val READ_IMPORT_REQUEST_CODE = 2
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.activity_status.*
|
||||
|
||||
class StatusActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_status)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(this)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
content.text = status
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
|
||||
class StatusFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_status, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
view.findViewById<TextView>(R.id.content).text = status
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +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 androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
|
||||
|
||||
private val data = identities.asSequence().map { Selectable(it) }.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.select_identity_row, parent, false)
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val selectable = data[position]
|
||||
holder.data = selectable
|
||||
holder.checkbox.isChecked = selectable.selected
|
||||
holder.checkbox.text = selectable.data.toString()
|
||||
holder.address.text = selectable.data.address
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) {
|
||||
var data: Selectable<BitmessageAddress>? = null
|
||||
val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!!
|
||||
val address = v.findViewById<TextView>(R.id.address)!!
|
||||
|
||||
init {
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
data?.selected = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selected: List<BitmessageAddress>
|
||||
get() {
|
||||
return data.asSequence()
|
||||
.filter { it.selected }
|
||||
.mapTo(LinkedList()) { it.data }
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* An adapter for contacts. Can be filtered by alias or address.
|
||||
*/
|
||||
class ContactAdapter(
|
||||
ctx: Context,
|
||||
private val originalData: List<BitmessageAddress>,
|
||||
private val slim: Boolean = false
|
||||
) :
|
||||
BaseAdapter(), Filterable {
|
||||
private val inflater = LayoutInflater.from(ctx)
|
||||
private var data: List<BitmessageAddress> = originalData
|
||||
|
||||
override fun getCount() = data.size
|
||||
|
||||
override fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val viewHolder = if (convertView == null) {
|
||||
ViewHolder(
|
||||
inflater.inflate(
|
||||
if (slim) {
|
||||
R.layout.contact_row_slim
|
||||
} else {
|
||||
R.layout.contact_row
|
||||
},
|
||||
parent, false
|
||||
)
|
||||
)
|
||||
} else {
|
||||
convertView.tag as ViewHolder
|
||||
}
|
||||
val item = getItem(position)
|
||||
viewHolder.avatar.setImageDrawable(Identicon(item))
|
||||
viewHolder.name.text = item.toString()
|
||||
viewHolder.address?.text = item.address
|
||||
|
||||
return viewHolder.view
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter = ContactFilter()
|
||||
|
||||
private inner class ViewHolder(val view: View) {
|
||||
val avatar: ImageView = view.findViewById(R.id.avatar)
|
||||
val name: TextView = view.findViewById(R.id.name)
|
||||
val address: TextView? = view.findViewById(R.id.address)
|
||||
|
||||
init {
|
||||
view.tag = this
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class ContactFilter : Filter() {
|
||||
override fun performFiltering(prefix: CharSequence?): Filter.FilterResults {
|
||||
val results = Filter.FilterResults()
|
||||
|
||||
if (prefix?.isEmpty() == false) {
|
||||
val prefixString = prefix.toString().toLowerCase()
|
||||
|
||||
val newValues = ArrayList<BitmessageAddress>()
|
||||
|
||||
originalData
|
||||
.forEach { value ->
|
||||
value.alias?.toLowerCase()?.let { alias ->
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
} else {
|
||||
val words =
|
||||
alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
|
||||
for (word in words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
val address = value.address.toLowerCase()
|
||||
if (address.contains(prefixString)) {
|
||||
newValues.add(value)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
if (newValues.isEmpty()) {
|
||||
try {
|
||||
newValues.add(BitmessageAddress(prefix.toString()))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues
|
||||
results.count = newValues.size
|
||||
} else {
|
||||
results.values = originalData
|
||||
results.count = originalData.size
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data = results.values as List<BitmessageAddress>
|
||||
if (results.count > 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.text.util.Linkify
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.*
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Constants
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
|
||||
|
||||
class ConversationAdapter internal constructor(
|
||||
ctx: Context,
|
||||
private val parent: Fragment,
|
||||
conversation: Conversation,
|
||||
label: BehaviorSubject<Label>
|
||||
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
|
||||
|
||||
private val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
|
||||
private var filteredMessages = label.value?.let { l -> conversation.messages.filter { m -> m.labels.any { it == l } } } ?: emptyList()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ConversationAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val messageView = inflater.inflate(R.layout.item_message_detail, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(messageView, this.parent, messageRepo)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: ConversationAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = filteredMessages[position]
|
||||
|
||||
viewHolder.apply {
|
||||
item = message
|
||||
avatar.setImageDrawable(Identicon(message.from))
|
||||
sender.text = message.from.toString()
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(message.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
|
||||
recipient.text = message.to.toString()
|
||||
status.setImageResource(message.status.getDrawable())
|
||||
text.text = message.text
|
||||
|
||||
Linkify.addLinks(text, Linkify.WEB_URLS)
|
||||
Linkify.addLinks(text,
|
||||
Constants.BITMESSAGE_ADDRESS_PATTERN,
|
||||
Constants.BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
labelAdapter.labels = message.labels.toList()
|
||||
|
||||
// FIXME: I think that's not quite correct
|
||||
if (message.isUnread()) {
|
||||
Singleton.labeler.markAsRead(message)
|
||||
messageRepo.save(message)
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = filteredMessages.size
|
||||
|
||||
inner class ViewHolder(
|
||||
itemView: View,
|
||||
parent: Fragment,
|
||||
messageRepo: MessageRepository
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var item: Plaintext? = null
|
||||
val avatar = itemView.findViewById<ImageView>(R.id.avatar)!!
|
||||
val sender = itemView.findViewById<TextView>(R.id.sender)!!
|
||||
val recipient = itemView.findViewById<TextView>(R.id.recipient)!!
|
||||
val status = itemView.findViewById<ImageView>(R.id.status)!!
|
||||
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.apply {
|
||||
setOnClickListener { view ->
|
||||
PopupMenu(itemView.context, view).apply {
|
||||
|
||||
menuInflater.inflate(R.menu.message, menu)
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(parent, item)
|
||||
true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (MessageDetailFragment.isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
filteredMessages.indexOf(item).let { i ->
|
||||
filteredMessages -= item
|
||||
notifyItemRemoved(i)
|
||||
}
|
||||
MainActivity.apply {
|
||||
updateUnread()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
R.id.archive -> {
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
show()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
|
||||
linksClickable = true
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
val labelAdapter = LabelAdapter(itemView.context, emptySet())
|
||||
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
|
||||
adapter = labelAdapter
|
||||
layoutManager = GridLayoutManager(itemView.context, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.getColor
|
||||
import ch.dissem.apps.abit.util.getIcon
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
|
||||
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
var labels = labels.toList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.icon(label.getIcon())
|
||||
viewHolder.label.text = label.getText(ctx)
|
||||
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
|
||||
fun setBackground(@ColorInt color: Int) {
|
||||
itemView.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Selectable<out T>(val data: T) {
|
||||
var selected = false
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Haruki Hasegawa
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MultiIdenticon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
import ch.dissem.apps.abit.util.getString
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||
*
|
||||
* @author Christian Basler
|
||||
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
*/
|
||||
abstract class SwipeableAdapter<T, H>(val ctx: Context) :
|
||||
RecyclerView.Adapter<H>() where H : SwipeableAdapter.AbstractViewHolder {
|
||||
|
||||
protected val data = LinkedList<T>()
|
||||
var eventListener: EventListener? = null
|
||||
|
||||
protected var label: Label? = null
|
||||
var selectedPosition = -1
|
||||
set(value) {
|
||||
val oldPosition = field
|
||||
field = value
|
||||
notifyItemChanged(oldPosition)
|
||||
notifyItemChanged(value)
|
||||
}
|
||||
var activateOnItemClick: Boolean = false
|
||||
|
||||
protected val labelUnknown: String = ctx.getString(R.string.unknown)
|
||||
|
||||
open class AbstractViewHolder(v: View, adapter: SwipeableAdapter<*, *>) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
val container = v.findViewById<FrameLayout>(R.id.container)!!
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
|
||||
container.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: H, position: Int) {
|
||||
val item = data[position]
|
||||
|
||||
holder.apply {
|
||||
if (activateOnItemClick) {
|
||||
container.setBackgroundResource(
|
||||
if (position == selectedPosition)
|
||||
R.drawable.bg_item_selected_state
|
||||
else
|
||||
R.drawable.bg_item_normal_state
|
||||
)
|
||||
}
|
||||
|
||||
setData(holder, item)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun setData(holder: H, item: T)
|
||||
|
||||
fun add(item: T) {
|
||||
val index = data.size
|
||||
data.add(item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
|
||||
fun addFirst(item: T) {
|
||||
data.addFirst(item)
|
||||
notifyItemInserted(0)
|
||||
}
|
||||
|
||||
fun addAll(items: Collection<T>) {
|
||||
val index = data.size
|
||||
data.addAll(items)
|
||||
notifyItemRangeInserted(index, items.size)
|
||||
}
|
||||
|
||||
fun remove(item: T) {
|
||||
val itemId = getItemId(item)
|
||||
val index = data.indexOfFirst { getItemId(it) == itemId }
|
||||
if (index >= 0) {
|
||||
removeAt(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
data.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItemId(data[position])
|
||||
}
|
||||
|
||||
abstract fun getItemId(item: T): Long
|
||||
|
||||
abstract fun update(item: T)
|
||||
|
||||
fun clear(newLabel: Label?) {
|
||||
label = newLabel
|
||||
data.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
}
|
||||
|
||||
class SwipeableConversationAdapter(ctx: Context) : SwipeableAdapter<Conversation, SwipeableConversationAdapter.ViewHolder>(ctx) {
|
||||
override fun getItemId(item: Conversation) = item.id.leastSignificantBits
|
||||
|
||||
override fun update(item: Conversation) {
|
||||
val index = data.indexOfFirst { it.id == item.id }
|
||||
if (index >= 0) {
|
||||
data[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.conversation_row, parent, false)
|
||||
return ViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun setData(holder: ViewHolder, item: Conversation) {
|
||||
holder.apply {
|
||||
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||
|
||||
sender.text = item.participants.sortedBy {
|
||||
(it.alias?.let { 0 } ?: 1) + if (it.isChan) 2 else 0
|
||||
}.map { it.alias ?: labelUnknown }.distinct().joinToString()
|
||||
subject.text = prepareMessageExtract(item.subject)
|
||||
extract.text = prepareMessageExtract(item.extract)
|
||||
item.messages.count { it.labels.contains(label) }.let { size ->
|
||||
if (size <= 1) {
|
||||
count.text = ""
|
||||
} else {
|
||||
count.text = size.toString()
|
||||
}
|
||||
}
|
||||
if (item.hasUnread()) {
|
||||
sender.typeface = Typeface.DEFAULT_BOLD
|
||||
subject.typeface = Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
sender.typeface = Typeface.DEFAULT
|
||||
subject.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(v: View, adapter: SwipeableConversationAdapter) : AbstractViewHolder(v, adapter) {
|
||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||
val count = v.findViewById<TextView>(R.id.count)!!
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeableMessageAdapter(ctx: Context) : SwipeableAdapter<Plaintext, SwipeableMessageAdapter.ViewHolder>(ctx) {
|
||||
override fun getItemId(item: Plaintext) = item.id as Long
|
||||
|
||||
override fun update(item: Plaintext) {
|
||||
val index = data.indexOfFirst { it.id == item.id }
|
||||
if (index >= 0) {
|
||||
data[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.message_row, parent, false)
|
||||
return ViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun setData(holder: ViewHolder, item: Plaintext) {
|
||||
holder.apply {
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
status.setImageResource(item.status.getDrawable())
|
||||
status.contentDescription = holder.status.context.getString(item.status.getString())
|
||||
|
||||
sender.text = item.from.toString()
|
||||
subject.text = prepareMessageExtract(item.subject)
|
||||
extract.text = prepareMessageExtract(item.text)
|
||||
if (item.isUnread()) {
|
||||
sender.typeface = Typeface.DEFAULT_BOLD
|
||||
subject.typeface = Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
sender.typeface = Typeface.DEFAULT
|
||||
subject.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(v: View, adapter: SwipeableMessageAdapter) : AbstractViewHolder(v, adapter) {
|
||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeToDeleteCallback(ctx: Context, swipeDirs: Int, private val eventListener: EventListener) : ItemTouchHelper.SimpleCallback(0, swipeDirs) {
|
||||
|
||||
private val backgroundLeft = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_left)!!
|
||||
private val backgroundRight = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_right)!!
|
||||
private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
|
||||
|
||||
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
/**
|
||||
* To disable "swipe" for specific item return 0 here.
|
||||
* For example:
|
||||
* if (viewHolder?.itemViewType == YourAdapter.SOME_TYPE) return 0
|
||||
* if (viewHolder?.adapterPosition == 0) return 0
|
||||
*/
|
||||
if (viewHolder.adapterPosition == 10) return 0
|
||||
return super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
|
||||
) {
|
||||
|
||||
val itemView = viewHolder.itemView
|
||||
val isCanceled = dX == 0f && !isCurrentlyActive
|
||||
|
||||
if (isCanceled) {
|
||||
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
return
|
||||
}
|
||||
|
||||
if (dX < 0) {
|
||||
backgroundLeft.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
|
||||
backgroundLeft.draw(c)
|
||||
} else {
|
||||
backgroundRight.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
|
||||
backgroundRight.draw(c)
|
||||
}
|
||||
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
}
|
||||
|
||||
private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
|
||||
c?.drawRect(left, top, right, bottom, clearPaint)
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> eventListener.onItemDeleted(viewHolder.adapterPosition)
|
||||
ItemTouchHelper.RIGHT -> eventListener.onItemArchived(viewHolder.adapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onItemDeleted(position: Int)
|
||||
|
||||
fun onItemArchived(position: Int)
|
||||
|
||||
fun onItemSelected(position: Int)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import ch.dissem.apps.abit.ImportIdentityActivity
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class AddIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private var parent: ViewGroup? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_identity)
|
||||
parent = container
|
||||
return inflater.inflate(R.layout.dialog_add_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
val ctx = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
|
||||
when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.create_identity -> {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.import_identity -> ctx.startActivity<ImportIdentityActivity>()
|
||||
R.id.add_chan -> addChanDialog()
|
||||
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
private fun addChanDialog() {
|
||||
val activity = activity ?: throw IllegalStateException("No activity available")
|
||||
val ctx = activity.baseContext ?: throw IllegalStateException("No context available")
|
||||
val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent)
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.add_chan)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val passphrase = dialogView.findViewById<TextView>(R.id.passphrase)
|
||||
Toast.makeText(ctx, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val pass = passphrase.text.toString()
|
||||
doAsync {
|
||||
val chan = bmc.createChan(pass)
|
||||
chan.alias = pass
|
||||
bmc.addresses.save(chan)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_chan_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply { addIdentityEntry(chan) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class DeterministicIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_deterministic_address)
|
||||
return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(dialogView, savedInstanceState)
|
||||
ok.setOnClickListener {
|
||||
dismiss()
|
||||
val context = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
val passphraseText = passphrase.text.toString()
|
||||
|
||||
Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identities = bmc.createDeterministicAddresses(
|
||||
passphraseText,
|
||||
number_of_identities.text.toString().toInt(),
|
||||
Pubkey.LATEST_VERSION,
|
||||
1L,
|
||||
shorter.isChecked
|
||||
)
|
||||
for ((i, identity) in identities.withIndex()) {
|
||||
if (identities.size == 1) {
|
||||
identity.alias = label.text.toString()
|
||||
} else {
|
||||
identity.alias = "${label.text} (${i + 1})"
|
||||
}
|
||||
bmc.addresses.save(identity)
|
||||
}
|
||||
uiThread {
|
||||
val messageRes = if (identities.size == 1) {
|
||||
R.string.toast_identity_created
|
||||
} else {
|
||||
R.string.toast_identities_created
|
||||
}
|
||||
Toast.makeText(context,
|
||||
messageRes,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
identities.forEach { identity ->
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import kotlinx.android.synthetic.main.dialog_full_node.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class FullNodeDialogActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.dialog_full_node)
|
||||
ok.setOnClickListener {
|
||||
preferences.wifiOnly = false
|
||||
network.scheduleNodeStart()
|
||||
finish()
|
||||
}
|
||||
dismiss.setOnClickListener {
|
||||
network.scheduleNodeStart()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
|
||||
import kotlinx.android.synthetic.main.dialog_select_message_encoding.*
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class SelectEncodingDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var encoding: Plaintext.Encoding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE
|
||||
dialog.setTitle(R.string.select_encoding_title)
|
||||
return inflater.inflate(R.layout.dialog_select_message_encoding, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
when (encoding) {
|
||||
SIMPLE -> radioGroup.check(R.id.simple)
|
||||
EXTENDED -> radioGroup.check(R.id.extended)
|
||||
else -> LOG.warn("Unexpected encoding: $encoding")
|
||||
}
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
encoding = when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.extended -> EXTENDED
|
||||
R.id.simple -> SIMPLE
|
||||
else -> {
|
||||
dismiss()
|
||||
return@OnClickListener
|
||||
}
|
||||
}
|
||||
val result = Intent()
|
||||
result.putExtra(EXTRA_ENCODING, encoding)
|
||||
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result)
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener {
|
||||
|
||||
override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
if (current) {
|
||||
// Show QR code in modal dialog
|
||||
val dialog = Dialog(ctx)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
val imageView = ImageView(ctx)
|
||||
imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
|
||||
imageView.setOnClickListener { dialog.dismiss() }
|
||||
dialog.addContentView(
|
||||
imageView,
|
||||
RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
val display = window.windowManager.defaultDisplay
|
||||
val size = Point()
|
||||
display.getSize(size)
|
||||
val dim = if (size.x < size.y) size.x else size.y
|
||||
|
||||
val lp = WindowManager.LayoutParams()
|
||||
lp.copyFrom(window.attributes)
|
||||
lp.width = dim
|
||||
lp.height = dim
|
||||
|
||||
window.attributes = lp
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import ch.dissem.apps.abit.*
|
||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
class ProfileSelectionListener(
|
||||
private val ctx: Context,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : AccountHeader.OnAccountHeaderListener {
|
||||
|
||||
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
when (profile.identifier.toInt()) {
|
||||
MainActivity.ADD_IDENTITY -> addIdentityDialog()
|
||||
MainActivity.MANAGE_IDENTITY -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show()
|
||||
} else {
|
||||
val show = Intent(ctx, AddressDetailActivity::class.java)
|
||||
show.putExtra(AddressDetailFragment.ARG_ITEM, identity)
|
||||
ctx.startActivity(show)
|
||||
}
|
||||
}
|
||||
else -> if (profile is ProfileDrawerItem) {
|
||||
val tag = profile.tag
|
||||
if (tag is BitmessageAddress) {
|
||||
Singleton.setIdentity(tag)
|
||||
MainActivity.apply {
|
||||
updateUnread()
|
||||
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
if (itemList is ListHolder<*>) {
|
||||
itemList.reloadList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// false if it should close the drawer
|
||||
return false
|
||||
}
|
||||
|
||||
private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
}
|
||||
@@ -1,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.listener
|
||||
|
||||
/**
|
||||
* A callback interface that all activities containing this fragment must
|
||||
* implement. This mechanism allows activities to be notified of item
|
||||
* selections.
|
||||
*/
|
||||
interface ListSelectionListener<in T> {
|
||||
/**
|
||||
* Callback for when an item has been selected.
|
||||
*/
|
||||
fun onItemSelected(item: T)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
import android.content.Context
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.notification.NewMessageNotification
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||
*
|
||||
*
|
||||
* Should show a notification when the app isn't running, but update the message list when it is.
|
||||
* Also,
|
||||
* notifications should be combined.
|
||||
*
|
||||
*/
|
||||
class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
|
||||
override fun setContext(ctx: BitmessageContext) {
|
||||
messageRepo = ctx.messages
|
||||
conversationService = ConversationService(messageRepo)
|
||||
}
|
||||
|
||||
private val unacknowledged = LinkedList<Plaintext>()
|
||||
private var numberOfUnacknowledgedMessages = 0
|
||||
private val notification = NewMessageNotification(ctx)
|
||||
private val pool = Executors.newSingleThreadExecutor()
|
||||
private lateinit var messageRepo: MessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
|
||||
init {
|
||||
emulateConversations = ctx.preferences.emulateConversations
|
||||
}
|
||||
|
||||
override fun receive(plaintext: Plaintext) {
|
||||
pool.submit {
|
||||
updateConversation(plaintext)
|
||||
unacknowledged.addFirst(plaintext)
|
||||
numberOfUnacknowledgedMessages++
|
||||
if (unacknowledged.size > 5) {
|
||||
unacknowledged.removeLast()
|
||||
}
|
||||
if (numberOfUnacknowledgedMessages == 1) {
|
||||
notification.singleNotification(plaintext)
|
||||
} else {
|
||||
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages)
|
||||
}
|
||||
notification.show()
|
||||
|
||||
// If MainActivity is shown, update the sidebar badges
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
}
|
||||
|
||||
fun resetNotification() {
|
||||
pool.submit {
|
||||
notification.hide()
|
||||
unacknowledged.clear()
|
||||
numberOfUnacknowledgedMessages = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateConversation(plaintext: Plaintext) {
|
||||
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
|
||||
conversationService.getSubject(listOf(plaintext))?.let { subject ->
|
||||
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var emulateConversations = false
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import org.jetbrains.anko.notificationManager
|
||||
|
||||
/**
|
||||
* Some base class to create and handle notifications.
|
||||
*/
|
||||
abstract class AbstractNotification(ctx: Context) {
|
||||
protected val ctx = ctx.applicationContext!!
|
||||
private val manager = ctx.notificationManager
|
||||
var notification: Notification? = null
|
||||
protected set
|
||||
protected var showing = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return an id unique to this notification class
|
||||
*/
|
||||
protected abstract val notificationId: Int
|
||||
|
||||
open fun show() {
|
||||
manager.notify(notificationId, notification)
|
||||
showing = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
showing = false
|
||||
manager.cancel(notificationId)
|
||||
}
|
||||
|
||||
protected fun initChannel(channelId: String, @ColorRes color: Int = R.color.colorPrimary) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
ctx.getText(R.string.app_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
lightColor = ContextCompat.getColor(ctx, color)
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
internal const val ONGOING_CHANNEL_ID = "abit.ongoing"
|
||||
internal const val MESSAGE_CHANNEL_ID = "abit.message"
|
||||
internal const val ERROR_CHANNEL_ID = "abit.error"
|
||||
|
||||
init {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Job
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class BatchNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(job: Job): BatchNotification {
|
||||
|
||||
builder.setContentTitle(ctx.getString(job.description))
|
||||
.setSmallIcon(job.icon)
|
||||
.setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0)
|
||||
|
||||
notification = builder.build()
|
||||
show()
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
|
||||
/**
|
||||
* Easily create notifications with error messages. Use carefully, users probably won't like them.
|
||||
* (But they are useful during development/testing)
|
||||
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ERROR_CHANNEL_ID)
|
||||
.setContentTitle(ctx.getString(R.string.app_name))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
|
||||
init {
|
||||
initChannel(ERROR_CHANNEL_ID, R.color.colorPrimaryDark)
|
||||
}
|
||||
|
||||
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = ERROR_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
const val ERROR_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.NodeStartupService
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Shows the network status (as long as the client is connected as a full node)
|
||||
*/
|
||||
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
val showAppIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
|
||||
builder
|
||||
.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
|
||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
.setContentIntent(pendingIntent)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun update(): Boolean {
|
||||
val running = NodeStartupService.isRunning
|
||||
builder.setOngoing(running)
|
||||
val connections = NodeStartupService.status.getProperty("network", "connections")
|
||||
if (!running) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node_disconnected)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
|
||||
} else if (connections == null || connections.properties.isEmpty()) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node_connecting)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
} else {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_full_node)
|
||||
val info = StringBuilder()
|
||||
for (stream in connections.properties) {
|
||||
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
|
||||
val nodeCount = stream.getProperty("nodes")!!.value as Int?
|
||||
if (nodeCount == 1) {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_1,
|
||||
streamNumber
|
||||
)
|
||||
)
|
||||
} else {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_n,
|
||||
streamNumber, nodeCount
|
||||
)
|
||||
)
|
||||
}
|
||||
info.append('\n')
|
||||
}
|
||||
builder.setContentText(info)
|
||||
}
|
||||
builder.mActions.clear()
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
if (running) {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
} else {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_start,
|
||||
ctx.getString(R.string.full_node_restart),
|
||||
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
}
|
||||
notification = builder.build()
|
||||
return running
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
|
||||
if (!update()) {
|
||||
cancel()
|
||||
}
|
||||
super@NetworkNotification.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun showShutdown() {
|
||||
timer?.cancel()
|
||||
update()
|
||||
super.show()
|
||||
}
|
||||
|
||||
override val notificationId = NETWORK_NOTIFICATION_ID
|
||||
|
||||
fun connecting() {
|
||||
builder.setOngoing(true)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.mActions.clear()
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NETWORK_NOTIFICATION_ID = 2
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.BigTextStyle
|
||||
import androidx.core.app.NotificationCompat.InboxStyle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
|
||||
import ch.dissem.apps.abit.util.toBitmap
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
init {
|
||||
initChannel(MESSAGE_CHANNEL_ID, R.color.colorPrimary)
|
||||
}
|
||||
|
||||
fun singleNotification(plaintext: Plaintext): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text)
|
||||
plaintext.subject?.let { subject ->
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(Identicon(plaintext.from).toBitmap(192))
|
||||
.setContentTitle(plaintext.from.toString())
|
||||
.setContentText(plaintext.subject)
|
||||
.setStyle(BigTextStyle().bigText(bigText))
|
||||
.setContentInfo("Info")
|
||||
|
||||
builder.setContentIntent(
|
||||
createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_reply, ctx.getString(R.string.reply),
|
||||
createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_delete, ctx.getString(R.string.delete),
|
||||
createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)
|
||||
)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun createActivityIntent(action: String, message: Plaintext): PendingIntent {
|
||||
val intent = Intent(ctx, MainActivity::class.java).putExtra(action, message)
|
||||
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
private fun createServiceIntent(
|
||||
ctx: Context,
|
||||
action: String,
|
||||
message: Plaintext
|
||||
): PendingIntent {
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(action, message)
|
||||
return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unacknowledged will be accessed from different threads, so make sure wherever it's
|
||||
* * accessed it will be in a `synchronized(unacknowledged)
|
||||
* * {}` block
|
||||
*/
|
||||
fun multiNotification(
|
||||
unacknowledged: Collection<Plaintext>,
|
||||
numberOfUnacknowledgedMessages: Int
|
||||
): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
|
||||
.setContentText(ctx.getString(R.string.app_name))
|
||||
|
||||
val inboxStyle = InboxStyle()
|
||||
|
||||
synchronized(unacknowledged) {
|
||||
for (msg in unacknowledged) {
|
||||
val sb = SpannableString(msg.from.toString() + " " + msg.subject)
|
||||
sb.setSpan(
|
||||
SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
inboxStyle.addLine(sb)
|
||||
}
|
||||
}
|
||||
builder.setStyle(inboxStyle)
|
||||
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.action = MainActivity.ACTION_SHOW_INBOX
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
|
||||
builder.setContentIntent(pendingIntent)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
private const val NEW_MESSAGE_NOTIFICATION_ID = 1
|
||||
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService
|
||||
import ch.dissem.apps.abit.util.PowStats
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_notification_proof_of_work)
|
||||
.setContentTitle(ctx.getString(R.string.proof_of_work_title))
|
||||
private var startTime = 0L
|
||||
private var progress = 0
|
||||
private var progressMax = 0
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
update(0)
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(numberOfItems: Int): ProofOfWorkNotification {
|
||||
|
||||
val showMessageIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentText(if (numberOfItems == 0)
|
||||
ctx.getString(R.string.proof_of_work_text_0)
|
||||
else
|
||||
ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun start(item: ProofOfWorkService.PowItem) {
|
||||
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
|
||||
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
|
||||
startTime = System.currentTimeMillis()
|
||||
progress = 0
|
||||
progressMax = delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 2000, period = 2000){
|
||||
val elapsedTime = System.currentTimeMillis() - startTime
|
||||
progress = elapsedTime.toInt()
|
||||
progressMax = progress + delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished() {
|
||||
timer?.cancel()
|
||||
progress = 0
|
||||
progressMax = 0
|
||||
if (showing) {
|
||||
builder.setProgress(0, 0, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.AddressRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [AddressRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
|
||||
|
||||
override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag)
|
||||
|
||||
override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag)
|
||||
|
||||
private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? {
|
||||
for (address in find(where)) {
|
||||
if (address.version > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.tag)) return address
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.ripe)) return address
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIdentities() = find("private_key IS NOT NULL")
|
||||
|
||||
override fun getChans() = find("chan = '1'")
|
||||
|
||||
override fun getSubscriptions() = find("subscribed = '1'")
|
||||
|
||||
override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) {
|
||||
find("subscribed = '1' AND version > 3")
|
||||
} else {
|
||||
find("subscribed = '1' AND version <= 3")
|
||||
}
|
||||
|
||||
override fun getContacts() = find("private_key IS NULL OR chan = '1'")
|
||||
|
||||
/**
|
||||
* Returns the contacts in the following order:
|
||||
*
|
||||
* * Subscribed addresses come first
|
||||
* * Addresses with aliases (alphabetically)
|
||||
* * Addresses without aliases are omitted
|
||||
*
|
||||
*
|
||||
* @return the ordered list of ids (address strings)
|
||||
*/
|
||||
fun getContactIds(): List<String> = findIds(
|
||||
"($COLUMN_PRIVATE_KEY IS NULL OR $COLUMN_CHAN = '1') AND $COLUMN_ALIAS IS NOT NULL",
|
||||
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS, $COLUMN_ADDRESS"
|
||||
)
|
||||
|
||||
private fun findIds(where: String, orderBy: String): List<String> {
|
||||
val result = LinkedList<String>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
orderBy
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS)))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun find(where: String): List<BitmessageAddress> {
|
||||
val result = LinkedList<BitmessageAddress>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getAddress(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getAddress(c: Cursor): BitmessageAddress {
|
||||
|
||||
fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let {
|
||||
BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it)))
|
||||
}
|
||||
|
||||
fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address ->
|
||||
c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes ->
|
||||
Factory.readPubkey(
|
||||
version = address.version, stream = address.stream,
|
||||
input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size,
|
||||
encrypted = false
|
||||
).let {
|
||||
address.pubkey = if (address.version == 4L && it is V3Pubkey) {
|
||||
V4Pubkey(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (getIdentity(c) ?: getContact(c)).apply {
|
||||
alias = c.getString(c.getColumnIndex(COLUMN_ALIAS))
|
||||
isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1
|
||||
isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(address: BitmessageAddress) = if (exists(address)) {
|
||||
update(address)
|
||||
} else {
|
||||
insert(address)
|
||||
}
|
||||
|
||||
private fun exists(address: BitmessageAddress): Boolean {
|
||||
sql.readableDatabase.rawQuery(
|
||||
"SELECT COUNT(*) FROM Address WHERE address=?",
|
||||
arrayOf(address.address)
|
||||
).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address)
|
||||
|
||||
val update = sql.writableDatabase.update(TABLE_NAME, values, "address=?", arrayOf(address.address))
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address).apply {
|
||||
put(COLUMN_ADDRESS, address.address)
|
||||
put(COLUMN_VERSION, address.version)
|
||||
put(COLUMN_CHAN, address.isChan)
|
||||
}
|
||||
|
||||
val insert = sql.writableDatabase.insert(TABLE_NAME, null, values)
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentValues(address: BitmessageAddress) = ContentValues().apply {
|
||||
address.alias?.let { put(COLUMN_ALIAS, it) }
|
||||
address.pubkey?.let { pubkey ->
|
||||
val out = ByteArrayOutputStream()
|
||||
pubkey.writer().writeUnencrypted(out)
|
||||
put(COLUMN_PUBLIC_KEY, out.toByteArray())
|
||||
}
|
||||
address.privateKey?.let { put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
|
||||
if (address.isChan) {
|
||||
put(COLUMN_CHAN, true)
|
||||
}
|
||||
put(COLUMN_SUBSCRIBED, address.isSubscribed)
|
||||
}
|
||||
|
||||
override fun remove(address: BitmessageAddress) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
|
||||
}
|
||||
|
||||
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Address"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_ALIAS = "alias"
|
||||
private const val COLUMN_PUBLIC_KEY = "public_key"
|
||||
private const val COLUMN_PRIVATE_KEY = "private_key"
|
||||
private const val COLUMN_SUBSCRIBED = "subscribed"
|
||||
private const val COLUMN_CHAN = "chan"
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.Inventory
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* [Inventory] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidInventory(private val sql: SqlHelper) : Inventory {
|
||||
|
||||
private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>()
|
||||
|
||||
override fun getInventory(vararg streams: Long): List<InventoryVector> {
|
||||
val result = LinkedList<InventoryVector>()
|
||||
val now = now
|
||||
for (stream in streams) {
|
||||
for ((key, value) in getCache(stream)) {
|
||||
if (value > now) {
|
||||
result.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
|
||||
cache[stream] = result
|
||||
|
||||
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = $stream", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH))
|
||||
val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES))
|
||||
InventoryVector.fromHash(blob)?.let { result.put(it, expires) }
|
||||
}
|
||||
}
|
||||
LOG.info("Stream #$stream inventory size: ${result.size}")
|
||||
return result
|
||||
}
|
||||
return cache[stream] ?: synchronized(cache) {
|
||||
return@synchronized cache[stream] ?: addToCache(stream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys }
|
||||
|
||||
override fun getObject(vector: InventoryVector): ObjectMessage? {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'$vector'", null, null, null, null
|
||||
).use { c ->
|
||||
if (!c.moveToFirst()) {
|
||||
LOG.info("Object requested that we don't have. IV: {}", vector)
|
||||
return null
|
||||
}
|
||||
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
val where = StringBuilder("1=1")
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream)
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version)
|
||||
}
|
||||
if (types.isNotEmpty()) {
|
||||
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
|
||||
}
|
||||
|
||||
val result = LinkedList<ObjectMessage>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(), null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun storeObject(objectMessage: ObjectMessage) {
|
||||
val iv = objectMessage.inventoryVector
|
||||
|
||||
if (getCache(objectMessage.stream).containsKey(iv))
|
||||
return
|
||||
|
||||
LOG.trace("Storing object {}", iv)
|
||||
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_HASH, objectMessage.inventoryVector.hash)
|
||||
put(COLUMN_STREAM, objectMessage.stream)
|
||||
put(COLUMN_EXPIRES, objectMessage.expiresTime)
|
||||
put(COLUMN_DATA, Encode.bytes(objectMessage))
|
||||
put(COLUMN_TYPE, objectMessage.type)
|
||||
put(COLUMN_VERSION, objectMessage.version)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
|
||||
getCache(objectMessage.stream)[iv] = objectMessage.expiresTime
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector)
|
||||
|
||||
override fun cleanup() {
|
||||
val fiveMinutesAgo = now - 5 * MINUTE
|
||||
sql.writableDatabase.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
|
||||
|
||||
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Inventory"
|
||||
private const val COLUMN_HASH = "hash"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_EXPIRES = "expires"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractLabelRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) :
|
||||
AbstractLabelRepository() {
|
||||
|
||||
override fun find(where: String): List<Label> {
|
||||
val result = LinkedList<Label>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
COLUMN_ORDER
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getLabel(c, context))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun save(label: Label) {
|
||||
val db = sql.writableDatabase
|
||||
if (label.id != null) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
|
||||
} else {
|
||||
db.transaction {
|
||||
val exists = DatabaseUtils.queryNumEntries(
|
||||
db,
|
||||
TABLE_NAME,
|
||||
"label=?",
|
||||
arrayOf(label.toString())
|
||||
) > 0
|
||||
|
||||
if (exists) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString()))
|
||||
} else {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.insertOrThrow(TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun findLabels(msgId: Any) =
|
||||
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
|
||||
companion object {
|
||||
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
|
||||
|
||||
private const val TABLE_NAME = "Label"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_LABEL = "label"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_COLOR = "color"
|
||||
private const val COLUMN_ORDER = "ord"
|
||||
|
||||
internal fun getLabel(c: Cursor, context: Context): Label {
|
||||
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
|
||||
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
|
||||
val text: String? = type?.getText(null, context)
|
||||
val label = Label(
|
||||
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(COLUMN_COLOR))
|
||||
)
|
||||
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import ch.dissem.apps.abit.util.UuidUtils.asUuid
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractMessageRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
|
||||
|
||||
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
|
||||
if (label === LABEL_ARCHIVE || label === null) {
|
||||
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
|
||||
} else {
|
||||
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
|
||||
}
|
||||
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) =
|
||||
if (label === LABEL_ARCHIVE) {
|
||||
super.findMessages(null as Label?, offset, limit)
|
||||
} else {
|
||||
super.findMessages(label, offset, limit)
|
||||
}
|
||||
|
||||
fun count() = DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
null,
|
||||
null
|
||||
).toInt()
|
||||
|
||||
private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
|
||||
if (separateIdentities) {
|
||||
val identity = prefs.currentIdentity
|
||||
return if (prefs.separateIdentities && identity != null) {
|
||||
"AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
|
||||
} else {
|
||||
"" to emptyArray()
|
||||
}
|
||||
} else {
|
||||
return "" to emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun countUnread(label: Label?) = countUnread(label, false)
|
||||
|
||||
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
|
||||
when {
|
||||
label === LABEL_ARCHIVE -> 0
|
||||
label == null -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
|
||||
selectIdentityQuery,
|
||||
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
|
||||
).toInt()
|
||||
else -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
|
||||
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
|
||||
selectIdentityQuery,
|
||||
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
|
||||
|
||||
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
|
||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
||||
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
|
||||
|
||||
val where = when {
|
||||
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
|
||||
label == null -> if (selectIdentityQuery.isNotBlank()) {
|
||||
"type = 'BROADCAST' OR recipient=? OR sender=?"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
|
||||
}
|
||||
val result = LinkedList<UUID>()
|
||||
sql.readableDatabase.query(
|
||||
true,
|
||||
TABLE_NAME,
|
||||
projection,
|
||||
where,
|
||||
selectIdentityArgs, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
||||
result.add(asUuid(uuidBytes))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
|
||||
val inventoryVector = message.inventoryVector
|
||||
if (inventoryVector == null || message.parents.isEmpty()) {
|
||||
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
|
||||
return
|
||||
}
|
||||
val childIV = inventoryVector.hash
|
||||
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
|
||||
|
||||
// save new parents
|
||||
var order = 0
|
||||
val values = ContentValues()
|
||||
for (parentIV in message.parents) {
|
||||
getMessage(parentIV)?.let { parent ->
|
||||
mergeConversations(db, parent.conversationId, message.conversationId)
|
||||
order++
|
||||
values.put("parent", parentIV.hash)
|
||||
values.put("child", childIV)
|
||||
values.put("pos", order)
|
||||
values.put("conversation", UuidUtils.asBytes(message.conversationId))
|
||||
db.insertOrThrow(PARENTS_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every occurrence of the source conversation ID with the target ID
|
||||
|
||||
* @param db is used to keep everything within one transaction
|
||||
* *
|
||||
* @param source ID of the conversation to be merged
|
||||
* *
|
||||
* @param target ID of the merge target
|
||||
*/
|
||||
private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
|
||||
val values = ContentValues()
|
||||
values.put("conversation", UuidUtils.asBytes(target))
|
||||
val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
|
||||
db.update(TABLE_NAME, values, where, null)
|
||||
db.update(PARENTS_TABLE_NAME, values, where, null)
|
||||
}
|
||||
|
||||
override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
|
||||
|
||||
private fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
|
||||
val result = LinkedList<Plaintext>()
|
||||
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$where $selectIdentityQuery", selectIdentityArgs, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature(
|
||||
Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))),
|
||||
ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA)))
|
||||
).build {
|
||||
id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV)))
|
||||
c.getString(c.getColumnIndex(COLUMN_SENDER))?.let {
|
||||
from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let {
|
||||
to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))
|
||||
sent = c.getLong(c.getColumnIndex(COLUMN_SENT))
|
||||
received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED))
|
||||
status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))
|
||||
ttl = c.getLong(c.getColumnIndex(COLUMN_TTL))
|
||||
retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES))
|
||||
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
|
||||
if (!c.isNull(nextTryColumn)) {
|
||||
nextTry = c.getLong(nextTryColumn)
|
||||
}
|
||||
conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))
|
||||
labels = findLabels(id!!)
|
||||
}
|
||||
|
||||
private fun findLabels(msgId: Any) =
|
||||
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||
|
||||
override fun save(message: Plaintext) {
|
||||
saveContactIfNecessary(message.from)
|
||||
saveContactIfNecessary(message.to)
|
||||
val db = sql.writableDatabase
|
||||
db.transaction {
|
||||
// save message
|
||||
if (message.id == null) {
|
||||
insert(db, message)
|
||||
} else {
|
||||
update(db, message)
|
||||
}
|
||||
|
||||
updateParents(db, message)
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
|
||||
|
||||
// save labels
|
||||
val values = ContentValues()
|
||||
for (label in message.labels) {
|
||||
values.put(JT_COLUMN_LABEL, label.id as Long?)
|
||||
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValues(message: Plaintext) = ContentValues(14).apply {
|
||||
put(COLUMN_IV, message.inventoryVector?.hash)
|
||||
put(COLUMN_TYPE, message.type.name)
|
||||
put(COLUMN_SENDER, message.from.address)
|
||||
put(COLUMN_RECIPIENT, message.to?.address)
|
||||
put(COLUMN_DATA, Encode.bytes(message))
|
||||
put(COLUMN_ACK_DATA, message.ackData)
|
||||
put(COLUMN_SENT, message.sent)
|
||||
put(COLUMN_RECEIVED, message.received)
|
||||
put(COLUMN_STATUS, message.status.name)
|
||||
put(COLUMN_INITIAL_HASH, message.initialHash)
|
||||
put(COLUMN_TTL, message.ttl)
|
||||
put(COLUMN_RETRIES, message.retries)
|
||||
put(COLUMN_NEXT_TRY, message.nextTry)
|
||||
put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
|
||||
}
|
||||
|
||||
private fun insert(db: SQLiteDatabase, message: Plaintext) {
|
||||
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
|
||||
message.id = id
|
||||
}
|
||||
|
||||
private fun update(db: SQLiteDatabase, message: Plaintext) {
|
||||
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
override fun remove(message: Plaintext) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
|
||||
val result = mutableListOf<Plaintext>()
|
||||
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
|
||||
"$COLUMN_ID ASC",
|
||||
"$limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "Message"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_IV = "iv"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_SENDER = "sender"
|
||||
private const val COLUMN_RECIPIENT = "recipient"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_ACK_DATA = "ack_data"
|
||||
private const val COLUMN_SENT = "sent"
|
||||
private const val COLUMN_RECEIVED = "received"
|
||||
private const val COLUMN_STATUS = "status"
|
||||
private const val COLUMN_TTL = "ttl"
|
||||
private const val COLUMN_RETRIES = "retries"
|
||||
private const val COLUMN_NEXT_TRY = "next_try"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_CONVERSATION = "conversation"
|
||||
|
||||
private const val PARENTS_TABLE_NAME = "Message_Parent"
|
||||
|
||||
private const val JOIN_TABLE_NAME = "Message_Label"
|
||||
private const val JT_COLUMN_MESSAGE = "message_id"
|
||||
private const val JT_COLUMN_LABEL = "label_id"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.database.sqlite.SQLiteDoneException
|
||||
import android.database.sqlite.SQLiteStatement
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.ports.NodeRegistry
|
||||
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
|
||||
import ch.dissem.bitmessage.utils.Collections
|
||||
import ch.dissem.bitmessage.utils.SqlStrings
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import ch.dissem.bitmessage.utils.UnixTime.DAY
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import ch.dissem.bitmessage.utils.max
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import kotlin.concurrent.getOrSet
|
||||
|
||||
const val MAX_ENTRY_AGE = 7 * DAY
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
|
||||
|
||||
private val loadExistingStatement = ThreadLocal<SQLiteStatement>()
|
||||
private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap()
|
||||
get() {
|
||||
if (field.isEmpty())
|
||||
field = loadStableNodes()
|
||||
return field
|
||||
}
|
||||
|
||||
init {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
private fun cleanUp() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString()))
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
private fun loadExistingTime(node: NetworkAddress): Long? {
|
||||
val statement: SQLiteStatement = loadExistingStatement.getOrSet {
|
||||
sql.writableDatabase.compileStatement(
|
||||
"SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?"
|
||||
)
|
||||
}
|
||||
statement.bindLong(1, node.stream)
|
||||
statement.bindBlob(2, node.IPv6)
|
||||
statement.bindLong(3, node.port.toLong())
|
||||
return try {
|
||||
statement.simpleQueryForLong()
|
||||
} catch (e: SQLiteDoneException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> {
|
||||
val result = LinkedList<NetworkAddress>()
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME),
|
||||
"stream IN (?)",
|
||||
arrayOf(SqlStrings.join(*streams)), null, null,
|
||||
"time DESC",
|
||||
limit.toString()
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(NetworkAddress(
|
||||
time = c.getLong(c.getColumnIndex(COLUMN_TIME)),
|
||||
stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)),
|
||||
services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)),
|
||||
IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)),
|
||||
port = c.getInt(c.getColumnIndex(COLUMN_PORT))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
streams
|
||||
.asSequence()
|
||||
.mapNotNull { stableNodes[it] }
|
||||
.filterNot { it.isEmpty() }
|
||||
.mapTo(result) { Collections.selectRandom(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun offerAddresses(nodes: List<NetworkAddress>) {
|
||||
sql.writableDatabase.transaction {
|
||||
cleanUp()
|
||||
nodes
|
||||
.filter {
|
||||
// Don't accept nodes from the future, it might be a trap
|
||||
it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE
|
||||
}
|
||||
.forEach { node ->
|
||||
synchronized(this) {
|
||||
val existing = loadExistingTime(node)
|
||||
if (existing == null) {
|
||||
insert(node)
|
||||
} else if (node.time > existing) {
|
||||
update(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(node: NetworkAddress) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_STREAM, node.stream)
|
||||
put(COLUMN_ADDRESS, node.IPv6)
|
||||
put(COLUMN_PORT, node.port)
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME,
|
||||
if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(node: NetworkAddress) {
|
||||
try {
|
||||
val time = if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME, max(node.time, time))
|
||||
}
|
||||
|
||||
sql.writableDatabase.update(
|
||||
TABLE_NAME,
|
||||
values,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(node: NetworkAddress) {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"time<${UnixTime.now - 8 * DAY}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Node"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_PORT = "port"
|
||||
private const val COLUMN_SERVICES = "services"
|
||||
private const val COLUMN_TIME = "time"
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.LinkedList
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder {
|
||||
private lateinit var bmc: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
this.bmc = context
|
||||
}
|
||||
|
||||
override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'${hex(initialHash)}'",
|
||||
null, null, null, null
|
||||
).use { c ->
|
||||
if (c.moveToFirst()) {
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
|
||||
)
|
||||
} else {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
|
||||
bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}")
|
||||
}
|
||||
|
||||
override fun getItems(): List<ByteArray> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_INITIAL_HASH)
|
||||
|
||||
val result = LinkedList<ByteArray>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection, null, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH))
|
||||
result.add(initialHash)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun putObject(item: ProofOfWorkRepository.Item) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
|
||||
put(COLUMN_DATA, Encode.bytes(item.objectMessage))
|
||||
put(COLUMN_VERSION, item.objectMessage.version)
|
||||
put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
|
||||
put(COLUMN_EXTRA_BYTES, item.extraBytes)
|
||||
item.message?.let { message ->
|
||||
put(COLUMN_EXPIRATION_TIME, item.expirationTime)
|
||||
put(COLUMN_MESSAGE_ID, message.id as Long?)
|
||||
}
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) =
|
||||
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
|
||||
|
||||
override fun removeObject(initialHash: ByteArray) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "POW"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
|
||||
private const val COLUMN_EXTRA_BYTES = "extra_bytes"
|
||||
private const val COLUMN_EXPIRATION_TIME = "expiration_time"
|
||||
private const val COLUMN_MESSAGE_ID = "message_id"
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION)
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf(
|
||||
0 to {
|
||||
executeMigration(db, "V1.0__Create_table_inventory")
|
||||
executeMigration(db, "V1.1__Create_table_address")
|
||||
executeMigration(db, "V1.2__Create_table_message")
|
||||
},
|
||||
1 to {
|
||||
// executeMigration(db, "V2.0__Update_table_message");
|
||||
executeMigration(db, "V2.1__Create_table_POW")
|
||||
},
|
||||
2 to {
|
||||
executeMigration(db, "V3.0__Update_table_address")
|
||||
},
|
||||
3 to {
|
||||
executeMigration(db, "V3.1__Update_table_POW")
|
||||
executeMigration(db, "V3.2__Update_table_message")
|
||||
},
|
||||
4 to {
|
||||
executeMigration(db, "V3.3__Create_table_node")
|
||||
},
|
||||
5 to {
|
||||
executeMigration(db, "V3.4__Add_label_outbox")
|
||||
},
|
||||
6 to {
|
||||
executeMigration(db, "V4.0__Create_table_message_parent")
|
||||
},
|
||||
7 to {
|
||||
setMissingConversationIds(db)
|
||||
}
|
||||
).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() }
|
||||
|
||||
/**
|
||||
* Set UUIDs for all messages that have no conversation ID
|
||||
*/
|
||||
private fun setMissingConversationIds(db: SQLiteDatabase) = db.query(
|
||||
"Message", arrayOf("id"),
|
||||
"conversation IS NULL", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(0)
|
||||
setMissingConversationId(id, db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
|
||||
val values = ContentValues(1).apply {
|
||||
put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
|
||||
}
|
||||
db.update("Message", values, "id=?", arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
private fun executeMigration(db: SQLiteDatabase, name: String) {
|
||||
for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) {
|
||||
db.execSQL(statement)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
private const val DATABASE_VERSION = 7
|
||||
const val DATABASE_NAME = "jabit.db"
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import ch.dissem.apps.abit.notification.BatchNotification
|
||||
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.util.*
|
||||
|
||||
class BatchProcessorService : Service() {
|
||||
private lateinit var notification: BatchNotification
|
||||
|
||||
override fun onCreate() {
|
||||
notification = BatchNotification(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = BatchBinder(this)
|
||||
|
||||
class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() {
|
||||
private val notification = service.notification
|
||||
|
||||
fun process(job: Job) = synchronized(queue) {
|
||||
ContextCompat.startForegroundService(
|
||||
service,
|
||||
Intent(service, BatchProcessorService::class.java)
|
||||
)
|
||||
service.startForeground(
|
||||
ONGOING_NOTIFICATION_ID,
|
||||
notification.notification
|
||||
)
|
||||
if (!working) {
|
||||
working = true
|
||||
service.processQueue(job)
|
||||
} else {
|
||||
queue.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processQueue(job: Job) {
|
||||
doAsync {
|
||||
var next: Job? = job
|
||||
while (next != null) {
|
||||
next.process(notification)
|
||||
|
||||
synchronized(queue) {
|
||||
next = queue.poll()
|
||||
if (next == null) {
|
||||
working = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var working = false
|
||||
private val queue = LinkedList<Job>()
|
||||
}
|
||||
}
|
||||
|
||||
interface Job {
|
||||
val icon: Int
|
||||
@DrawableRes get
|
||||
|
||||
val description: Int
|
||||
@StringRes get
|
||||
|
||||
val numberOfItems: Int
|
||||
var numberOfProcessedItems: Int
|
||||
|
||||
/**
|
||||
* Runs the job. This shouldn't happen in a separate thread, as this is handled by the service.
|
||||
*/
|
||||
fun process(notification: BatchNotification)
|
||||
}
|
||||
|
||||
data class SimpleJob<T>(
|
||||
override val numberOfItems: Int,
|
||||
/**
|
||||
* Provides the next batch of items, given the last item of the previous batch,
|
||||
* or null for the first batch.
|
||||
*/
|
||||
private val provider: (T?) -> List<T>,
|
||||
/**
|
||||
* Processes an item.
|
||||
*/
|
||||
private val processor: (T) -> Unit,
|
||||
override val icon: Int,
|
||||
override val description: Int
|
||||
) : Job {
|
||||
override var numberOfProcessedItems: Int = 0
|
||||
|
||||
override fun process(notification: BatchNotification) {
|
||||
notification.update(this)
|
||||
var batch = provider.invoke(null)
|
||||
while (batch.isNotEmpty()) {
|
||||
Thread.yield()
|
||||
batch.forEach {
|
||||
processor.invoke(it)
|
||||
Thread.yield()
|
||||
}
|
||||
numberOfProcessedItems += batch.size
|
||||
notification.update(this)
|
||||
batch = provider.invoke(batch.last())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class BitmessageIntentService : IntentService("BitmessageIntentService") {
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
}
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
intent?.let {
|
||||
if (it.hasExtra(EXTRA_DELETE_MESSAGE)) {
|
||||
val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext
|
||||
bmc.labeler.delete(item)
|
||||
bmc.messages.save(item)
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
}
|
||||
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
|
||||
network.enableNode()
|
||||
}
|
||||
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
||||
network.disableNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"
|
||||
const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"
|
||||
const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
class CleanupService : JobService() {
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
doAsync {
|
||||
Singleton.getBitmessageContext(this@CleanupService).cleanup()
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters?) = false
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
/**
|
||||
* Starts the full node if
|
||||
* * it is active
|
||||
* * it is not already running
|
||||
*
|
||||
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
|
||||
*/
|
||||
class NodeStartupService : JobService() {
|
||||
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
|
||||
|
||||
private lateinit var notification: NetworkNotification
|
||||
|
||||
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (bmc.isRunning() && !preferences.connectionAllowed) {
|
||||
bmc.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notification = NetworkNotification(this)
|
||||
}
|
||||
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
if (preferences.online) {
|
||||
registerReceiver(
|
||||
connectivityReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||
}
|
||||
)
|
||||
startForeground(0, notification.notification)
|
||||
NodeStartupService.running = false
|
||||
|
||||
if (!isRunning) {
|
||||
running = true
|
||||
notification.connecting()
|
||||
if (!bmc.isRunning()) {
|
||||
bmc.startup()
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (bmc.isRunning()) {
|
||||
bmc.shutdown()
|
||||
}
|
||||
running = false
|
||||
notification.showShutdown()
|
||||
doAsync {
|
||||
bmc.cleanup()
|
||||
}
|
||||
try {
|
||||
unregisterReceiver(connectivityReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// For some reason, onStartJob wasn't called so the receiver isn't registered.
|
||||
// Let's just ignore this.
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
|
||||
* depending on Android version.
|
||||
*/
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
network.scheduleNodeStart()
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = running && Singleton.bitmessageContext?.isRunning() == true
|
||||
|
||||
val status: Property
|
||||
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import androidx.core.content.ContextCompat
|
||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
|
||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
|
||||
import ch.dissem.apps.abit.util.PowStats
|
||||
import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be
|
||||
* killed by the system before the nonce is found.
|
||||
*/
|
||||
class ProofOfWorkService : Service() {
|
||||
private lateinit var notification: ProofOfWorkNotification
|
||||
|
||||
override fun onCreate() {
|
||||
notification = ProofOfWorkNotification(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = PowBinder(this)
|
||||
|
||||
class PowBinder internal constructor(private val service: ProofOfWorkService) : Binder() {
|
||||
private val notification = service.notification
|
||||
|
||||
fun process(item: PowItem) = synchronized(queue) {
|
||||
ContextCompat.startForegroundService(
|
||||
service,
|
||||
Intent(service, ProofOfWorkService::class.java)
|
||||
)
|
||||
service.startForeground(
|
||||
ONGOING_NOTIFICATION_ID,
|
||||
notification.notification
|
||||
)
|
||||
if (!calculating) {
|
||||
calculating = true
|
||||
service.calculateNonce(item)
|
||||
} else {
|
||||
queue.add(item)
|
||||
notification.update(queue.size).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class PowItem(
|
||||
val initialHash: ByteArray,
|
||||
val targetValue: ByteArray,
|
||||
val callback: ProofOfWorkEngine.Callback
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PowItem
|
||||
|
||||
if (!Arrays.equals(initialHash, other.initialHash)) return false
|
||||
if (!Arrays.equals(targetValue, other.targetValue)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = Arrays.hashCode(initialHash)
|
||||
result = 31 * result + Arrays.hashCode(targetValue)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateNonce(item: PowItem) {
|
||||
notification.start(item)
|
||||
val startTime = System.currentTimeMillis()
|
||||
engine.calculateNonce(
|
||||
item.initialHash,
|
||||
item.targetValue,
|
||||
object : ProofOfWorkEngine.Callback {
|
||||
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
|
||||
notification.finished()
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
|
||||
try {
|
||||
item.callback.onNonceCalculated(initialHash, nonce)
|
||||
} finally {
|
||||
var next: PowItem? = null
|
||||
synchronized(queue) {
|
||||
next = queue.poll()
|
||||
if (next == null) {
|
||||
calculating = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
} else {
|
||||
notification.update(queue.size).show()
|
||||
}
|
||||
}
|
||||
next?.let { calculateNonce(it) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Object to use as a thread-safe lock
|
||||
private val engine = MultiThreadedPOWEngine()
|
||||
private val queue = LinkedList<PowItem>()
|
||||
private var calculating: Boolean = false
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
|
||||
import android.content.Context.BIND_AUTO_CREATE
|
||||
|
||||
/**
|
||||
* Proof of Work engine that uses the Proof of Work service.
|
||||
*/
|
||||
class ServicePowEngine(private val ctx: Context) : ProofOfWorkEngine {
|
||||
private val queue = LinkedList<PowItem>()
|
||||
private var service: PowBinder? = null
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) = synchronized(lock) {
|
||||
this@ServicePowEngine.service = service as PowBinder
|
||||
while (!queue.isEmpty()) {
|
||||
service.process(queue.poll())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
service = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
||||
val item = PowItem(initialHash, target, callback)
|
||||
synchronized(lock) {
|
||||
service?.process(item) ?: {
|
||||
queue.add(item)
|
||||
ctx.bindService(Intent(ctx, ProofOfWorkService::class.java), connection, BIND_AUTO_CREATE)
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val lock = Any()
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.listener.MessageListener
|
||||
import ch.dissem.apps.abit.repository.*
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
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.networking.nio.NioNetworkHandler
|
||||
import ch.dissem.bitmessage.ports.DefaultLabeler
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import ch.dissem.bitmessage.utils.TTL
|
||||
import ch.dissem.bitmessage.utils.UnixTime.DAY
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Provides singleton objects across the application.
|
||||
*/
|
||||
object Singleton {
|
||||
var currentLabel = BehaviorSubject.create<Label>()
|
||||
|
||||
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
|
||||
val labeler = DefaultLabeler().apply {
|
||||
listener = { message, added, removed ->
|
||||
MainActivity.apply {
|
||||
runOnUiThread {
|
||||
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
|
||||
currentLabel.value?.let {label ->
|
||||
when {
|
||||
label.type == Label.Type.TRASH
|
||||
&& added.all { it.type == Label.Type.TRASH }
|
||||
&& removed.any { it.type == Label.Type.TRASH } -> {
|
||||
// work-around for messages that are deleted from trash
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
label.type == Label.Type.UNREAD
|
||||
&& added.all { it.type == Label.Type.TRASH } -> {
|
||||
// work-around for messages that are deleted from unread, which already have the unread label removed
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
label == AndroidLabelRepository.LABEL_ARCHIVE && !added.isEmpty() -> {
|
||||
// work-around for messages in archive, which isn't an actual label but an absence of labels
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
added.contains(label) -> {
|
||||
// in most cases, top should be the correct position, but time will show if
|
||||
// the message should be properly sorted in
|
||||
swipeableMessageAdapter.addFirst(message)
|
||||
}
|
||||
removed.contains(label) -> {
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
|
||||
swipeableMessageAdapter.update(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
|
||||
updateUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var bitmessageContext: BitmessageContext? = null
|
||||
private set
|
||||
private var conversationService: ConversationService? = null
|
||||
private var messageListener: MessageListener? = null
|
||||
private var identity: BitmessageAddress? = null
|
||||
private var powRepo: AndroidProofOfWorkRepository? = null
|
||||
private var creatingIdentity: Boolean = false
|
||||
|
||||
fun getBitmessageContext(context: Context): BitmessageContext =
|
||||
init({ bitmessageContext }, { bitmessageContext = it }) {
|
||||
BitmessageContext.build {
|
||||
TTL.pubkey = 2 * DAY
|
||||
val ctx = context.applicationContext
|
||||
val sqlHelper = SqlHelper(ctx)
|
||||
proofOfWorkEngine = ServicePowEngine(ctx)
|
||||
cryptography = SpongyCryptography()
|
||||
nodeRegistry = AndroidNodeRegistry(sqlHelper)
|
||||
inventory = AndroidInventory(sqlHelper)
|
||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
||||
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
|
||||
messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
|
||||
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
|
||||
networkHandler = NioNetworkHandler(4)
|
||||
listener = getMessageListener(ctx)
|
||||
labeler = Singleton.labeler
|
||||
preferences.sendPubkeyOnIdentityCreation = false
|
||||
preferences.port = context.preferences.listeningPort
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMessageListAdapterInListener(adapter: SwipeableMessageAdapter) {
|
||||
swipeableMessageAdapter = WeakReference(adapter)
|
||||
}
|
||||
|
||||
fun getMessageListener(ctx: Context) = init({ messageListener }, { messageListener = it }) { MessageListener(ctx) }
|
||||
|
||||
fun getLabelRepository(ctx: Context) = getBitmessageContext(ctx).labels as AndroidLabelRepository
|
||||
|
||||
fun getMessageRepository(ctx: Context) = getBitmessageContext(ctx).messages as AndroidMessageRepository
|
||||
|
||||
fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository
|
||||
|
||||
fun getIdentity(ctx: Context): BitmessageAddress? =
|
||||
init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc ->
|
||||
val identities = bmc.addresses.getIdentities()
|
||||
if (identities.isNotEmpty()) {
|
||||
identities[0]
|
||||
} else {
|
||||
if (!creatingIdentity) {
|
||||
creatingIdentity = true
|
||||
doAsync {
|
||||
val identity = bmc.createIdentity(false,
|
||||
Pubkey.Feature.DOES_ACK)
|
||||
identity.alias = ctx.getString(R.string.alias_default_identity)
|
||||
bmc.addresses.save(identity)
|
||||
|
||||
uiThread {
|
||||
Singleton.identity = identity
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply { addIdentityEntry(identity) }
|
||||
}
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setIdentity(identity: BitmessageAddress) {
|
||||
if (identity.privateKey == null)
|
||||
throw IllegalArgumentException("Identity expected, but no private key available")
|
||||
Singleton.identity = identity
|
||||
}
|
||||
|
||||
fun getConversationService(ctx: Context) = init(ctx, { conversationService }, { conversationService = it }) { ConversationService(it.messages) }
|
||||
|
||||
private inline fun <T> init(crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: () -> T): T =
|
||||
getter() ?: {
|
||||
synchronized(Singleton) {
|
||||
getter() ?: {
|
||||
val v = creator()
|
||||
setter(v)
|
||||
v
|
||||
}.invoke()
|
||||
}
|
||||
}.invoke()
|
||||
|
||||
private inline fun <T> init(ctx: Context, crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: (BitmessageContext) -> T): T =
|
||||
getter() ?: {
|
||||
val bmc = getBitmessageContext(ctx)
|
||||
synchronized(Singleton) {
|
||||
getter() ?: {
|
||||
val v = creator(bmc)
|
||||
setter(v)
|
||||
v
|
||||
}.invoke()
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_BOOT_COMPLETED
|
||||
import ch.dissem.apps.abit.util.network
|
||||
import ch.dissem.apps.abit.util.preferences
|
||||
|
||||
/**
|
||||
* Starts the Bitmessage "full node" service if conditions allow it
|
||||
*/
|
||||
class StartServiceReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.action == ACTION_BOOT_COMPLETED && context.preferences.online) {
|
||||
context.network.enableNode(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class to work with Assets.
|
||||
*/
|
||||
object Assets {
|
||||
fun readSqlStatements(ctx: Context, name: String): List<String> {
|
||||
try {
|
||||
val `in` = ctx.assets.open(name)
|
||||
val scanner = Scanner(`in`, "UTF-8").useDelimiter(";")
|
||||
val result = LinkedList<String>()
|
||||
while (scanner.hasNext()) {
|
||||
val statement = scanner.next().trim { it <= ' ' }
|
||||
if ("" != statement) {
|
||||
result.add(statement)
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Plaintext.Status.getDrawable() = when (this) {
|
||||
Plaintext.Status.RECEIVED -> 0
|
||||
Plaintext.Status.DRAFT -> R.drawable.draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
|
||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
|
||||
Plaintext.Status.SENT -> R.drawable.sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun Plaintext.Status.getString() = when (this) {
|
||||
Plaintext.Status.RECEIVED -> R.string.status_received
|
||||
Plaintext.Status.DRAFT -> R.string.status_draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
|
||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
|
||||
Plaintext.Status.SENT -> R.string.status_sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
@@ -1,37 +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.util
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
object Constants {
|
||||
const val PREFERENCE_ONLINE = "online"
|
||||
const val PREFERENCE_WIFI_ONLY = "wifi_only"
|
||||
const val PREFERENCE_REQUIRE_CHARGING = "require_charging"
|
||||
const val PREFERENCE_EMULATE_CONVERSATIONS = "emulate_conversations"
|
||||
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
|
||||
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
|
||||
const val PREFERENCE_POW_COUNT = "pow_count"
|
||||
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
|
||||
|
||||
const val BITMESSAGE_URL_SCHEMA = "bitmessage:"
|
||||
|
||||
val BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b")!!
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
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.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
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* Some helper methods to work with drawables.
|
||||
*/
|
||||
object Drawables {
|
||||
internal val LOG = LoggerFactory.getLogger(Drawables::class.java)
|
||||
|
||||
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 Drawable.toBitmap(width: Int, height: Int = width): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
setBounds(0, 0, canvas.width, canvas.height)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun BitmessageAddress.qrCode(): Bitmap? {
|
||||
val link = StringBuilder()
|
||||
link.append(Constants.BITMESSAGE_URL_SCHEMA)
|
||||
link.append(address)
|
||||
if (alias != null) {
|
||||
link.append("?label=").append(alias)
|
||||
}
|
||||
pubkey?.apply {
|
||||
link.append(if (alias == null) '?' else '&')
|
||||
val pubkey = ByteArrayOutputStream()
|
||||
writer().writeUnencrypted(pubkey)
|
||||
link.append("pubkey=")
|
||||
.append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
|
||||
|
||||
}
|
||||
val result: BitMatrix
|
||||
try {
|
||||
result = MultiFormatWriter().encode(
|
||||
link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
|
||||
)
|
||||
} catch (e: WriterException) {
|
||||
Drawables.LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
|
||||
val w = result.width
|
||||
val h = result.height
|
||||
val pixels = IntArray(w * h)
|
||||
for (y in 0 until h) {
|
||||
val offset = y * w
|
||||
for (x in 0 until w) {
|
||||
pixels[offset + x] = if (result.get(x, y)) BLACK else WHITE
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.exports.ContactExport
|
||||
import ch.dissem.bitmessage.exports.MessageExport
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import com.beust.klaxon.JsonArray
|
||||
import com.beust.klaxon.Parser
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* Helper object for data export and import.
|
||||
*/
|
||||
object Exports {
|
||||
|
||||
fun exportData(target: File, ctx: Context): File {
|
||||
val temp = if (target.isDirectory) {
|
||||
File(target, "export-${UnixTime.now}.zip")
|
||||
} else {
|
||||
target
|
||||
}
|
||||
ZipOutputStream(FileOutputStream(temp)).use { zip ->
|
||||
zip.putNextEntry(ZipEntry("contacts.json"))
|
||||
val addressRepo = Singleton.getAddressRepository(ctx)
|
||||
val exportContacts = ContactExport.exportContacts(addressRepo.getContacts())
|
||||
zip.write(
|
||||
exportContacts.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
|
||||
val labelRepo = Singleton.getLabelRepository(ctx)
|
||||
zip.putNextEntry(ZipEntry("labels.json"))
|
||||
val exportLabels = MessageExport.exportLabels(labelRepo.getLabels())
|
||||
zip.write(
|
||||
exportLabels.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
zip.putNextEntry(ZipEntry("messages.json"))
|
||||
val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
val exportMessages = MessageExport.exportMessages(messageRepo.getAllMessages())
|
||||
zip.write(
|
||||
exportMessages.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
fun importData(zipFile: Uri, ctx: Context) {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
val labels = mutableMapOf<String, Label>()
|
||||
|
||||
processEntry(ctx, zipFile, "contacts.json") { json ->
|
||||
ContactExport.importContacts(json).forEach { contact ->
|
||||
bmc.addresses.save(contact)
|
||||
}
|
||||
}
|
||||
bmc.labels.getLabels().forEach { label ->
|
||||
labels[label.toString()] = label
|
||||
}
|
||||
processEntry(ctx, zipFile, "labels.json") { json ->
|
||||
MessageExport.importLabels(json).forEach { label ->
|
||||
if (!labels.contains(label.toString())) {
|
||||
bmc.labels.save(label)
|
||||
labels[label.toString()] = label
|
||||
}
|
||||
}
|
||||
}
|
||||
processEntry(ctx, zipFile, "messages.json") { json ->
|
||||
MessageExport.importMessages(json, labels).forEach { message ->
|
||||
bmc.messages.save(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEntry(ctx: Context, zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) =
|
||||
ZipInputStream(ctx.contentResolver.openInputStream(zipFile)).use { zip ->
|
||||
var nextEntry = zip.nextEntry
|
||||
while (nextEntry != null) {
|
||||
if (nextEntry.name == entry) {
|
||||
processor(Parser().parse(zip) as JsonArray<*>)
|
||||
}
|
||||
nextEntry = zip.nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
|
||||
/*
|
||||
* Helper methods to help with translating the default labels, getting label colors and so on.
|
||||
*/
|
||||
|
||||
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)
|
||||
Label.Type.SENT -> ctx.getString(R.string.sent)
|
||||
Label.Type.UNREAD -> ctx.getString(R.string.unread)
|
||||
Label.Type.TRASH -> ctx.getString(R.string.trash)
|
||||
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
|
||||
else -> alternative
|
||||
}
|
||||
|
||||
fun Label.getIcon(): IIcon = when (type) {
|
||||
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
|
||||
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
|
||||
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
|
||||
Label.Type.SENT -> CommunityMaterial.Icon.cmd_send
|
||||
Label.Type.BROADCAST -> CommunityMaterial.Icon.cmd_rss
|
||||
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
|
||||
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
|
||||
else -> CommunityMaterial.Icon.cmd_label
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
|
||||
color
|
||||
} else default
|
||||
@@ -1,77 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
|
||||
import ch.dissem.apps.abit.service.CleanupService
|
||||
import ch.dissem.apps.abit.service.NodeStartupService
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val Context.network get() = NetworkUtils.getInstance(this)
|
||||
|
||||
class NetworkUtils internal constructor(private val ctx: Context) {
|
||||
|
||||
private val jobScheduler by lazy { ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler }
|
||||
|
||||
fun enableNode(ask: Boolean = true) {
|
||||
if (ask && !ctx.preferences.connectionAllowed) {
|
||||
// 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 {
|
||||
scheduleNodeStart()
|
||||
}
|
||||
}
|
||||
|
||||
fun disableNode() {
|
||||
jobScheduler.cancelAll()
|
||||
}
|
||||
|
||||
fun scheduleNodeStart() {
|
||||
JobInfo.Builder(0, ComponentName(ctx, NodeStartupService::class.java)).let { builder ->
|
||||
when {
|
||||
ctx.preferences.wifiOnly ->
|
||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
|
||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
|
||||
else ->
|
||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
}
|
||||
builder.setRequiresCharging(ctx.preferences.requireCharging)
|
||||
builder.setPersisted(true)
|
||||
|
||||
jobScheduler.schedule(builder.build())
|
||||
}
|
||||
|
||||
JobInfo.Builder(1, ComponentName(ctx, CleanupService::class.java)).let { builder ->
|
||||
builder.setPeriodic(TimeUnit.DAYS.toMillis(1))
|
||||
builder.setRequiresDeviceIdle(true)
|
||||
builder.setRequiresCharging(true)
|
||||
|
||||
jobScheduler.schedule(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: WeakReference<NetworkUtils>? = null
|
||||
|
||||
internal fun getInstance(ctx: Context): NetworkUtils {
|
||||
var networkUtils = instance?.get()
|
||||
if (networkUtils == null) {
|
||||
networkUtils = NetworkUtils(ctx.applicationContext)
|
||||
instance = WeakReference(networkUtils)
|
||||
}
|
||||
return networkUtils
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_POW_AVERAGE
|
||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_POW_COUNT
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* POW statistics that might help estimate the POW time, depending on
|
||||
*/
|
||||
object PowStats {
|
||||
private val TWO_POW_64: BigInteger = BigInteger.valueOf(2).pow(64)
|
||||
|
||||
private var averagePowUnitTime = 0L
|
||||
private var powCount = 0L
|
||||
|
||||
fun getExpectedPowTimeInMilliseconds(ctx: Context, target: ByteArray): Long {
|
||||
if (averagePowUnitTime == 0L) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
synchronized(this) {
|
||||
averagePowUnitTime = preferences.getLong(PREFERENCE_POW_AVERAGE, 0L)
|
||||
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L)
|
||||
}
|
||||
}
|
||||
return (averagePowUnitTime * BigInteger(target) / TWO_POW_64).toLong()
|
||||
}
|
||||
|
||||
fun addPow(ctx: Context, time: Long, target: ByteArray) {
|
||||
val targetBigInt = BigInteger(target)
|
||||
val powCountBefore = BigInteger.valueOf(powCount)
|
||||
synchronized(this) {
|
||||
powCount++
|
||||
averagePowUnitTime = (
|
||||
(averagePowUnitTime * powCountBefore + (time * TWO_POW_64 / targetBigInt)) / powCount
|
||||
).toLong()
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
preferences.edit()
|
||||
.putLong(PREFERENCE_POW_AVERAGE, averagePowUnitTime)
|
||||
.putLong(PREFERENCE_POW_COUNT, powCount)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Long.times(other: BigInteger) = this.toBigInteger() * other
|
||||
private operator fun BigInteger.div(other: Long) = this / other.toBigInteger()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user