Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

299 changed files with 3834 additions and 15572 deletions

179
.gitignore vendored
View File

@ -1,165 +1,18 @@
# Created by https://www.gitignore.io
### Android ###
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
/*/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
### Android Patch ###
gen-external-apklibs
### Gradle ###
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Eclipse ###
*.pydevproject
.metadata
.gradle
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# PDT-specific
.buildpath
# sbteclipse plugin
.target
# TeXlipse plugin
.texlipse
### Linux ###
*~
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### OSX ###
# Mac Specific
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
# Files that might appear on external disk
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
@ -168,24 +21,8 @@ Network Trash Folder
Temporary Items
.apdisk
# Jekyll Specific
_site/
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# Ruby
Gemfile.lock

48
404.html Normal file
View 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
View 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/>.

View File

@ -1,14 +1,56 @@
# Abit
# Carte Noire
A Bitmessage client for Android.
A simple Jekyll theme for blogging. Not named after the coffee.
**Please be aware that due to the protocol, sending messages might suck your
battery dry. Also, it causes a lot of traffic and may use up your data plan
rather quickly. Use at your own peril.**
![Homepage](http://i.imgur.com/xlmHArV.png)
Abit uses the [Jabit Bitmessage library](https://github.com/Dissem/Jabit).
### Article
![Article](http://i.imgur.com/8rD8FfC.png)
## Requirements
You'll need at least Android 4.4 KitKat. Due to the Proof of Work that comes
with the protocol you might want as fast a processor and as many cores as
you can get.
### Disqus Comments
![Comments](http://i.imgur.com/TBZHQwF.png)
### 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
View 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: "&lt;/&gt; on <a href=\"https://github.com/Dissem/Abit\">Github</a> &nbsp;<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
View 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
View 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
View 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
View 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>

View 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
View 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
View File

@ -0,0 +1 @@
{{ content }}

153
_layouts/page.html Normal file
View 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
View 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 %}

View 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/

View 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/).

View 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/

View 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
View File

@ -1 +0,0 @@
/build

View File

@ -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
}

View File

@ -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 *;
#}

View File

@ -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>

View File

@ -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
);

View File

@ -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'
);

View File

@ -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);

View File

@ -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);

View File

@ -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
);

View File

@ -1 +0,0 @@
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';

View File

@ -1,2 +0,0 @@
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
ALTER TABLE POW ADD COLUMN message_id BIGINT;

View File

@ -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;

View File

@ -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);

View File

@ -1 +0,0 @@
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);

View File

@ -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)
);

View File

@ -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"
}
}

View File

@ -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()
}
}
}

View File

@ -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"
}
}

View File

@ -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
)
}

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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]+)=(.*)$")
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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) }
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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 }
}
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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) }
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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 {
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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")!!
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

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

View File

@ -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()
}

View File

@ -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