Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
179
.gitignore
vendored
179
.gitignore
vendored
@ -1,165 +1,18 @@
|
|||||||
# Created by https://www.gitignore.io
|
# Mac Specific
|
||||||
|
|
||||||
### 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 ###
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
# Files that might appear on external disk
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
.Trashes
|
||||||
.VolumeIcon.icns
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
# Directories potentially created on remote AFP share
|
||||||
.AppleDB
|
.AppleDB
|
||||||
@ -168,24 +21,8 @@ Network Trash Folder
|
|||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
|
# Jekyll Specific
|
||||||
|
_site/
|
||||||
|
|
||||||
### Windows ###
|
# Ruby
|
||||||
# Windows image file caches
|
Gemfile.lock
|
||||||
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
|
|
||||||
|
|
||||||
|
48
404.html
Normal file
48
404.html
Normal file
@ -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>
|
28
LICENSE
Normal file
28
LICENSE
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Copyright (C) 2014 Jacob Tomlinson
|
||||||
|
|
||||||
|
The contents of this website, which consists of the files in
|
||||||
|
* _config.yml
|
||||||
|
* _data
|
||||||
|
* _drafts
|
||||||
|
* _posts
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
However the website theme built using jekyll is Open Source under the following
|
||||||
|
GPLv3 license.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
62
README.md
62
README.md
@ -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
|
![Homepage](http://i.imgur.com/xlmHArV.png)
|
||||||
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
|
||||||
|
![Article](http://i.imgur.com/8rD8FfC.png)
|
||||||
|
|
||||||
## Requirements
|
### Disqus Comments
|
||||||
You'll need at least Android 4.4 KitKat. Due to the Proof of Work that comes
|
![Comments](http://i.imgur.com/TBZHQwF.png)
|
||||||
with the protocol you might want as fast a processor and as many cores as
|
|
||||||
you can get.
|
### Posts grouped by year
|
||||||
|
![All Posts](http://i.imgur.com/9bNs2Sc.png)
|
||||||
|
|
||||||
|
### JavaScript Search
|
||||||
|
![Search](http://i.imgur.com/yQqMeSl.png)
|
||||||
|
|
||||||
|
### Menu by mmenu
|
||||||
|
![Menu](http://i.imgur.com/SClrNSH.png)
|
||||||
|
|
||||||
|
## 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
_config.yml
Normal file
40
_config.yml
Normal file
@ -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]
|
2
_data/thumbnail.yml
Normal file
2
_data/thumbnail.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
gravatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=500&d=mm"
|
||||||
|
jekyll: "https://i.imgur.com/aRQcGSi.png"
|
86
_includes/footer.html
Normal file
86
_includes/footer.html
Normal file
@ -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 %}
|
45
_includes/head.html
Normal file
45
_includes/head.html
Normal file
@ -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>
|
14
_includes/header.html
Normal file
14
_includes/header.html
Normal file
@ -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>
|
8
_includes/social_links.html
Normal file
8
_includes/social_links.html
Normal file
@ -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>
|
19
_layouts/default.html
Normal file
19
_layouts/default.html
Normal file
@ -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>
|
1
_layouts/none.html
Normal file
1
_layouts/none.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{ content }}
|
153
_layouts/page.html
Normal file
153
_layouts/page.html
Normal file
@ -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 %}
|
187
_layouts/post.html
Normal file
187
_layouts/post.html
Normal file
@ -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 %}
|
59
_posts/2014-06-08-using-thumbnails.md
Normal file
59
_posts/2014-06-08-using-thumbnails.md
Normal file
@ -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/
|
21
_posts/2014-06-09-so-what-is-jekyll.md
Normal file
21
_posts/2014-06-09-so-what-is-jekyll.md
Normal file
@ -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/).
|
105
_posts/2014-06-10-carte-noire-in-action.md
Normal file
105
_posts/2014-06-10-carte-noire-in-action.md
Normal file
@ -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
|
||||||
|
|
||||||
|
![Thumper](https://i.imgur.com/DMCHDqF.jpg)
|
||||||
|
|
||||||
|
|
||||||
|
### 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/
|
28
_posts/2015-03-23-welcome-to-carte-noir.md
Normal file
28
_posts/2015-03-23-welcome-to-carte-noir.md
Normal file
@ -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
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/build
|
|
116
app/build.gradle
116
app/build.gradle
@ -1,116 +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 27
|
|
||||||
buildToolsVersion "26.0.2"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
|
|
||||||
minSdkVersion 19
|
|
||||||
targetSdkVersion 27
|
|
||||||
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.0.2'
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
|
||||||
|
|
||||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
|
||||||
implementation "com.android.support:preference-v7:$supportVersion"
|
|
||||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
|
||||||
implementation "com.android.support:support-v13:$supportVersion"
|
|
||||||
implementation "com.android.support:preference-v14:$supportVersion"
|
|
||||||
implementation "com.android.support:design:$supportVersion"
|
|
||||||
implementation "com.android.support:multidex:1.0.2"
|
|
||||||
|
|
||||||
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
|
|
||||||
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
|
||||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
|
||||||
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
|
||||||
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
|
||||||
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
|
|
||||||
|
|
||||||
implementation 'org.slf4j:slf4j-android:1.7.25'
|
|
||||||
|
|
||||||
implementation 'com.mikepenz:materialize:1.1.2@aar'
|
|
||||||
implementation('com.mikepenz:materialdrawer:6.0.2@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
implementation "com.mikepenz:iconics-core:3.0.0@aar"
|
|
||||||
implementation "com.mikepenz:iconics-views:3.0.0@aar"
|
|
||||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
|
||||||
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
|
|
||||||
|
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
|
|
||||||
implementation 'com.google.zxing:core:3.3.1'
|
|
||||||
|
|
||||||
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8'
|
|
||||||
implementation 'com.github.amlcurran.showcaseview:library:5.4.3'
|
|
||||||
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
implementation 'com.github.angads25:filepicker:1.1.1'
|
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
testImplementation 'org.mockito:mockito-core:2.13.0'
|
|
||||||
testImplementation 'org.hamcrest:hamcrest-library:1.3'
|
|
||||||
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
|
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
testImplementation 'org.robolectric:robolectric:3.6.1'
|
|
||||||
testImplementation "org.robolectric:shadows-multidex:3.6.1"
|
|
||||||
|
|
||||||
androidTestImplementation "com.android.support:multidex:1.0.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
idea.module {
|
|
||||||
downloadJavadoc = true
|
|
||||||
downloadSources = true
|
|
||||||
}
|
|
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@ -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,211 +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.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name="android.support.multidex.MultiDexApplication"
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android: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.BitmessageService"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
|
||||||
android:name=".service.ProofOfWorkService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<!-- Synchronization -->
|
|
||||||
<provider
|
|
||||||
android:name=".synchronization.StubProvider"
|
|
||||||
android:authorities="ch.dissem.apps.abit.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:syncable="true" />
|
|
||||||
|
|
||||||
<!-- Exports -->
|
|
||||||
<provider
|
|
||||||
android:name="android.support.v4.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=".synchronization.AuthenticatorService"
|
|
||||||
android:exported="true"
|
|
||||||
tools:ignore="ExportedService">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.accounts.AccountAuthenticator"
|
|
||||||
android:resource="@xml/authenticator" />
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".synchronization.SyncService"
|
|
||||||
android:exported="true"
|
|
||||||
tools:ignore="ExportedService">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.content.SyncAdapter"
|
|
||||||
android:resource="@xml/syncadapter" />
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".service.BitmessageIntentService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<!-- Receive Wi-Fi connection state changes -->
|
|
||||||
<receiver
|
|
||||||
android:name=".listener.WifiReceiver"
|
|
||||||
android:enabled="@bool/is_pre_api_21">
|
|
||||||
<intent-filter>
|
|
||||||
<!-- This is bad for battery life, but needed on older devices to check
|
|
||||||
if WiFi is available. Let's be honest, the whole app is bad for
|
|
||||||
battery life. -->
|
|
||||||
<action
|
|
||||||
android:name="android.net.conn.CONNECTIVITY_CHANGE"
|
|
||||||
tools:ignore="BatteryLife" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
|
||||||
android:name=".service.StartServiceReceiver"
|
|
||||||
android:enabled="@bool/is_post_api_21">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".service.StartupNodeOnWifiService"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
|
||||||
|
|
||||||
<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 android.support.v4.app.ListFragment
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ListView
|
|
||||||
|
|
||||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
abstract class AbstractItemListFragment<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,210 +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 android.support.v4.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.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(Drawables.qrCode(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,145 +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.apps.abit.util.FabUtils
|
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
|
||||||
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)
|
|
||||||
updateList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateList() {
|
|
||||||
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)
|
|
||||||
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu)
|
|
||||||
.addOnMenuItemClickListener { _, _, itemId ->
|
|
||||||
when (itemId) {
|
|
||||||
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
|
|
||||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
|
|
||||||
.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) = updateList()
|
|
||||||
|
|
||||||
private data class ViewHolder(
|
|
||||||
val ctx: Context,
|
|
||||||
val avatar: ImageView,
|
|
||||||
val name: TextView,
|
|
||||||
val streamNumber: TextView,
|
|
||||||
val subscribed: View
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import 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,293 +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 android.support.v4.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 == null || id.privateKey == null)) {
|
|
||||||
id = Singleton.getIdentity(context!!)
|
|
||||||
}
|
|
||||||
if (id?.privateKey != null) {
|
|
||||||
identity = id
|
|
||||||
} else {
|
|
||||||
throw IllegalStateException("No identity set for ComposeMessageFragment")
|
|
||||||
}
|
|
||||||
broadcast = getBoolean(EXTRA_BROADCAST, false)
|
|
||||||
if (containsKey(EXTRA_RECIPIENT)) {
|
|
||||||
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
|
|
||||||
}
|
|
||||||
if (containsKey(EXTRA_SUBJECT)) {
|
|
||||||
subject = getString(EXTRA_SUBJECT)
|
|
||||||
}
|
|
||||||
if (containsKey(EXTRA_CONTENT)) {
|
|
||||||
content = getString(EXTRA_CONTENT)
|
|
||||||
}
|
|
||||||
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?:
|
|
||||||
Plaintext.Encoding.SIMPLE
|
|
||||||
|
|
||||||
if (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 (!Preferences.requestAcknowledgements(ctx)) {
|
|
||||||
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,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 android.support.v7.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 android.support.v4.app.NavUtils
|
|
||||||
import android.support.v7.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,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2015 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.graphics.*
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.text.TextPaint
|
|
||||||
|
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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) {
|
|
||||||
var x: Float
|
|
||||||
var y: Float
|
|
||||||
val width = canvas.width.toFloat()
|
|
||||||
val height = canvas.height.toFloat()
|
|
||||||
val cellWidth = width / SIZE.toFloat()
|
|
||||||
val cellHeight = height / SIZE.toFloat()
|
|
||||||
paint.color = background
|
|
||||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
|
||||||
paint.color = color
|
|
||||||
for (row in 0 until SIZE) {
|
|
||||||
for (column in 0 until SIZE) {
|
|
||||||
if (fields[row][column]) {
|
|
||||||
x = cellWidth * column
|
|
||||||
y = cellHeight * row
|
|
||||||
canvas.drawCircle(
|
|
||||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
|
||||||
paint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isChan) {
|
|
||||||
textPaint.textSize = 2 * cellHeight
|
|
||||||
canvas.drawText("[isChan]", width / 2, 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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.app.Fragment
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.bitmessage.wif.WifImporter
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
|
|
||||||
import org.ini4j.InvalidFileFormatException
|
|
||||||
import org.jetbrains.anko.longToast
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
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?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val wifData = arguments.getString(WIF_DATA)
|
|
||||||
val bmc = Singleton.getBitmessageContext(activity)
|
|
||||||
|
|
||||||
try {
|
|
||||||
importer = WifImporter(bmc, wifData)
|
|
||||||
} catch (e: InvalidFileFormatException) {
|
|
||||||
longToast(R.string.invalid_wif_file)
|
|
||||||
activity.finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = AddressSelectorAdapter(importer.getIdentities())
|
|
||||||
val layoutManager = LinearLayoutManager(
|
|
||||||
activity,
|
|
||||||
LinearLayoutManager.VERTICAL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
|
||||||
recyclerView.layoutManager = layoutManager
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
recyclerView.addItemDecoration(
|
|
||||||
SimpleListDividerDecorator(
|
|
||||||
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
view.findViewById<Button>(R.id.finish).setOnClickListener {
|
|
||||||
importer.importAll(adapter.selected)
|
|
||||||
MainActivity.apply {
|
|
||||||
for (selected in adapter.selected) {
|
|
||||||
addIdentityEntry(selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val WIF_DATA = "wif_data"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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) {
|
|
||||||
fragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.content, InputWifFragment())
|
|
||||||
.commit()
|
|
||||||
} else {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
|
|
||||||
|
|
||||||
val fragment = ImportIdentitiesFragment()
|
|
||||||
fragment.arguments = bundle
|
|
||||||
|
|
||||||
fragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.content, fragment)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.app.Fragment
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.Toast
|
|
||||||
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.beginTransaction()
|
|
||||||
.replace(R.id.content, fragment)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,28 +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 updateList(label: L)
|
|
||||||
|
|
||||||
fun setActivateOnItemClick(activateOnItemClick: Boolean)
|
|
||||||
|
|
||||||
fun showPreviousList(): Boolean
|
|
||||||
}
|
|
@ -1,551 +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.Point
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import ch.dissem.apps.abit.drawer.ProfileImageListener
|
|
||||||
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
|
|
||||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
|
||||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
|
||||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
|
||||||
import ch.dissem.apps.abit.util.Labels
|
|
||||||
import ch.dissem.apps.abit.util.NetworkUtils
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import ch.dissem.bitmessage.BitmessageContext
|
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
|
||||||
import com.github.amlcurran.showcaseview.ShowcaseView
|
|
||||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
|
||||||
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 kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
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 lateinit var bmc: BitmessageContext
|
|
||||||
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)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
fab.hide()
|
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
|
|
||||||
val listFragment = MessageListFragment()
|
|
||||||
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 (Preferences.useTrustedNode(this)) {
|
|
||||||
SyncAdapter.startSync(this)
|
|
||||||
} else {
|
|
||||||
SyncAdapter.stopSync(this)
|
|
||||||
}
|
|
||||||
if (drawer.isDrawerOpen) {
|
|
||||||
val lps = RelativeLayout.LayoutParams(
|
|
||||||
ViewGroup
|
|
||||||
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
).apply {
|
|
||||||
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
|
|
||||||
addRule(RelativeLayout.ALIGN_PARENT_LEFT)
|
|
||||||
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
|
|
||||||
setMargins(margin, margin, margin, margin)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowcaseView.Builder(this)
|
|
||||||
.withMaterialShowcase()
|
|
||||||
.setStyle(R.style.CustomShowcaseTheme)
|
|
||||||
.setContentTitle(R.string.full_node)
|
|
||||||
.setContentText(R.string.full_node_description)
|
|
||||||
.setTarget {
|
|
||||||
val view = drawer.stickyFooter
|
|
||||||
val location = IntArray(2)
|
|
||||||
view.getLocationInWindow(location)
|
|
||||||
val x = location[0] + 7 * view.width / 8
|
|
||||||
val y = location[1] + view.height / 2
|
|
||||||
Point(x, y)
|
|
||||||
}
|
|
||||||
.replaceEndButton(R.layout.showcase_button)
|
|
||||||
.hideOnTouchOutside()
|
|
||||||
.build()
|
|
||||||
.setButtonPosition(lps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.full_node)
|
|
||||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
|
||||||
.withChecked(Preferences.isFullNodeActive(this))
|
|
||||||
.withOnCheckedChangeListener { _, _, isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
NetworkUtils.enableNode(this@MainActivity)
|
|
||||||
} else {
|
|
||||||
NetworkUtils.disableNode(this@MainActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
|
|
||||||
} else if (currentLabel.value == null) {
|
|
||||||
currentLabel.value = labels[0]
|
|
||||||
}
|
|
||||||
for (label in labels) {
|
|
||||||
addLabelEntry(label)
|
|
||||||
}
|
|
||||||
currentLabel.value?.let {
|
|
||||||
drawer.setSelection(it.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.value = tag
|
|
||||||
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.updateList()
|
|
||||||
} else {
|
|
||||||
changeList(AddressListFragment())
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
R.string.settings -> {
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.item_list, SettingsFragment())
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
R.string.full_node -> return true
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
updateUnread()
|
|
||||||
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
|
|
||||||
NetworkUtils.enableNode(this, false)
|
|
||||||
}
|
|
||||||
Singleton.getMessageListener(this).resetNotification()
|
|
||||||
currentLabel.addObserver(this) { label ->
|
|
||||||
if (label != null && label.id is Long) {
|
|
||||||
drawer.setSelection(label.id as Long)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
active = true
|
|
||||||
super.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
currentLabel.removeObserver(this)
|
|
||||||
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(Labels.getIcon(label))
|
|
||||||
.withIconColor(Labels.getColor(label))
|
|
||||||
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 = bmc.messages.countUnread(label)
|
|
||||||
if (unread > 0) {
|
|
||||||
(item as PrimaryDrawerItem).withBadge(unread.toString())
|
|
||||||
} else {
|
|
||||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
|
||||||
}
|
|
||||||
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 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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
fun updateNodeSwitch() {
|
|
||||||
apply {
|
|
||||||
runOnUiThread {
|
|
||||||
nodeSwitch.withChecked(Preferences.isFullNodeActive(this))
|
|
||||||
drawer.updateStickyFooterItem(nodeSwitch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,53 +0,0 @@
|
|||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.NavUtils
|
|
||||||
import android.view.MenuItem
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
|
||||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
|
|
||||||
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM))
|
|
||||||
val fragment = MessageDetailFragment()
|
|
||||||
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,305 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.annotation.IdRes
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v7.widget.GridLayoutManager
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.text.util.Linkify
|
|
||||||
import android.text.util.Linkify.WEB_URLS
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.util.Assets
|
|
||||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
|
|
||||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
|
|
||||||
import ch.dissem.apps.abit.util.Drawables
|
|
||||||
import ch.dissem.apps.abit.util.Labels
|
|
||||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
|
||||||
import com.mikepenz.iconics.view.IconicsImageView
|
|
||||||
import kotlinx.android.synthetic.main.fragment_message_detail.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(Assets.getStatusDrawable(item.status))
|
|
||||||
status.contentDescription = getString(Assets.getStatusString(item.status))
|
|
||||||
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(Assets.getStatusDrawable(message.status))
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) :
|
|
||||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
private val labels = labels.toMutableList()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
|
||||||
val context = parent.context
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
|
|
||||||
// Inflate the custom layout
|
|
||||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
|
||||||
|
|
||||||
// Return a new holder instance
|
|
||||||
return ViewHolder(contactView)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Involves populating data into the item through holder
|
|
||||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
|
||||||
// Get the data model based on position
|
|
||||||
val label = labels[position]
|
|
||||||
|
|
||||||
viewHolder.icon.icon?.color(Labels.getColor(label))
|
|
||||||
viewHolder.icon.icon?.icon(Labels.getIcon(label))
|
|
||||||
viewHolder.label.text = Labels.getText(label, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = labels.size
|
|
||||||
|
|
||||||
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
|
||||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* The fragment argument representing the item ID that this fragment
|
|
||||||
* represents.
|
|
||||||
*/
|
|
||||||
const val ARG_ITEM = "item"
|
|
||||||
|
|
||||||
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,324 +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.support.v4.app.Fragment
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.support.v7.widget.RecyclerView.OnScrollListener
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.Toast
|
|
||||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
|
||||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
|
||||||
import ch.dissem.apps.abit.adapter.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.FabUtils
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
|
|
||||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
|
||||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.support.v4.onUiThread
|
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 15
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list fragment representing a list of Messages. This fragment
|
|
||||||
* also supports tablet devices by allowing list items to be given an
|
|
||||||
* 'activated' state upon selection. This helps indicate which item is
|
|
||||||
* currently being viewed in a [MessageDetailFragment].
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
|
||||||
* interface.
|
|
||||||
*/
|
|
||||||
class MessageListFragment : Fragment(), ListHolder<Label> {
|
|
||||||
|
|
||||||
private var isLoading = false
|
|
||||||
private var isLastPage = false
|
|
||||||
|
|
||||||
private var layoutManager: LinearLayoutManager? = null
|
|
||||||
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
|
|
||||||
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
|
|
||||||
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
|
|
||||||
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
|
|
||||||
|
|
||||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
|
||||||
layoutManager?.let { layoutManager ->
|
|
||||||
val visibleItemCount = layoutManager.childCount
|
|
||||||
val totalItemCount = layoutManager.itemCount
|
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
|
|
||||||
if (!isLoading && !isLastPage) {
|
|
||||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
|
||||||
&& firstVisibleItemPosition >= 0) {
|
|
||||||
loadMoreItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var emptyTrashMenuItem: MenuItem? = null
|
|
||||||
private lateinit var messageRepo: AndroidMessageRepository
|
|
||||||
private var activateOnItemClick: Boolean = false
|
|
||||||
|
|
||||||
private val backStack = Stack<Label>()
|
|
||||||
|
|
||||||
fun loadMoreItems() {
|
|
||||||
isLoading = true
|
|
||||||
swipeableMessageAdapter?.let { messageAdapter ->
|
|
||||||
doAsync {
|
|
||||||
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
|
|
||||||
onUiThread {
|
|
||||||
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)
|
|
||||||
|
|
||||||
currentLabel.addObserver(this) { new -> doUpdateList(new) }
|
|
||||||
doUpdateList(currentLabel.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
currentLabel.removeObserver(this)
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doUpdateList(label: Label?) {
|
|
||||||
val mainActivity = activity as? MainActivity
|
|
||||||
swipeableMessageAdapter?.clear(label)
|
|
||||||
if (label == null) {
|
|
||||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
|
||||||
swipeableMessageAdapter?.notifyDataSetChanged()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
|
||||||
mainActivity?.apply {
|
|
||||||
if ("archive" == label.toString()) {
|
|
||||||
updateTitle(getString(R.string.archive))
|
|
||||||
} else {
|
|
||||||
updateTitle(label.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
|
||||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val context = context ?: throw IllegalStateException("No context available")
|
|
||||||
|
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
|
||||||
|
|
||||||
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
|
|
||||||
// animation is running)
|
|
||||||
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
|
|
||||||
setInterceptVerticalScrollingWhileAnimationRunning(true)
|
|
||||||
isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// swipe manager
|
|
||||||
val swipeManager = RecyclerViewSwipeManager()
|
|
||||||
|
|
||||||
//swipeableMessageAdapter
|
|
||||||
val adapter = SwipeableMessageAdapter().apply {
|
|
||||||
setActivateOnItemClick(activateOnItemClick)
|
|
||||||
}
|
|
||||||
adapter.eventListener = object : SwipeableMessageAdapter.EventListener {
|
|
||||||
override fun onItemDeleted(item: Plaintext) {
|
|
||||||
if (MessageDetailFragment.isInTrash(item)) {
|
|
||||||
Singleton.labeler.delete(item)
|
|
||||||
messageRepo.remove(item)
|
|
||||||
} else {
|
|
||||||
Singleton.labeler.delete(item)
|
|
||||||
messageRepo.save(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemArchived(item: Plaintext) {
|
|
||||||
Singleton.labeler.archive(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemViewClicked(v: View?) {
|
|
||||||
val position = recycler_view.getChildAdapterPosition(v)
|
|
||||||
adapter.setSelectedPosition(position)
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
val item = adapter.getItem(position)
|
|
||||||
(activity as MainActivity).onItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrap for swiping
|
|
||||||
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
|
|
||||||
|
|
||||||
val animator = SwipeDismissItemAnimator()
|
|
||||||
|
|
||||||
// Change animations are enabled by default since support-v7-recyclerview v22.
|
|
||||||
// Disable the change animation in order to make turning back animation of swiped item
|
|
||||||
// works properly.
|
|
||||||
animator.supportsChangeAnimations = false
|
|
||||||
|
|
||||||
recycler_view.layoutManager = layoutManager
|
|
||||||
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableMessageAdapter
|
|
||||||
recycler_view.itemAnimator = animator
|
|
||||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
|
||||||
|
|
||||||
recycler_view.addItemDecoration(SimpleListDividerDecorator(
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true))
|
|
||||||
|
|
||||||
// NOTE:
|
|
||||||
// The initialization order is very important! This order determines the priority of
|
|
||||||
// touch event handling.
|
|
||||||
//
|
|
||||||
// priority: TouchActionGuard > Swipe > DragAndDrop
|
|
||||||
touchActionGuardManager.attachRecyclerView(recycler_view)
|
|
||||||
swipeManager.attachRecyclerView(recycler_view)
|
|
||||||
|
|
||||||
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
|
||||||
recyclerViewSwipeManager = swipeManager
|
|
||||||
this.swipeableMessageAdapter = adapter
|
|
||||||
|
|
||||||
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)
|
|
||||||
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
|
|
||||||
.addOnMenuItemClickListener { _, _, itemId ->
|
|
||||||
val identity = Singleton.getIdentity(context)
|
|
||||||
if (identity == null) {
|
|
||||||
Toast.makeText(activity, R.string.no_identity_warning,
|
|
||||||
Toast.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
when (itemId) {
|
|
||||||
1 -> {
|
|
||||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
|
||||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
|
||||||
intent.putExtra(EXTRA_BROADCAST, true)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
|
||||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
recyclerViewSwipeManager?.release()
|
|
||||||
recyclerViewSwipeManager = null
|
|
||||||
|
|
||||||
recyclerViewTouchActionGuardManager?.release()
|
|
||||||
recyclerViewTouchActionGuardManager = null
|
|
||||||
|
|
||||||
recycler_view.itemAnimator = null
|
|
||||||
recycler_view.adapter = null
|
|
||||||
|
|
||||||
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
|
|
||||||
wrappedAdapter = null
|
|
||||||
|
|
||||||
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)
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.empty_trash -> {
|
|
||||||
currentLabel.value?.let { label ->
|
|
||||||
if (label.type != Label.Type.TRASH) return true
|
|
||||||
|
|
||||||
doAsync {
|
|
||||||
for (message in messageRepo.findMessages(label)) {
|
|
||||||
messageRepo.remove(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiThread { doUpdateList(label) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateList(label: Label) {
|
|
||||||
currentLabel.value = label
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
|
||||||
swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick)
|
|
||||||
this.activateOnItemClick = activateOnItemClick
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
currentLabel.value = backStack.pop()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,200 +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.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.support.v4.content.FileProvider.getUriForFile
|
|
||||||
import android.support.v7.preference.Preference
|
|
||||||
import android.support.v7.preference.PreferenceFragmentCompat
|
|
||||||
import android.widget.Toast
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
|
||||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
|
|
||||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
|
|
||||||
import ch.dissem.apps.abit.util.Exports
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import com.mikepenz.aboutlibraries.Libs
|
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.support.v4.indeterminateProgressDialog
|
|
||||||
import org.jetbrains.anko.support.v4.startActivity
|
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.preferences)
|
|
||||||
|
|
||||||
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
|
|
||||||
val cleanup = findPreference("cleanup")
|
|
||||||
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
|
|
||||||
findPreference("export")?.onPreferenceClickListener = exportClickListener()
|
|
||||||
findPreference("import")?.onPreferenceClickListener = importClickListener()
|
|
||||||
findPreference("status").onPreferenceClickListener = statusClickListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
Preferences.cleanupExportDirectory(ctx)
|
|
||||||
|
|
||||||
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 = context ?: throw IllegalStateException("No context available")
|
|
||||||
|
|
||||||
indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
|
|
||||||
doAsync {
|
|
||||||
val exportDirectory = Preferences.getExportDirectory(ctx)
|
|
||||||
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 {
|
|
||||||
startActivity<StatusActivity>()
|
|
||||||
}
|
|
||||||
return@OnPreferenceClickListener true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
val ctx = context ?: throw IllegalStateException("No context available")
|
|
||||||
when (requestCode) {
|
|
||||||
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx)
|
|
||||||
READ_IMPORT_REQUEST_CODE -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
|
||||||
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 as? MainActivity)?.floatingActionButton?.hide()
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
|
||||||
.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
|
|
||||||
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
|
||||||
when (key) {
|
|
||||||
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
|
|
||||||
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) {
|
|
||||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
|
||||||
val ctx = context ?: throw IllegalStateException("No context available")
|
|
||||||
if (node != null) {
|
|
||||||
SyncAdapter.startSync(ctx)
|
|
||||||
} else {
|
|
||||||
SyncAdapter.stopSync(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
|
|
||||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
|
||||||
if (node != null) {
|
|
||||||
val ctx = context ?: throw IllegalStateException("No context available")
|
|
||||||
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
|
|
||||||
SyncAdapter.startPowSync(ctx)
|
|
||||||
} else {
|
|
||||||
SyncAdapter.stopPowSync(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 android.support.v7.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 android.support.v4.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 android.support.v7.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.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
|
|
||||||
.filter { it.selected }
|
|
||||||
.mapTo(LinkedList()) { it.data }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.adapter
|
|
||||||
|
|
||||||
import ch.dissem.apps.abit.util.PRNGFixes
|
|
||||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
class AndroidCryptography : SpongyCryptography() {
|
|
||||||
init {
|
|
||||||
PRNGFixes.apply()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,146 +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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
|
|
||||||
}
|
|
@ -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,250 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2015 Haruki Hasegawa
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.adapter
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import ch.dissem.apps.abit.Identicon
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
|
||||||
import ch.dissem.apps.abit.util.Assets
|
|
||||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
|
|
||||||
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
|
||||||
*
|
|
||||||
* @author Christian Basler
|
|
||||||
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
|
||||||
*/
|
|
||||||
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
|
|
||||||
|
|
||||||
private val data = LinkedList<Plaintext>()
|
|
||||||
var eventListener: EventListener? = null
|
|
||||||
private val itemViewOnClickListener: View.OnClickListener
|
|
||||||
private val swipeableViewContainerOnClickListener: View.OnClickListener
|
|
||||||
|
|
||||||
private var label: Label? = null
|
|
||||||
private var selectedPosition = -1
|
|
||||||
private var activateOnItemClick: Boolean = false
|
|
||||||
|
|
||||||
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
|
||||||
this.activateOnItemClick = activateOnItemClick
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventListener {
|
|
||||||
fun onItemDeleted(item: Plaintext)
|
|
||||||
|
|
||||||
fun onItemArchived(item: Plaintext)
|
|
||||||
|
|
||||||
fun onItemViewClicked(v: View?)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
|
|
||||||
val container = v.findViewById<FrameLayout>(R.id.container)!!
|
|
||||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
|
||||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
|
||||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
|
||||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
|
||||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
|
||||||
|
|
||||||
override fun getSwipeableContainerView() = container
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
|
|
||||||
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
|
|
||||||
|
|
||||||
// SwipeableItemAdapter requires stable ID, and also
|
|
||||||
// have to implement the getItemId() method appropriately.
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun add(item: Plaintext) {
|
|
||||||
data.add(item)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addFirst(item: Plaintext) {
|
|
||||||
val index = data.size
|
|
||||||
data.addFirst(item)
|
|
||||||
notifyItemInserted(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAll(items: Collection<Plaintext>) {
|
|
||||||
val index = data.size
|
|
||||||
data.addAll(items)
|
|
||||||
notifyItemRangeInserted(index, items.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(item: Plaintext) {
|
|
||||||
val index = data.indexOf(item)
|
|
||||||
data.removeAll { it.id == item.id }
|
|
||||||
notifyItemRemoved(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update(item: Plaintext) {
|
|
||||||
val index = data.indexOfFirst { it.id == item.id }
|
|
||||||
if (index >= 0) {
|
|
||||||
data[index] = item
|
|
||||||
notifyItemChanged(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear(newLabel: Label?) {
|
|
||||||
label = newLabel
|
|
||||||
data.clear()
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onItemViewClick(v: View) {
|
|
||||||
eventListener?.onItemViewClicked(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSwipeableViewContainerClick(v: View) {
|
|
||||||
eventListener?.onItemViewClicked(
|
|
||||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getItem(position: Int) = data[position]
|
|
||||||
|
|
||||||
override fun getItemId(position: Int) = data[position].id as Long
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val item = data[position]
|
|
||||||
|
|
||||||
holder.apply {
|
|
||||||
if (activateOnItemClick) {
|
|
||||||
container.setBackgroundResource(
|
|
||||||
if (position == selectedPosition)
|
|
||||||
R.drawable.bg_item_selected_state
|
|
||||||
else
|
|
||||||
R.drawable.bg_item_normal_state
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set listeners
|
|
||||||
// (if the item is *pinned*, click event comes to the itemView)
|
|
||||||
itemView.setOnClickListener(itemViewOnClickListener)
|
|
||||||
// (if the item is *not pinned*, click event comes to the container)
|
|
||||||
container.setOnClickListener(swipeableViewContainerOnClickListener)
|
|
||||||
|
|
||||||
// set data
|
|
||||||
avatar.setImageDrawable(Identicon(item.from))
|
|
||||||
status.setImageResource(Assets.getStatusDrawable(item.status))
|
|
||||||
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = data.size
|
|
||||||
|
|
||||||
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
|
|
||||||
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
|
||||||
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
|
|
||||||
} else {
|
|
||||||
REACTION_CAN_SWIPE_BOTH_H
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SwitchIntDef")
|
|
||||||
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
|
|
||||||
holder.itemView.setBackgroundResource(when (type) {
|
|
||||||
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
|
|
||||||
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
|
|
||||||
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
|
||||||
R.drawable.bg_swipe_item_neutral
|
|
||||||
} else {
|
|
||||||
R.drawable.bg_swipe_item_right
|
|
||||||
}
|
|
||||||
else -> R.drawable.bg_swipe_item_neutral
|
|
||||||
})
|
|
||||||
|
|
||||||
@SuppressLint("SwitchIntDef")
|
|
||||||
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
|
|
||||||
when (result) {
|
|
||||||
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
|
|
||||||
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
|
|
||||||
|
|
||||||
fun setSelectedPosition(selectedPosition: Int) {
|
|
||||||
val oldPosition = this.selectedPosition
|
|
||||||
this.selectedPosition = selectedPosition
|
|
||||||
notifyItemChanged(oldPosition)
|
|
||||||
notifyItemChanged(selectedPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
|
|
||||||
private var adapter: SwipeableMessageAdapter? = adapter
|
|
||||||
private val item = adapter.data[position]
|
|
||||||
|
|
||||||
override fun onPerformAction() {
|
|
||||||
adapter?.eventListener?.onItemDeleted(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleanUp() {
|
|
||||||
adapter = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
|
|
||||||
private var adapter: SwipeableMessageAdapter? = adapter
|
|
||||||
private val item = adapter.data[position]
|
|
||||||
|
|
||||||
override fun onPerformAction() {
|
|
||||||
adapter?.eventListener?.onItemArchived(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleanUp() {
|
|
||||||
adapter = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.adapter
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import ch.dissem.bitmessage.InternalContext
|
|
||||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches between two [ProofOfWorkEngine]s depending on the configuration.
|
|
||||||
*
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
class SwitchingProofOfWorkEngine(
|
|
||||||
private val ctx: Context,
|
|
||||||
private val preference: String,
|
|
||||||
private val option: ProofOfWorkEngine,
|
|
||||||
private val fallback: ProofOfWorkEngine
|
|
||||||
) : ProofOfWorkEngine, InternalContext.ContextHolder {
|
|
||||||
|
|
||||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
|
||||||
if (preferences.getBoolean(preference, false)) {
|
|
||||||
option.calculateNonce(initialHash, target, callback)
|
|
||||||
} else {
|
|
||||||
fallback.calculateNonce(initialHash, target, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setContext(context: InternalContext) = listOf(option, fallback)
|
|
||||||
.filterIsInstance<InternalContext.ContextHolder>()
|
|
||||||
.forEach { it.setContext(context) }
|
|
||||||
}
|
|
@ -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.support.v7.app.AppCompatDialogFragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import ch.dissem.apps.abit.ImportIdentityActivity
|
|
||||||
import ch.dissem.apps.abit.MainActivity
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.bitmessage.BitmessageContext
|
|
||||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
|
||||||
import kotlinx.android.synthetic.main.dialog_add_identity.*
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.support.v4.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 -> 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 android.support.v7.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,46 +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.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.apps.abit.util.NetworkUtils
|
|
||||||
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.setWifiOnly(this@FullNodeDialogActivity, false)
|
|
||||||
NetworkUtils.enableNode(applicationContext)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
dismiss.setOnClickListener {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
NetworkUtils.scheduleNodeStart(applicationContext)
|
|
||||||
}
|
|
||||||
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 android.support.v7.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.Drawables
|
|
||||||
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(Drawables.qrCode(Singleton.getIdentity(ctx)))
|
|
||||||
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,53 +0,0 @@
|
|||||||
package ch.dissem.apps.abit.drawer
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.support.v4.app.FragmentManager
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
|
|
||||||
import com.mikepenz.materialdrawer.AccountHeader
|
|
||||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
|
||||||
|
|
||||||
import ch.dissem.apps.abit.AddressDetailActivity
|
|
||||||
import ch.dissem.apps.abit.AddressDetailFragment
|
|
||||||
import ch.dissem.apps.abit.MainActivity
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
|
||||||
|
|
||||||
import android.widget.Toast.LENGTH_LONG
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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,68 +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.bitmessage.BitmessageContext
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
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 {
|
|
||||||
private val unacknowledged = LinkedList<Plaintext>()
|
|
||||||
private var numberOfUnacknowledgedMessages = 0
|
|
||||||
private val notification = NewMessageNotification(ctx)
|
|
||||||
private val pool = Executors.newSingleThreadExecutor()
|
|
||||||
|
|
||||||
override fun receive(plaintext: Plaintext) {
|
|
||||||
pool.submit {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.listener
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.util.NetworkUtils
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import org.jetbrains.anko.connectivityManager
|
|
||||||
|
|
||||||
class WifiReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(ctx: Context, intent: Intent) {
|
|
||||||
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
|
|
||||||
val bmc = Singleton.getBitmessageContext(ctx)
|
|
||||||
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
|
|
||||||
NetworkUtils.doStartBitmessageService(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 android.support.annotation.ColorRes
|
|
||||||
import android.support.v4.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,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 android.support.annotation.StringRes
|
|
||||||
import android.support.v4.app.NotificationCompat
|
|
||||||
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Easily create notifications with error messages. Use carefully, users probably won't like them.
|
|
||||||
* (But they are useful during development/testing)
|
|
||||||
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
|
|
||||||
|
|
||||||
private val builder = NotificationCompat.Builder(ctx, ERROR_CHANNEL_ID)
|
|
||||||
.setContentTitle(ctx.getString(R.string.app_name))
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
|
|
||||||
init {
|
|
||||||
initChannel(ERROR_CHANNEL_ID, R.color.colorPrimaryDark)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
|
||||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
|
||||||
.setContentText(ctx.getString(resId, *args))
|
|
||||||
notification = builder.build()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
|
||||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
|
||||||
.setContentText(ctx.getString(resId, *args))
|
|
||||||
notification = builder.build()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override val notificationId = ERROR_NOTIFICATION_ID
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ERROR_NOTIFICATION_ID = 4
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.notification
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.support.v4.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.BitmessageService
|
|
||||||
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)
|
|
||||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StringFormatMatches")
|
|
||||||
private fun update(): Boolean {
|
|
||||||
val running = BitmessageService.isRunning
|
|
||||||
builder.setOngoing(running)
|
|
||||||
val connections = BitmessageService.status.getProperty("network", "connections")
|
|
||||||
if (!running) {
|
|
||||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
|
|
||||||
} else if (connections == null || connections.properties.isEmpty()) {
|
|
||||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
|
||||||
} else {
|
|
||||||
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()
|
|
||||||
ctx.stopService(Intent(ctx, BitmessageService::class.java))
|
|
||||||
}
|
|
||||||
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,134 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.notification
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.support.v4.app.NotificationCompat
|
|
||||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
|
||||||
import android.support.v4.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.R
|
|
||||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
|
||||||
import ch.dissem.bitmessage.entity.Plaintext
|
|
||||||
|
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
|
|
||||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
|
|
||||||
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
|
|
||||||
import ch.dissem.apps.abit.util.Drawables.toBitmap
|
|
||||||
|
|
||||||
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(toBitmap(Identicon(plaintext.from), 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 android.support.v4.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,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.pow
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import ch.dissem.bitmessage.InternalContext
|
|
||||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
|
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
|
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
|
|
||||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
|
||||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christian Basler
|
|
||||||
*/
|
|
||||||
class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder {
|
|
||||||
private lateinit var context: InternalContext
|
|
||||||
|
|
||||||
private val pool: ExecutorService
|
|
||||||
|
|
||||||
init {
|
|
||||||
pool = Executors.newCachedThreadPool { r ->
|
|
||||||
val thread = Executors.defaultThreadFactory().newThread(r)
|
|
||||||
thread.priority = Thread.MIN_PRIORITY
|
|
||||||
thread
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) =
|
|
||||||
pool.execute {
|
|
||||||
val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW")
|
|
||||||
|
|
||||||
val request = ProofOfWorkRequest(identity, initialHash,
|
|
||||||
CALCULATE, target)
|
|
||||||
SyncAdapter.startPowSync(ctx)
|
|
||||||
try {
|
|
||||||
val cryptoMsg = CryptoCustomMessage(request)
|
|
||||||
cryptoMsg.signAndEncrypt(
|
|
||||||
identity,
|
|
||||||
cryptography().createPublicKey(identity.publicDecryptionKey)
|
|
||||||
)
|
|
||||||
val node = Preferences.getTrustedNode(ctx)
|
|
||||||
if (node == null) {
|
|
||||||
LOG.error("trusted node is not defined")
|
|
||||||
} else {
|
|
||||||
context.networkHandler.send(
|
|
||||||
node,
|
|
||||||
Preferences.getTrustedNodePort(ctx),
|
|
||||||
cryptoMsg)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LOG.error(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setContext(context: InternalContext) {
|
|
||||||
this.context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,109 +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.Labels
|
|
||||||
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? = Labels.getText(type, 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,260 +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.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) : AbstractMessageRepository() {
|
|
||||||
|
|
||||||
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
|
|
||||||
super.findMessages(null as Label?, offset, limit)
|
|
||||||
} else {
|
|
||||||
super.findMessages(label, offset, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun countUnread(label: Label?) = 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=?))",
|
|
||||||
arrayOf(Label.Type.UNREAD.name)
|
|
||||||
).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=?))",
|
|
||||||
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
|
|
||||||
).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findConversations(label: Label?): List<UUID> {
|
|
||||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
|
||||||
|
|
||||||
val where = when {
|
|
||||||
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
|
|
||||||
label == null -> null
|
|
||||||
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
|
|
||||||
}
|
|
||||||
val result = LinkedList<UUID>()
|
|
||||||
sql.readableDatabase.query(
|
|
||||||
true,
|
|
||||||
TABLE_NAME, projection, where,
|
|
||||||
null, null, null, null, null
|
|
||||||
).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): List<Plaintext> {
|
|
||||||
val result = LinkedList<Plaintext>()
|
|
||||||
|
|
||||||
// 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, null, 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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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.NetworkUtils
|
|
||||||
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)) {
|
|
||||||
NetworkUtils.enableNode(this)
|
|
||||||
}
|
|
||||||
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
|
||||||
NetworkUtils.disableNode(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,107 +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.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Handler
|
|
||||||
import ch.dissem.apps.abit.notification.NetworkNotification
|
|
||||||
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import ch.dissem.bitmessage.BitmessageContext
|
|
||||||
import ch.dissem.bitmessage.utils.Property
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define a Service that returns an IBinder for the
|
|
||||||
* sync adapter class, allowing the sync adapter framework to call
|
|
||||||
* onPerformSync().
|
|
||||||
*/
|
|
||||||
class BitmessageService : Service() {
|
|
||||||
|
|
||||||
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
|
|
||||||
private lateinit var notification: NetworkNotification
|
|
||||||
|
|
||||||
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
if (bmc.isRunning() && !Preferences.isConnectionAllowed(this@BitmessageService)) {
|
|
||||||
bmc.shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cleanupHandler = Handler()
|
|
||||||
private val cleanupTask: Runnable = object : Runnable {
|
|
||||||
override fun run() {
|
|
||||||
bmc.cleanup()
|
|
||||||
if (isRunning) {
|
|
||||||
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
registerReceiver(
|
|
||||||
connectivityReceiver,
|
|
||||||
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
|
||||||
)
|
|
||||||
notification = NetworkNotification(this)
|
|
||||||
running = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (!isRunning) {
|
|
||||||
running = true
|
|
||||||
notification.connecting()
|
|
||||||
startForeground(NETWORK_NOTIFICATION_ID, notification.notification)
|
|
||||||
if (!bmc.isRunning()) {
|
|
||||||
bmc.startup()
|
|
||||||
}
|
|
||||||
notification.show()
|
|
||||||
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L)
|
|
||||||
}
|
|
||||||
return Service.START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
if (bmc.isRunning()) {
|
|
||||||
bmc.shutdown()
|
|
||||||
}
|
|
||||||
running = false
|
|
||||||
notification.showShutdown()
|
|
||||||
cleanupHandler.removeCallbacks(cleanupTask)
|
|
||||||
bmc.cleanup()
|
|
||||||
unregisterReceiver(connectivityReceiver)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = null
|
|
||||||
|
|
||||||
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 android.support.v4.content.ContextCompat
|
|
||||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
|
|
||||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
|
|
||||||
import ch.dissem.apps.abit.util.PowStats
|
|
||||||
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,194 +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.AndroidCryptography
|
|
||||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
|
||||||
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
|
|
||||||
import ch.dissem.apps.abit.listener.MessageListener
|
|
||||||
import ch.dissem.apps.abit.pow.ServerPowEngine
|
|
||||||
import ch.dissem.apps.abit.repository.*
|
|
||||||
import ch.dissem.apps.abit.util.Constants
|
|
||||||
import ch.dissem.apps.abit.util.Observable
|
|
||||||
import ch.dissem.bitmessage.BitmessageContext
|
|
||||||
import ch.dissem.bitmessage.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 org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides singleton objects across the application.
|
|
||||||
*/
|
|
||||||
object Singleton {
|
|
||||||
var currentLabel = Observable<Label?>(null)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
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 = SwitchingProofOfWorkEngine(
|
|
||||||
ctx, Constants.PREFERENCE_SERVER_POW,
|
|
||||||
ServerPowEngine(ctx),
|
|
||||||
ServicePowEngine(ctx)
|
|
||||||
)
|
|
||||||
cryptography = AndroidCryptography()
|
|
||||||
nodeRegistry = AndroidNodeRegistry(sqlHelper)
|
|
||||||
inventory = AndroidInventory(sqlHelper)
|
|
||||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
|
||||||
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
|
|
||||||
messageRepo = AndroidMessageRepository(sqlHelper)
|
|
||||||
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
|
|
||||||
networkHandler = NioNetworkHandler()
|
|
||||||
listener = getMessageListener(ctx)
|
|
||||||
labeler = Singleton.labeler
|
|
||||||
preferences.sendPubkeyOnIdentityCreation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getProofOfWorkRepository(ctx: Context) = powRepo ?: getBitmessageContext(ctx).internals.proofOfWorkRepository
|
|
||||||
|
|
||||||
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,20 +0,0 @@
|
|||||||
package ch.dissem.apps.abit.service
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import ch.dissem.apps.abit.util.NetworkUtils
|
|
||||||
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 == "android.intent.action.BOOT_COMPLETED") {
|
|
||||||
if (Preferences.isFullNodeActive(context)) {
|
|
||||||
NetworkUtils.enableNode(context, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package ch.dissem.apps.abit.service
|
|
||||||
|
|
||||||
import android.app.job.JobParameters
|
|
||||||
import android.app.job.JobService
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.annotation.RequiresApi
|
|
||||||
import ch.dissem.apps.abit.util.NetworkUtils
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
class StartupNodeOnWifiService : JobService() {
|
|
||||||
override fun onStartJob(params: JobParameters?): Boolean {
|
|
||||||
val bmc = Singleton.getBitmessageContext(this)
|
|
||||||
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
|
|
||||||
NetworkUtils.doStartBitmessageService(applicationContext)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopJob(params: JobParameters?) = if (Preferences.isWifiOnly(this)) {
|
|
||||||
// Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
|
|
||||||
// depending on Android version.
|
|
||||||
Preferences.isFullNodeActive(this)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Christian Basler
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ch.dissem.apps.abit.synchronization
|
|
||||||
|
|
||||||
import android.accounts.AbstractAccountAuthenticator
|
|
||||||
import android.accounts.Account
|
|
||||||
import android.accounts.AccountAuthenticatorResponse
|
|
||||||
import android.accounts.NetworkErrorException
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implement AbstractAccountAuthenticator and stub out all
|
|
||||||
* of its methods
|
|
||||||
*/
|
|
||||||
class Authenticator(context: Context) : AbstractAccountAuthenticator(context) {
|
|
||||||
|
|
||||||
override fun editProperties(r: AccountAuthenticatorResponse, s: String) =
|
|
||||||
throw UnsupportedOperationException("Editing properties is not supported")
|
|
||||||
|
|
||||||
// Don't add additional accounts
|
|
||||||
@Throws(NetworkErrorException::class)
|
|
||||||
override fun addAccount(r: AccountAuthenticatorResponse, s: String, s2: String, strings: Array<String>, bundle: Bundle) = null
|
|
||||||
|
|
||||||
// Ignore attempts to confirm credentials
|
|
||||||
@Throws(NetworkErrorException::class)
|
|
||||||
override fun confirmCredentials(r: AccountAuthenticatorResponse, account: Account, bundle: Bundle) = null
|
|
||||||
|
|
||||||
@Throws(NetworkErrorException::class)
|
|
||||||
override fun getAuthToken(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
|
|
||||||
throw UnsupportedOperationException("Getting an authentication token is not supported")
|
|
||||||
|
|
||||||
override fun getAuthTokenLabel(s: String) =
|
|
||||||
throw UnsupportedOperationException("Getting a label for the auth token is not supported")
|
|
||||||
|
|
||||||
@Throws(NetworkErrorException::class)
|
|
||||||
override fun updateCredentials(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
|
|
||||||
throw UnsupportedOperationException("Updating user credentials is not supported")
|
|
||||||
|
|
||||||
@Throws(NetworkErrorException::class)
|
|
||||||
override fun hasFeatures(r: AccountAuthenticatorResponse, account: Account, strings: Array<String>) =
|
|
||||||
throw UnsupportedOperationException("Checking features for the account is not supported")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val ACCOUNT_SYNC = Account("Bitmessage", "ch.dissem.bitmessage")
|
|
||||||
val ACCOUNT_POW = Account("Proof of Work ", "ch.dissem.bitmessage")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +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.synchronization
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bound Service that instantiates the authenticator
|
|
||||||
* when started.
|
|
||||||
*/
|
|
||||||
class AuthenticatorService : Service() {
|
|
||||||
/**
|
|
||||||
* Instance field that stores the authenticator object
|
|
||||||
*/
|
|
||||||
private var authenticator: Authenticator? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
// Create a new authenticator object
|
|
||||||
authenticator = Authenticator(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When the system binds to this Service to make the RPC call
|
|
||||||
* return the authenticator's IBinder.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent) = authenticator?.iBinder
|
|
||||||
}
|
|
@ -1,72 +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.synchronization
|
|
||||||
|
|
||||||
import android.content.ContentProvider
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.net.Uri
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Define an implementation of ContentProvider that stubs out
|
|
||||||
* all methods
|
|
||||||
*/
|
|
||||||
class StubProvider : ContentProvider() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Always return true, indicating that the
|
|
||||||
* provider loaded correctly.
|
|
||||||
*/
|
|
||||||
override fun onCreate() = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return no type for MIME type
|
|
||||||
*/
|
|
||||||
override fun getType(uri: Uri) = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query() always returns no results
|
|
||||||
*/
|
|
||||||
override fun query(
|
|
||||||
uri: Uri,
|
|
||||||
projection: Array<String>?,
|
|
||||||
selection: String?,
|
|
||||||
selectionArgs: Array<String>?,
|
|
||||||
sortOrder: String?) = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* insert() always returns null (no URI)
|
|
||||||
*/
|
|
||||||
override fun insert(uri: Uri, values: ContentValues?) = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete() always returns "no rows affected" (0)
|
|
||||||
*/
|
|
||||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update() always returns "no rows affected" (0)
|
|
||||||
*/
|
|
||||||
override fun update(
|
|
||||||
uri: Uri,
|
|
||||||
values: ContentValues?,
|
|
||||||
selection: String?,
|
|
||||||
selectionArgs: Array<String>?) = 0
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val AUTHORITY = "ch.dissem.apps.abit.provider"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,185 +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.synchronization
|
|
||||||
|
|
||||||
import android.accounts.Account
|
|
||||||
import android.accounts.AccountManager
|
|
||||||
import android.content.*
|
|
||||||
import android.os.Bundle
|
|
||||||
import ch.dissem.apps.abit.service.Singleton
|
|
||||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
|
|
||||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
|
|
||||||
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
|
|
||||||
import ch.dissem.apps.abit.util.Preferences
|
|
||||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
|
||||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
|
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
|
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
|
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE
|
|
||||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync Adapter to synchronize with the Bitmessage network - fetches
|
|
||||||
* new objects and then disconnects.
|
|
||||||
*/
|
|
||||||
class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) {
|
|
||||||
|
|
||||||
private val bmc = Singleton.getBitmessageContext(context)
|
|
||||||
|
|
||||||
override fun onPerformSync(
|
|
||||||
account: Account,
|
|
||||||
extras: Bundle,
|
|
||||||
authority: String,
|
|
||||||
provider: ContentProviderClient,
|
|
||||||
syncResult: SyncResult
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (account == ACCOUNT_SYNC) {
|
|
||||||
if (Preferences.isConnectionAllowed(context)) {
|
|
||||||
syncData()
|
|
||||||
}
|
|
||||||
} else if (account == ACCOUNT_POW) {
|
|
||||||
syncPOW()
|
|
||||||
} else {
|
|
||||||
syncResult.stats.numAuthExceptions++
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
syncResult.stats.numIoExceptions++
|
|
||||||
} catch (e: DecryptionFailedException) {
|
|
||||||
syncResult.stats.numAuthExceptions++
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun syncData() {
|
|
||||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
|
||||||
if (bmc.isRunning()) {
|
|
||||||
LOG.info("Synchronization skipped, Abit is acting as a full node")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val trustedNode = Preferences.getTrustedNode(context)
|
|
||||||
if (trustedNode == null) {
|
|
||||||
LOG.info("Trusted node not available, disabling synchronization")
|
|
||||||
stopSync(context)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
LOG.info("Synchronization started")
|
|
||||||
bmc.synchronize(
|
|
||||||
trustedNode,
|
|
||||||
Preferences.getTrustedNodePort(context),
|
|
||||||
Preferences.getTimeoutInSeconds(context),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
LOG.info("Synchronization finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun syncPOW() {
|
|
||||||
val identity = Singleton.getIdentity(context)
|
|
||||||
if (identity == null) {
|
|
||||||
LOG.info("No identity available - skipping POW synchronization")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val trustedNode = Preferences.getTrustedNode(context)
|
|
||||||
if (trustedNode == null) {
|
|
||||||
LOG.info("Trusted node not available, disabling POW synchronization")
|
|
||||||
stopPowSync(context)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
|
||||||
LOG.info("Looking for completed POW")
|
|
||||||
|
|
||||||
val privateKey = identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
|
|
||||||
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
|
|
||||||
val reader = ProofOfWorkRequest.Reader(identity)
|
|
||||||
val powRepo = Singleton.getProofOfWorkRepository(context)
|
|
||||||
val items = powRepo.getItems()
|
|
||||||
for (initialHash in items) {
|
|
||||||
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
|
|
||||||
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
|
|
||||||
val cryptoMsg = CryptoCustomMessage(
|
|
||||||
ProofOfWorkRequest(identity, initialHash, CALCULATE, target))
|
|
||||||
cryptoMsg.signAndEncrypt(identity, signingKey)
|
|
||||||
val response = bmc.send(
|
|
||||||
trustedNode,
|
|
||||||
Preferences.getTrustedNodePort(context),
|
|
||||||
cryptoMsg
|
|
||||||
)
|
|
||||||
if (response.isError) {
|
|
||||||
LOG.error("Server responded with error: ${String(response.getData())}")
|
|
||||||
} else {
|
|
||||||
val (_, _, request, data) = CryptoCustomMessage.read(response, reader).decrypt(privateKey)
|
|
||||||
if (request == COMPLETE) {
|
|
||||||
bmc.internals.proofOfWorkService.onNonceCalculated(initialHash, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
stopPowSync(context)
|
|
||||||
}
|
|
||||||
LOG.info("Synchronization finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LOG = LoggerFactory.getLogger(SyncAdapter::class.java)
|
|
||||||
|
|
||||||
private const val SYNC_FREQUENCY = 15 * 60L // seconds
|
|
||||||
|
|
||||||
fun startSync(ctx: Context) {
|
|
||||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
|
||||||
val account = addAccount(ctx, ACCOUNT_SYNC)
|
|
||||||
|
|
||||||
// Recommend a schedule for automatic synchronization. The system may modify this based
|
|
||||||
// on other scheduled syncs and network utilization.
|
|
||||||
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopSync(ctx: Context) {
|
|
||||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
|
||||||
val account = addAccount(ctx, ACCOUNT_SYNC)
|
|
||||||
|
|
||||||
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startPowSync(ctx: Context) {
|
|
||||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
|
||||||
val account = addAccount(ctx, ACCOUNT_POW)
|
|
||||||
|
|
||||||
// Recommend a schedule for automatic synchronization. The system may modify this based
|
|
||||||
// on other scheduled syncs and network utilization.
|
|
||||||
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopPowSync(ctx: Context) {
|
|
||||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
|
||||||
val account = addAccount(ctx, ACCOUNT_POW)
|
|
||||||
|
|
||||||
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addAccount(ctx: Context, account: Account): Account {
|
|
||||||
if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) {
|
|
||||||
// Inform the system that this account supports sync
|
|
||||||
ContentResolver.setIsSyncable(account, AUTHORITY, 1)
|
|
||||||
// Inform the system that this account is eligible for auto sync when the network is up
|
|
||||||
ContentResolver.setSyncAutomatically(account, AUTHORITY, true)
|
|
||||||
}
|
|
||||||
return account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.synchronization
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define a Service that returns an IBinder for the
|
|
||||||
* sync adapter class, allowing the sync adapter framework to call
|
|
||||||
* onPerformSync().
|
|
||||||
*/
|
|
||||||
class SyncService : Service() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate the sync adapter object.
|
|
||||||
*/
|
|
||||||
override fun onCreate() = synchronized(syncAdapterLock) {
|
|
||||||
if (syncAdapter == null) {
|
|
||||||
syncAdapter = SyncAdapter(this, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an object that allows the system to invoke
|
|
||||||
* the sync adapter.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent) = syncAdapter?.syncAdapterBinder
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Storage for an instance of the sync adapter
|
|
||||||
private var syncAdapter: SyncAdapter? = null
|
|
||||||
// Object to use as a thread-safe lock
|
|
||||||
private val syncAdapterLock = Any()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +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.support.annotation.DrawableRes
|
|
||||||
import android.support.annotation.StringRes
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@DrawableRes
|
|
||||||
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
|
|
||||||
Plaintext.Status.RECEIVED -> 0
|
|
||||||
Plaintext.Status.DRAFT -> R.drawable.draft
|
|
||||||
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
|
|
||||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
|
|
||||||
Plaintext.Status.SENT -> R.drawable.sent
|
|
||||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringRes
|
|
||||||
fun getStatusString(status: Plaintext.Status) = when (status) {
|
|
||||||
Plaintext.Status.RECEIVED -> R.string.status_received
|
|
||||||
Plaintext.Status.DRAFT -> R.string.status_draft
|
|
||||||
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
|
|
||||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
|
|
||||||
Plaintext.Status.SENT -> R.string.status_sent
|
|
||||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -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_WIFI_ONLY = "wifi_only"
|
|
||||||
const val PREFERENCE_TRUSTED_NODE = "trusted_node"
|
|
||||||
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout"
|
|
||||||
const val PREFERENCE_SERVER_POW = "server_pow"
|
|
||||||
const val PREFERENCE_FULL_NODE = "full_node"
|
|
||||||
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 BITMESSAGE_URL_SCHEMA = "bitmessage:"
|
|
||||||
|
|
||||||
val BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b")!!
|
|
||||||
}
|
|
@ -1,102 +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.util.Base64
|
|
||||||
import android.util.Base64.NO_WRAP
|
|
||||||
import android.util.Base64.URL_SAFE
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import ch.dissem.apps.abit.Identicon
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.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 {
|
|
||||||
private val LOG = LoggerFactory.getLogger(Drawables::class.java)
|
|
||||||
|
|
||||||
private const val QR_CODE_SIZE = 350
|
|
||||||
|
|
||||||
fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
|
|
||||||
val item = menu.findItem(menuItem)
|
|
||||||
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toBitmap(identicon: Identicon, width: Int, height: Int = width): Bitmap {
|
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
identicon.setBounds(0, 0, canvas.width, canvas.height)
|
|
||||||
identicon.draw(canvas)
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun qrCode(address: BitmessageAddress?): Bitmap? {
|
|
||||||
if (address == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val link = StringBuilder()
|
|
||||||
link.append(Constants.BITMESSAGE_URL_SCHEMA)
|
|
||||||
link.append(address.address)
|
|
||||||
if (address.alias != null) {
|
|
||||||
link.append("?label=").append(address.alias)
|
|
||||||
}
|
|
||||||
address.pubkey?.apply {
|
|
||||||
link.append(if (address.alias == null) '?' else '&')
|
|
||||||
val pubkey = ByteArrayOutputStream()
|
|
||||||
writer().writeUnencrypted(pubkey)
|
|
||||||
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
|
|
||||||
|
|
||||||
}
|
|
||||||
val result: BitMatrix
|
|
||||||
try {
|
|
||||||
result = MultiFormatWriter().encode(link.toString(),
|
|
||||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null)
|
|
||||||
} catch (e: WriterException) {
|
|
||||||
LOG.error(e.message, e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val w = result.width
|
|
||||||
val h = result.height
|
|
||||||
val pixels = IntArray(w * h)
|
|
||||||
for (y in 0 until h) {
|
|
||||||
val offset = y * w
|
|
||||||
for (x in 0 until w) {
|
|
||||||
pixels[offset + x] = if (result.get(x, y)) BLACK else WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
||||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,31 +0,0 @@
|
|||||||
package ch.dissem.apps.abit.util
|
|
||||||
|
|
||||||
import android.support.annotation.DrawableRes
|
|
||||||
import ch.dissem.apps.abit.MainActivity
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
|
||||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities to work with the common floating action button in the main activity
|
|
||||||
*/
|
|
||||||
object FabUtils {
|
|
||||||
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
|
||||||
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
|
||||||
fab.removeAllOnMenuItemClickListeners()
|
|
||||||
fab.show()
|
|
||||||
fab.closeMenu()
|
|
||||||
val mainFab = fab.mainFab
|
|
||||||
mainFab.setImageResource(drawableRes)
|
|
||||||
fab.setMenu(menu)
|
|
||||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
|
||||||
if (isOpened) {
|
|
||||||
// It will be turned 45 degrees, which makes an x out of the +
|
|
||||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
|
||||||
} else {
|
|
||||||
mainFab.setImageResource(drawableRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fab
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package ch.dissem.apps.abit.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.annotation.ColorInt
|
|
||||||
|
|
||||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
|
||||||
import com.mikepenz.iconics.typeface.IIcon
|
|
||||||
|
|
||||||
import ch.dissem.apps.abit.R
|
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to help with translating the default labels, getting label colors and so on.
|
|
||||||
*/
|
|
||||||
object Labels {
|
|
||||||
fun getText(label: Label, ctx: Context): String = getText(label.type, label.toString(), ctx)!!
|
|
||||||
|
|
||||||
fun getText(type: Label.Type?, alternative: String?, ctx: Context) = when (type) {
|
|
||||||
Label.Type.INBOX -> ctx.getString(R.string.inbox)
|
|
||||||
Label.Type.DRAFT -> ctx.getString(R.string.draft)
|
|
||||||
Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
|
|
||||||
Label.Type.SENT -> ctx.getString(R.string.sent)
|
|
||||||
Label.Type.UNREAD -> ctx.getString(R.string.unread)
|
|
||||||
Label.Type.TRASH -> ctx.getString(R.string.trash)
|
|
||||||
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
|
|
||||||
else -> alternative
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIcon(label: Label): IIcon = when (label.type) {
|
|
||||||
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
|
|
||||||
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
|
|
||||||
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
|
|
||||||
Label.Type.SENT -> CommunityMaterial.Icon.cmd_send
|
|
||||||
Label.Type.BROADCAST -> CommunityMaterial.Icon.cmd_rss
|
|
||||||
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
|
|
||||||
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
|
|
||||||
else -> CommunityMaterial.Icon.cmd_label
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getColor(label: Label) = if (label.type == null) {
|
|
||||||
label.color
|
|
||||||
} else 0xFF000000.toInt()
|
|
||||||
}
|
|
@ -1,64 +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 android.support.annotation.RequiresApi
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import ch.dissem.apps.abit.MainActivity
|
|
||||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
|
|
||||||
import ch.dissem.apps.abit.service.BitmessageService
|
|
||||||
import ch.dissem.apps.abit.service.StartupNodeOnWifiService
|
|
||||||
|
|
||||||
|
|
||||||
object NetworkUtils {
|
|
||||||
|
|
||||||
fun enableNode(ctx: Context, ask: Boolean = true) {
|
|
||||||
Preferences.setFullNodeActive(ctx, true)
|
|
||||||
if (Preferences.isWifiOnly(ctx)) {
|
|
||||||
if (Preferences.isConnectionAllowed(ctx)) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
scheduleNodeStart(ctx)
|
|
||||||
} else {
|
|
||||||
doStartBitmessageService(ctx)
|
|
||||||
MainActivity.updateNodeSwitch()
|
|
||||||
}
|
|
||||||
} else if (ask) {
|
|
||||||
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
|
|
||||||
if (ctx !is Activity) {
|
|
||||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
|
||||||
}
|
|
||||||
ctx.startActivity(dialogIntent)
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
scheduleNodeStart(ctx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
doStartBitmessageService(ctx)
|
|
||||||
MainActivity.updateNodeSwitch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doStartBitmessageService(ctx: Context) {
|
|
||||||
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableNode(ctx: Context) {
|
|
||||||
Preferences.setFullNodeActive(ctx, false)
|
|
||||||
ctx.stopService(Intent(ctx, BitmessageService::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
fun scheduleNodeStart(ctx: Context) {
|
|
||||||
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
|
|
||||||
val builder = JobInfo.Builder(0, serviceComponent)
|
|
||||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
|
|
||||||
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
|
|
||||||
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
|
||||||
jobScheduler.schedule(builder.build())
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user