Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f36f36ab3 | |||
| 66108c8618 | |||
| 3ae572bcf2 | |||
| d36ffe1939 | |||
| d704a40b66 | |||
| d3f1e6abd2 | |||
| 91cc90ec04 | |||
| 30c5bf6b90 | |||
| 572ecf1577 | |||
| a8dada6c89 | |||
| 911dfa7a27 | |||
| 26fca259bc | |||
| 55746743c4 | |||
| f77bbe1a43 | |||
| 8770575c95 | |||
| 61c9cde2f5 | |||
| e93dc43f89 | |||
| a203af654b | |||
| 6be31c9f4e | |||
| 42cf18445c | |||
| 22ac1920a2 | |||
| cd8192e99c | |||
| 300da1730d | |||
| 65c03bd638 | |||
| 18d72d727c | |||
| 37371a0e94 | |||
| 2c41aff3af | |||
| e74b18ed3f | |||
| d5407b7b01 | |||
| 249c97d4ba | |||
| 6f26e84f71 | |||
| 0a8459750a | |||
| 4be1f1e505 | |||
| 21fde7c22e | |||
| 96af8e0750 | |||
| b34e678c68 | |||
| 9af80f008d | |||
| edd1124c32 | |||
| e249c86b79 | |||
| 7332886786 | |||
| 6a8648ca28 | |||
| b3dd53a5df | |||
| 2b1fb436a9 | |||
| a5b3c33394 | |||
| dc3bfce9a2 | |||
| 4ab64c0ed1 | |||
| b94e48e544 | |||
| 9e8067cda5 | |||
| 30bb407bbf | |||
| a1fb11357b | |||
| 0efd17a356 | |||
| 91451b0ce7 | |||
| da6cd43a46 | |||
| bfd5a72b52 | |||
| dea9231fcf | |||
| 34000e7b79 | |||
| a18ef1ac29 | |||
| 246649d028 | |||
| c1d5af0034 | |||
| 8d58af423c | |||
| 5ea317c295 | |||
| cc4c16e970 | |||
| 1c226a6a5b | |||
| d416db1307 | |||
| 141c17a28c | |||
| c8a0301402 | |||
| af2bfc796b | |||
| dd9539aa3f | |||
| da73388d8c | |||
| c3eef5d711 | |||
| 044776acbb | |||
| e102908acf | |||
| f0e03f15a3 | |||
| 709e333e78 | |||
| f705e13d0b | |||
| 0ecfbd3fb8 | |||
| 59f0bc7b74 | |||
| 5db5442064 | |||
| 563085ed79 | |||
| df121b25c6 | |||
| 17a99b6562 | |||
| a4f6642f6a | |||
| a3c3fc082d | |||
| d682df4cdb | |||
| 3d7c1a504e | |||
| 6bcb7fc50e | |||
| 9275f5ca9c | |||
| adfb3a920a | |||
| 491a8a0ccb | |||
| 4a854045e0 | |||
| c4d76fc8ce | |||
| 6f6a2e4e6c | |||
| 6ec25cfae7 | |||
| b4c5d8cc21 | |||
| 2d2916f498 | |||
| abd25f4010 | |||
| eb322d94a2 | |||
| 6d21a99b3d | |||
| 694fa97dce | |||
| fdc2277324 | |||
| 1c5aed0f6c | |||
| 41f4571bf6 | |||
| b0828ec1e5 | |||
| 73383aa2c2 | |||
| b676983ba4 | |||
| 055bd39a42 | |||
| 9040026965 | |||
| c44f702ba5 | |||
| 54a319638b | |||
| e98eefe2cc | |||
| 2a17bbe34b | |||
| 7fe7ee42fc | |||
| 725089c604 | |||
| f5bf5c8bca | |||
| 9b1bf6bdb3 | |||
| f19996f79c | |||
| e149efcfff | |||
| ec3f3d2ca9 | |||
| ed5fb69eaf | |||
| e8de311d7e | |||
| 32a36e57f6 | |||
| e232457811 | |||
| e4d7bf4893 | |||
| 64e0479a37 | |||
| 13cb804fc2 | |||
| fd415b9c8c | |||
| 1659aaa1ee | |||
| 67c06b9884 | |||
| 348fa8daed | |||
| 3c7fd02613 | |||
| a7ebfefdbc | |||
| dd8ac629b2 | |||
| a17685fd10 | |||
| 496fffe6ee | |||
| e5b00c7453 | |||
| 5e2d19df58 | |||
| cb2040b0ce | |||
| 89a5ada48a |
+171
-8
@@ -1,18 +1,165 @@
|
||||
# Mac Specific
|
||||
# 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 ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
# Files that might appear on external disk
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
@@ -21,8 +168,24 @@ Network Trash Folder
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
# Jekyll Specific
|
||||
_site/
|
||||
|
||||
# Ruby
|
||||
Gemfile.lock
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
title: 404
|
||||
permalink: /404.html
|
||||
---
|
||||
|
||||
<div class="container-fluid index error">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12 content-panel articles">
|
||||
<h1 class="header author-header">404</h1>
|
||||
|
||||
<div class="error-text">
|
||||
Sorry but this page doesn't seem to exist.
|
||||
</div>
|
||||
|
||||
<div class="error-text">
|
||||
<a href="{{ site.baseurl }}/">Home</a> |
|
||||
<a href="{{ site.baseurl }}/posts/">All Posts</a> |
|
||||
<a href="{{ site.baseurl }}/search/">Search</a>
|
||||
</div>
|
||||
|
||||
{% include social_links.html %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content-panel related">
|
||||
{% for post in site.posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Suggested Article</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
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/>.
|
||||
@@ -1,56 +1,14 @@
|
||||
# Carte Noire
|
||||
# Abit
|
||||
|
||||
A simple Jekyll theme for blogging. Not named after the coffee.
|
||||
A Bitmessage client for Android.
|
||||
|
||||

|
||||
**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.**
|
||||
|
||||
### Article
|
||||

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

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

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

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

|
||||
|
||||
## Contact
|
||||
If you wish to contact me regarding this theme please raise an issue on GitHub,
|
||||
tweet me [@_jacobtomlinson](http://www.twitter.com/_jacobtomlinson) or email me
|
||||
[jacob@jacobtomlinson.co.uk](mailto:jacob@jacobtomlinson.co.uk).
|
||||
|
||||
## Contribution
|
||||
Pull requests are very welcome.
|
||||
|
||||
## Theme
|
||||
This jekyll theme has been created from scratch. Ideas and inspiration are taken
|
||||
from other places but the code is my own.
|
||||
|
||||
## Tools and Libraries
|
||||
The following tools and libraries are used in this theme
|
||||
|
||||
### JavaScript
|
||||
* [jQuery](http://jquery.com/)
|
||||
* [MMenu](http://mmenu.frebsite.nl/)
|
||||
* [HighlightJS](https://highlightjs.org/)
|
||||
* [Simple Jekyll Search](https://github.com/christian-fei/Simple-Jekyll-Search)
|
||||
|
||||
### CSS
|
||||
* [Bootstrap](http://getbootstrap.com/)
|
||||
* [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
|
||||
|
||||
### Social
|
||||
* [AddThis](http://www.addthis.com/)
|
||||
* [Disqus](https://disqus.com/)
|
||||
|
||||
### Other
|
||||
* [Real Favicon Generator](http://realfavicongenerator.net/)
|
||||
* [Google Analytics](http://www.google.com/analytics/)
|
||||
|
||||
## License
|
||||
The jekyll theme, HTML, CSS and JavaScript is licensed under GPLv3 (unless stated otherwise in the file).
|
||||
## 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.
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely need to edit after that.
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site settings
|
||||
title: Abit
|
||||
email: chrigu.meyer@gmail.com
|
||||
description: > # this means to ignore newlines until "baseurl:"
|
||||
Write an awesome description for your new site here. You can edit this
|
||||
line in _config.yml. It will appear in your document head meta (for
|
||||
Google search results) and in your feed.xml site description.
|
||||
baseurl: "/Abit" # the subpath of your site, e.g. /blog
|
||||
url: "https://dissem.github.io" # the base hostname & protocol for your site
|
||||
title_description: "A Bitmessage client for Android™"
|
||||
title_image: "ic_launcher-web.png"
|
||||
#google_analytics: "UA-20365477-4"
|
||||
#disqus_account: cartenoire
|
||||
github_repository: https://github.com/Dissem/Abit
|
||||
|
||||
|
||||
# Social usernames/URLs
|
||||
#twitter_username: jekyllrb
|
||||
github_username: Dissem
|
||||
|
||||
# Footer
|
||||
footer_left: "Made with <i class=\"fa fa-heart\"></i> by <a href=\"https://twitter.com/Dissem\">Christian Basler</a>"
|
||||
footer_right: "</> on <a href=\"https://github.com/Dissem/Abit\">Github</a> <i class=\"fa fa-github-alt\"></i>"
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
|
||||
kramdown:
|
||||
input: GFM
|
||||
syntax_highlighter: rouge
|
||||
|
||||
permalink: pretty
|
||||
exclude: [vendor]
|
||||
@@ -1,2 +0,0 @@
|
||||
gravatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=500&d=mm"
|
||||
jekyll: "https://i.imgur.com/aRQcGSi.png"
|
||||
@@ -1,86 +0,0 @@
|
||||
<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 %}
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!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 +0,0 @@
|
||||
{{ content }}
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
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 %}
|
||||
@@ -1,187 +0,0 @@
|
||||
---
|
||||
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 %}
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
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/
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
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/).
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
layout: post
|
||||
title: Carte Noire in Action
|
||||
date: 2014-06-10 12:31:19
|
||||
summary: See what the different elements looks like.
|
||||
categories: jekyll
|
||||
thumbnail: cogs
|
||||
tags:
|
||||
- demo
|
||||
- action
|
||||
- carte
|
||||
- noire
|
||||
---
|
||||
|
||||
**Note** - This article is a derivative of ["See pixyll in action"][1], taken from the lovely jekyll theme [pixyll][4].
|
||||
|
||||
All links are easy to [locate and discern](#), yet don't detract from the harmony
|
||||
of a paragraph. The _same_ goes for italics and __bold__ elements. Even the the strikeout
|
||||
works if <del>for some reason you need to update your post</del>. For consistency's sake,
|
||||
<ins>The same goes for insertions</ins>, of course.
|
||||
|
||||
### Code, with syntax highlighting
|
||||
|
||||
Code blocks use the [peppermint][2] theme.
|
||||
|
||||
{% highlight ruby %}
|
||||
class Awesome < ActiveRecord::Base
|
||||
include EvenMoreAwesome
|
||||
|
||||
validates_presence_of :something
|
||||
validates :email, email_format: true
|
||||
|
||||
def initialize(email, name = nil)
|
||||
self.email = email
|
||||
self.name = name
|
||||
end
|
||||
end
|
||||
{% endhighlight %}
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<title>Title</title>
|
||||
|
||||
<style>body {width: 500px;}</style>
|
||||
|
||||
<script type="application/javascript">
|
||||
function $init() {return true;}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<p checked class="title" id='title'>Title</p>
|
||||
<!-- here goes the rest of the page -->
|
||||
</body>
|
||||
```
|
||||
|
||||
# Headings!
|
||||
|
||||
They're responsive, and well-proportioned (in `padding`, `line-height`, `margin`, and `font-size`).
|
||||
|
||||
##### They draw the perfect amount of attention
|
||||
|
||||
This allows your content to have the proper informational and contextual hierarchy. Yay.
|
||||
|
||||
### There are lists, too
|
||||
|
||||
* Apples
|
||||
* Oranges
|
||||
* Potatoes
|
||||
* Milk
|
||||
|
||||
1. Mow the lawn
|
||||
2. Feed the dog
|
||||
3. Dance
|
||||
|
||||
### Images look great, too
|
||||
|
||||

|
||||
|
||||
|
||||
### Stylish blockquotes included
|
||||
|
||||
You can use the markdown quote syntax, `>` for simple quotes.
|
||||
|
||||
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse quis porta mauris.
|
||||
|
||||
### LaTeX support
|
||||
|
||||
The default math delimiters are \$\$. Hence `$$ E = m \cdot c^2 $$` yields $$ E = m \cdot c^2 $$
|
||||
|
||||
And here's something more fancy:
|
||||
|
||||
$$ \zeta(s) = \frac{1}{\Gamma(s)} \int \limits_0^\infty x^{s-1} \sum_{n=1}^\infty e^{-nx} \mathrm{d}x = \frac{1}{\Gamma(s)} \int \limits_0^\infty \frac{x^{s-1}}{e^x - 1} \mathrm{d}x $$
|
||||
|
||||
|
||||
### There's more being added all the time
|
||||
|
||||
Checkout the [Github repository][3] to request,
|
||||
or add, features.
|
||||
|
||||
Happy writing.
|
||||
|
||||
[1]: http://pixyll.com/jekyll/pixyll/2014/06/10/see-pixyll-in-action/
|
||||
[2]: https://noahfrederick.com/log/lion-terminal-theme-peppermint/
|
||||
[3]: https://github.com/jacobtomlinson/carte-noire
|
||||
[4]: http://pixyll.com/
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
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/
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,94 @@
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
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 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ch.dissem.apps." + appName.toLowerCase()
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 25
|
||||
versionCode 12
|
||||
versionName "1.0-beta12"
|
||||
jackOptions.enabled = false
|
||||
multiDexEnabled true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ext.jabitVersion = '2.0.4'
|
||||
ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT'
|
||||
ext.supportVersion = '25.3.1'
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
compile "com.android.support:appcompat-v7:$supportVersion"
|
||||
compile "com.android.support:support-v4:$supportVersion"
|
||||
compile "com.android.support:design:$supportVersion"
|
||||
compile "com.android.support:multidex:1.0.1"
|
||||
|
||||
compile "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
compile "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
|
||||
compile 'org.slf4j:slf4j-android:1.7.25'
|
||||
|
||||
compile 'com.mikepenz:materialize:1.0.1@aar'
|
||||
compile('com.mikepenz:materialdrawer:5.9.0@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile('com.mikepenz:aboutlibraries:5.9.5@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile "com.mikepenz:iconics-core:2.8.3@aar"
|
||||
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
|
||||
compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar'
|
||||
|
||||
compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
|
||||
compile 'com.google.zxing:core:3.3.0'
|
||||
|
||||
compile 'io.github.yavski:fab-speed-dial:1.0.6'
|
||||
compile 'com.github.amlcurran.showcaseview:library:5.4.3'
|
||||
compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile 'com.github.angads25:filepicker:1.1.0'
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:2.7.22'
|
||||
}
|
||||
|
||||
idea.module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
|
||||
android {
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
# 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 *;
|
||||
#}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="ch.dissem.apps.abit"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_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:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
tools:replace="android:allowBackup">
|
||||
<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=".SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<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">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
android:parentActivityName=".SettingsActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".SettingsActivity"/>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
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'
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
|
||||
ALTER TABLE POW ADD COLUMN message_id BIGINT;
|
||||
@@ -0,0 +1,4 @@
|
||||
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;
|
||||
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
@@ -0,0 +1 @@
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);
|
||||
@@ -0,0 +1,11 @@
|
||||
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)
|
||||
);
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder {
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
*/
|
||||
private static final String STATE_ACTIVATED_POSITION = "activated_position";
|
||||
/**
|
||||
* A dummy implementation of the {@link ListSelectionListener} interface that does
|
||||
* nothing. Used only when this fragment is not attached to an activity.
|
||||
*/
|
||||
private static final ListSelectionListener<Object> dummyCallbacks =
|
||||
new ListSelectionListener<Object>() {
|
||||
@Override
|
||||
public void onItemSelected(Object item) {
|
||||
// NO OP
|
||||
}
|
||||
};
|
||||
/**
|
||||
* The fragment's current callback object, which is notified of list item
|
||||
* clicks.
|
||||
*/
|
||||
private ListSelectionListener<? super T> callbacks = dummyCallbacks;
|
||||
/**
|
||||
* The current activated item position. Only used on tablets.
|
||||
*/
|
||||
private int activatedPosition = ListView.INVALID_POSITION;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
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
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
? ListView.CHOICE_MODE_SINGLE
|
||||
: ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (context instanceof ListSelectionListener) {
|
||||
//noinspection unchecked
|
||||
callbacks = (ListSelectionListener) context;
|
||||
} else {
|
||||
throw new IllegalStateException("Activity must implement fragment's callbacks.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
|
||||
// Reset the active callbacks interface to the dummy implementation.
|
||||
callbacks = dummyCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView listView, View view, int position, long id) {
|
||||
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.
|
||||
//noinspection unchecked
|
||||
callbacks.onItemSelected((T) listView.getItemAtPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
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.
|
||||
*/
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
|
||||
if (isVisible()) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
? ListView.CHOICE_MODE_SINGLE
|
||||
: ListView.CHOICE_MODE_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setActivatedPosition(int position) {
|
||||
if (position == ListView.INVALID_POSITION) {
|
||||
getListView().setItemChecked(activatedPosition, false);
|
||||
} else {
|
||||
getListView().setItemChecked(position, true);
|
||||
}
|
||||
|
||||
activatedPosition = position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 {@link MainActivity}.
|
||||
* <p/>
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a {@link AddressDetailFragment}.
|
||||
*/
|
||||
public class AddressDetailActivity extends DetailActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
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.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
|
||||
getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM));
|
||||
AddressDetailFragment fragment = new AddressDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a {@link MainActivity}
|
||||
* in two-pane mode (on tablets) or a {@link MessageDetailActivity}
|
||||
* on handsets.
|
||||
*/
|
||||
public class AddressDetailFragment extends Fragment {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM = "item";
|
||||
public static final String EXPORT_POSTFIX = ".keys.dat";
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private BitmessageAddress item;
|
||||
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public AddressDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments().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 = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.address, menu);
|
||||
|
||||
FragmentActivity activity = getActivity();
|
||||
Drawables.addIcon(activity, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail);
|
||||
Drawables.addIcon(activity, menu, R.id.share, GoogleMaterial.Icon.gmd_share);
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete);
|
||||
Drawables.addIcon(activity, menu, R.id.export,
|
||||
CommunityMaterial.Icon.cmd_export)
|
||||
.setVisible(item != null && item.getPrivateKey() != null);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
final Activity ctx = getActivity();
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.write_message: {
|
||||
BitmessageAddress identity = Singleton.getIdentity(ctx);
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Intent intent = new Intent(ctx, ComposeMessageActivity.class);
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity);
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item);
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.delete: {
|
||||
int warning;
|
||||
if (item.getPrivateKey() != null)
|
||||
warning = R.string.delete_identity_warning;
|
||||
else
|
||||
warning = R.string.delete_contact_warning;
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setMessage(warning)
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Singleton.getAddressRepository(ctx).remove(item);
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (item.getPrivateKey() != null && mainActivity != null) {
|
||||
mainActivity.removeIdentityEntry(item);
|
||||
}
|
||||
item = null;
|
||||
ctx.onBackPressed();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
case R.id.export: {
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setMessage(R.string.confirm_export)
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TITLE, item +
|
||||
EXPORT_POSTFIX);
|
||||
WifExporter exporter = new WifExporter(Singleton
|
||||
.getBitmessageContext(ctx));
|
||||
exporter.addIdentity(item);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString
|
||||
());
|
||||
startActivity(Intent.createChooser(shareIntent, null));
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
case R.id.share: {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress());
|
||||
startActivity(Intent.createChooser(shareIntent, null));
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_address_detail, container, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
if (item != null) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (item.isChan()) {
|
||||
activity.setTitle(R.string.title_chan_detail);
|
||||
} else if (item.getPrivateKey() != null) {
|
||||
activity.setTitle(R.string.title_identity_detail);
|
||||
} else if (item.isSubscribed()) {
|
||||
activity.setTitle(R.string.title_subscription_detail);
|
||||
} else {
|
||||
activity.setTitle(R.string.title_contact_detail);
|
||||
}
|
||||
|
||||
((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item));
|
||||
TextView name = (TextView) rootView.findViewById(R.id.name);
|
||||
name.setText(item.toString());
|
||||
name.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
item.setAlias(s.toString());
|
||||
}
|
||||
});
|
||||
TextView address = (TextView) rootView.findViewById(R.id.address);
|
||||
address.setText(item.getAddress());
|
||||
address.setSelected(true);
|
||||
((TextView) rootView.findViewById(R.id.stream_number)).setText(
|
||||
getString(R.string.stream_number, item.getStream()));
|
||||
if (item.getPrivateKey() == null) {
|
||||
Switch active = (Switch) rootView.findViewById(R.id.active);
|
||||
active.setChecked(item.isSubscribed());
|
||||
active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton button, boolean checked) {
|
||||
item.setSubscribed(checked);
|
||||
}
|
||||
});
|
||||
|
||||
ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id
|
||||
.pubkey_available);
|
||||
|
||||
if (item.getPubkey() == null) {
|
||||
pubkeyAvailableImg.setAlpha(0.3f);
|
||||
TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id
|
||||
.pubkey_available_desc);
|
||||
pubkeyAvailableDesc.setText(R.string.pubkey_not_available);
|
||||
}
|
||||
} else {
|
||||
rootView.findViewById(R.id.active).setVisibility(View.GONE);
|
||||
rootView.findViewById(R.id.pubkey_available).setVisibility(View.GONE);
|
||||
rootView.findViewById(R.id.pubkey_available_desc).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// QR code
|
||||
ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code);
|
||||
qrCode.setImageBitmap(Drawables.qrCode(item));
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (item != null) {
|
||||
Singleton.getAddressRepository(getContext()).save(item);
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null && item.getPrivateKey() != null) {
|
||||
mainActivity.updateIdentityEntry(item);
|
||||
}
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import io.github.yavski.fabspeeddial.FabSpeedDial;
|
||||
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
|
||||
|
||||
/**
|
||||
* Fragment that shows a list of all contacts, the ones we subscribed to first.
|
||||
*/
|
||||
public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
updateList();
|
||||
}
|
||||
|
||||
public void updateList() {
|
||||
List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext())
|
||||
.getContacts();
|
||||
Collections.sort(addresses, new Comparator<BitmessageAddress>() {
|
||||
@Override
|
||||
public int compare(BitmessageAddress lhs, BitmessageAddress rhs) {
|
||||
// Yields the following order:
|
||||
// * Subscribed addresses come first
|
||||
// * Addresses with Aliases (alphabetically)
|
||||
// * Addresses (alphabetically)
|
||||
if (lhs.isSubscribed() == rhs.isSubscribed()) {
|
||||
if (lhs.getAlias() != null) {
|
||||
if (rhs.getAlias() != null) {
|
||||
return lhs.getAlias().compareTo(rhs.getAlias());
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (rhs.getAlias() != null) {
|
||||
return 1;
|
||||
} else {
|
||||
return lhs.getAddress().compareTo(rhs.getAddress());
|
||||
}
|
||||
}
|
||||
if (lhs.isSubscribed()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
setListAdapter(new ArrayAdapter<BitmessageAddress>(
|
||||
getActivity(),
|
||||
android.R.layout.simple_list_item_activated_1,
|
||||
android.R.id.text1,
|
||||
addresses) {
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
convertView = inflater.inflate(R.layout.subscription_row, parent, false);
|
||||
}
|
||||
BitmessageAddress item = getItem(position);
|
||||
assert item != null;
|
||||
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new
|
||||
Identicon(item));
|
||||
TextView name = (TextView) convertView.findViewById(R.id.name);
|
||||
name.setText(item.toString());
|
||||
TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number);
|
||||
streamNumber.setText(getContext().getString(R.string.stream_number,
|
||||
item.getStream()));
|
||||
convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ?
|
||||
View.VISIBLE : View.INVISIBLE);
|
||||
return convertView;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context ctx) {
|
||||
super.onAttach(ctx);
|
||||
if (ctx instanceof ActionBarListener) {
|
||||
((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_address_list, container, false);
|
||||
|
||||
FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact);
|
||||
fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() {
|
||||
@Override
|
||||
public boolean onMenuItemSelected(MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_read_qr_code:
|
||||
IntentIntegrator.forSupportFragment(AddressListFragment.this)
|
||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
|
||||
.initiateScan();
|
||||
return true;
|
||||
case R.id.action_create_contact:
|
||||
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && data.hasExtra("SCAN_RESULT")) {
|
||||
Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT"));
|
||||
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateList(Label label) {
|
||||
updateList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget.Toolbar;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
public class ComposeMessageActivity extends AppCompatActivity {
|
||||
public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER";
|
||||
public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT";
|
||||
public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT";
|
||||
public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT";
|
||||
public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST";
|
||||
public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING";
|
||||
public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.toolbar_layout);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(false);
|
||||
|
||||
// Display the fragment as the main content.
|
||||
ComposeMessageFragment fragment = new ComposeMessageFragment();
|
||||
fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static void launchReplyTo(Fragment fragment, Plaintext item) {
|
||||
fragment.startActivity(getReplyIntent(fragment.getActivity(), item));
|
||||
}
|
||||
|
||||
public static void launchReplyTo(Activity activity, Plaintext item) {
|
||||
activity.startActivity(getReplyIntent(activity, item));
|
||||
}
|
||||
|
||||
private static Intent getReplyIntent(Context ctx, Plaintext item) {
|
||||
Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class);
|
||||
BitmessageAddress receivingIdentity = item.getTo();
|
||||
if (receivingIdentity.isChan()) {
|
||||
// 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.getFrom());
|
||||
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.getEncoding() == EXTENDED) {
|
||||
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED);
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_PARENT, item);
|
||||
String prefix;
|
||||
if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3)
|
||||
.equalsIgnoreCase("RE:")) {
|
||||
prefix = "";
|
||||
} else {
|
||||
prefix = "RE: ";
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject());
|
||||
replyIntent.putExtra(EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n"
|
||||
+ item.getText());
|
||||
return replyIntent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.ContactAdapter;
|
||||
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_PARENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
public class ComposeMessageFragment extends Fragment {
|
||||
private BitmessageAddress identity;
|
||||
private BitmessageAddress recipient;
|
||||
private String subject;
|
||||
private String content;
|
||||
private AutoCompleteTextView recipientInput;
|
||||
private EditText subjectInput;
|
||||
private EditText bodyInput;
|
||||
private boolean broadcast;
|
||||
private Plaintext.Encoding encoding;
|
||||
private Plaintext parent;
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public ComposeMessageFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
if (getArguments().containsKey(EXTRA_IDENTITY)) {
|
||||
identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY);
|
||||
if (getActivity() != null) {
|
||||
if (identity == null || identity.getPrivateKey() == null) {
|
||||
identity = Singleton.getIdentity(getActivity());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("No identity set for ComposeMessageFragment");
|
||||
}
|
||||
broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false);
|
||||
if (getArguments().containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getArguments().getString(EXTRA_SUBJECT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_CONTENT)) {
|
||||
content = getArguments().getString(EXTRA_CONTENT);
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_ENCODING)) {
|
||||
encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING);
|
||||
} else {
|
||||
encoding = Plaintext.Encoding.SIMPLE;
|
||||
}
|
||||
if (getArguments().containsKey(EXTRA_PARENT)) {
|
||||
parent = (Plaintext) getArguments().getSerializable(EXTRA_PARENT);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("No identity set for ComposeMessageFragment");
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false);
|
||||
recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient);
|
||||
if (broadcast) {
|
||||
recipientInput.setVisibility(View.GONE);
|
||||
} else {
|
||||
final ContactAdapter adapter = new ContactAdapter(getContext());
|
||||
recipientInput.setAdapter(adapter);
|
||||
recipientInput.setOnItemClickListener(
|
||||
new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
|
||||
adapter.getItem(pos);
|
||||
}
|
||||
}
|
||||
);
|
||||
recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long
|
||||
id) {
|
||||
recipient = adapter.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
if (recipient != null) {
|
||||
recipientInput.setText(recipient.toString());
|
||||
}
|
||||
}
|
||||
subjectInput = (EditText) rootView.findViewById(R.id.subject);
|
||||
subjectInput.setText(subject);
|
||||
bodyInput = (EditText) rootView.findViewById(R.id.body);
|
||||
bodyInput.setText(content);
|
||||
|
||||
if (recipient == null) {
|
||||
recipientInput.requestFocus();
|
||||
} else if (subject == null || subject.isEmpty()) {
|
||||
subjectInput.requestFocus();
|
||||
} else {
|
||||
bodyInput.requestFocus();
|
||||
bodyInput.setSelection(0);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (identity == null || identity.getPrivateKey() == null) {
|
||||
identity = Singleton.getIdentity(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.compose, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.send:
|
||||
send();
|
||||
return true;
|
||||
case R.id.select_encoding:
|
||||
SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(EXTRA_ENCODING, encoding);
|
||||
encodingDialog.setArguments(args);
|
||||
encodingDialog.setTargetFragment(this, 0);
|
||||
encodingDialog.show(getFragmentManager(), "select encoding dialog");
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 0 && resultCode == RESULT_OK) {
|
||||
encoding = (Plaintext.Encoding) data.getSerializableExtra(EXTRA_ENCODING);
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void send() {
|
||||
Plaintext.Builder builder;
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(getContext());
|
||||
if (broadcast) {
|
||||
builder = new Plaintext.Builder(BROADCAST)
|
||||
.from(identity);
|
||||
} else {
|
||||
String inputString = recipientInput.getText().toString();
|
||||
if (recipient == null || !recipient.toString().equals(inputString)) {
|
||||
try {
|
||||
recipient = new BitmessageAddress(inputString);
|
||||
} catch (Exception e) {
|
||||
List<BitmessageAddress> contacts = Singleton.getAddressRepository
|
||||
(getContext()).getContacts();
|
||||
for (BitmessageAddress contact : contacts) {
|
||||
if (inputString.equalsIgnoreCase(contact.getAlias())) {
|
||||
recipient = contact;
|
||||
if (inputString.equals(contact.getAlias()))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
builder = new Plaintext.Builder(MSG)
|
||||
.from(identity)
|
||||
.to(recipient);
|
||||
}
|
||||
switch (encoding) {
|
||||
case SIMPLE:
|
||||
builder.message(
|
||||
subjectInput.getText().toString(),
|
||||
bodyInput.getText().toString()
|
||||
);
|
||||
break;
|
||||
case EXTENDED:
|
||||
builder.message(
|
||||
new Message.Builder()
|
||||
.subject(subjectInput.getText().toString())
|
||||
.body(bodyInput.getText().toString())
|
||||
.addParent(parent)
|
||||
.build()
|
||||
);
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(
|
||||
getContext(),
|
||||
getContext().getString(R.string.error_unsupported_encoding, encoding),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
builder.message(
|
||||
subjectInput.getText().toString(),
|
||||
bodyInput.getText().toString()
|
||||
);
|
||||
break;
|
||||
}
|
||||
bmc.send(builder.build());
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V2Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
|
||||
|
||||
import static android.util.Base64.URL_SAFE;
|
||||
|
||||
public class CreateAddressActivity extends AppCompatActivity {
|
||||
private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$");
|
||||
private byte[] pubkeyBytes;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri != null)
|
||||
setContentView(R.layout.activity_open_bitmessage_link);
|
||||
else
|
||||
setContentView(R.layout.activity_create_bitmessage_address);
|
||||
|
||||
final TextView address = (TextView) findViewById(R.id.address);
|
||||
final EditText label = (EditText) findViewById(R.id.label);
|
||||
final Switch subscribe = (Switch) findViewById(R.id.subscribe);
|
||||
|
||||
if (uri != null) {
|
||||
String addressText = getAddress(uri);
|
||||
String[] parameters = getParameters(uri);
|
||||
for (String parameter : parameters) {
|
||||
Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter);
|
||||
if (matcher.find()) {
|
||||
String key = matcher.group(1).toLowerCase();
|
||||
String value = matcher.group(2);
|
||||
switch (key) {
|
||||
case "label":
|
||||
label.setText(value.trim());
|
||||
break;
|
||||
case "action":
|
||||
subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe"));
|
||||
break;
|
||||
case "pubkey":
|
||||
pubkeyBytes = Base64.decode(value, URL_SAFE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address.setText(addressText);
|
||||
}
|
||||
|
||||
final Button cancel = (Button) findViewById(R.id.cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
final Button ok = (Button) findViewById(R.id.do_import);
|
||||
ok.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String addressText = String.valueOf(address.getText()).trim();
|
||||
try {
|
||||
BitmessageAddress bmAddress = new BitmessageAddress(addressText);
|
||||
bmAddress.setAlias(label.getText().toString());
|
||||
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext
|
||||
(CreateAddressActivity.this);
|
||||
bmc.addContact(bmAddress);
|
||||
if (subscribe.isChecked()) {
|
||||
bmc.addSubscribtion(bmAddress);
|
||||
}
|
||||
if (pubkeyBytes != null) {
|
||||
try {
|
||||
final Pubkey pubkey;
|
||||
InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes);
|
||||
long stream = bmAddress.getStream();
|
||||
switch ((int) bmAddress.getVersion()) {
|
||||
case 2:
|
||||
pubkey = V2Pubkey.read(pubkeyStream, stream);
|
||||
break;
|
||||
case 3:
|
||||
pubkey = V3Pubkey.read(pubkeyStream, stream);
|
||||
break;
|
||||
case 4:
|
||||
pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream));
|
||||
break;
|
||||
default:
|
||||
pubkey = null;
|
||||
}
|
||||
if (pubkey != null) {
|
||||
bmAddress.setPubkey(pubkey);
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK);
|
||||
finish();
|
||||
} catch (RuntimeException e) {
|
||||
address.setError(getString(R.string.error_illegal_address));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getAddress(Uri uri) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String schemeSpecificPart = uri.getSchemeSpecificPart();
|
||||
if (!schemeSpecificPart.startsWith("BM-")) {
|
||||
result.append("BM-");
|
||||
}
|
||||
if (schemeSpecificPart.contains("?")) {
|
||||
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')));
|
||||
} else if (schemeSpecificPart.contains("#")) {
|
||||
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')));
|
||||
} else {
|
||||
result.append(schemeSpecificPart);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String[] getParameters(Uri uri) {
|
||||
int index = uri.getSchemeSpecificPart().indexOf('?');
|
||||
if (index >= 0) {
|
||||
String parameterPart = uri.getSchemeSpecificPart().substring(index + 1);
|
||||
return parameterPart.split("&");
|
||||
} else {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.mikepenz.materialize.MaterializeBuilder;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public abstract class DetailActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scrolling_toolbar_layout);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
// Show the Up button in the action bar.
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
new MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case 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, new Intent(this, MainActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.support.annotation.NonNull;
|
||||
import android.text.TextPaint;
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class Identicon extends Drawable {
|
||||
private static final int SIZE = 9;
|
||||
private static final int CENTER_COLUMN = 5;
|
||||
|
||||
private final Paint paint;
|
||||
private final int color;
|
||||
private final int background;
|
||||
private final boolean[][] fields;
|
||||
private final boolean chan;
|
||||
private final TextPaint textPaint;
|
||||
|
||||
public Identicon(@NonNull BitmessageAddress input) {
|
||||
paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setAntiAlias(true);
|
||||
textPaint = new TextPaint();
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setColor(0xFF607D8B);
|
||||
textPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
|
||||
|
||||
chan = input.isChan();
|
||||
|
||||
byte[] hash = input.getRipe();
|
||||
|
||||
fields = new boolean[SIZE][SIZE];
|
||||
color = Color.HSVToColor(new float[]{
|
||||
Math.abs(hash[0] * hash[1] + hash[2]) % 360,
|
||||
0.8f,
|
||||
1.0f
|
||||
});
|
||||
background = Color.HSVToColor(new float[]{
|
||||
Math.abs(hash[1] * hash[2] + hash[0]) % 360,
|
||||
0.8f,
|
||||
1.0f
|
||||
});
|
||||
|
||||
for (int row = 0; row < SIZE; row++) {
|
||||
if (!chan || row < 5 || row > 6) {
|
||||
for (int column = 0; column <= CENTER_COLUMN; column++) {
|
||||
if (
|
||||
(row - SIZE / 2) * (row - SIZE / 2)
|
||||
+ (column - SIZE / 2) * (column - SIZE / 2)
|
||||
< SIZE / 2 * SIZE / 2
|
||||
) {
|
||||
fields[row][column] = hash[(row * CENTER_COLUMN + column) % hash.length]
|
||||
>= 0;
|
||||
fields[row][SIZE - column - 1] = fields[row][column];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
float x, y;
|
||||
float width = canvas.getWidth();
|
||||
float height = canvas.getHeight();
|
||||
float cellWidth = width / (float) SIZE;
|
||||
float cellHeight = height / (float) SIZE;
|
||||
paint.setColor(background);
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint);
|
||||
paint.setColor(color);
|
||||
for (int row = 0; row < SIZE; row++) {
|
||||
for (int column = 0; column < SIZE; column++) {
|
||||
if (fields[row][column]) {
|
||||
x = cellWidth * column;
|
||||
y = cellHeight * row;
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chan) {
|
||||
textPaint.setTextSize(2 * cellHeight);
|
||||
canvas.drawText("[chan]", width / 2, 6.7f * cellHeight, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
paint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSPARENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.annotation.Nullable;
|
||||
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 com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.wif.WifImporter;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class ImportIdentitiesFragment extends Fragment {
|
||||
public static final String WIF_DATA = "wif_data";
|
||||
private AddressSelectorAdapter adapter;
|
||||
private WifImporter importer;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
String wifData = getArguments().getString(WIF_DATA);
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(getActivity());
|
||||
View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false);
|
||||
try {
|
||||
importer = new WifImporter(bmc, wifData);
|
||||
adapter = new AddressSelectorAdapter(importer.getIdentities());
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(),
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false);
|
||||
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
recyclerView.addItemDecoration(new SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(getActivity(), R.drawable.list_divider_h), true));
|
||||
} catch (IOException e) {
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
importer.importAll(adapter.getSelected());
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
for (BitmessageAddress selected : adapter.getSelected()) {
|
||||
mainActivity.addIdentityEntry(selected);
|
||||
}
|
||||
}
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class ImportIdentityActivity extends DetailActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String wifData;
|
||||
if (savedInstanceState == null) {
|
||||
wifData = null;
|
||||
} else {
|
||||
wifData = savedInstanceState.getString(WIF_DATA);
|
||||
}
|
||||
if (wifData == null) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, new InputWifFragment())
|
||||
.commit();
|
||||
} else {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(WIF_DATA, wifData);
|
||||
|
||||
ImportIdentitiesFragment fragment = new ImportIdentitiesFragment();
|
||||
fragment.setArguments(bundle);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.angads25.filepicker.controller.DialogSelectionListener;
|
||||
import com.github.angads25.filepicker.model.DialogConfigs;
|
||||
import com.github.angads25.filepicker.model.DialogProperties;
|
||||
import com.github.angads25.filepicker.view.FilePickerDialog;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class InputWifFragment extends Fragment {
|
||||
private TextView wifData;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_import_input, container, false);
|
||||
wifData = (TextView) view.findViewById(R.id.wif_input);
|
||||
|
||||
view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(WIF_DATA, wifData.getText().toString());
|
||||
|
||||
ImportIdentitiesFragment fragment = new ImportIdentitiesFragment();
|
||||
fragment.setArguments(bundle);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.import_input_data, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
DialogProperties properties = new DialogProperties();
|
||||
properties.selection_mode = DialogConfigs.SINGLE_MODE;
|
||||
properties.selection_type = DialogConfigs.FILE_SELECT;
|
||||
properties.root = new File(DialogConfigs.DEFAULT_DIR);
|
||||
properties.error_dir = new File(DialogConfigs.DEFAULT_DIR);
|
||||
properties.extensions = null;
|
||||
FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties);
|
||||
dialog.setTitle(getString(R.string.select_file_title));
|
||||
dialog.setDialogSelectionListener(new DialogSelectionListener() {
|
||||
@Override
|
||||
public void onSelectedFilePaths(String[] files) {
|
||||
if (files.length > 0) {
|
||||
try (InputStream in = new FileInputStream(files[0])) {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
//noinspection ConstantConditions
|
||||
while ((length = in.read(buffer)) != -1) {
|
||||
data.write(buffer, 0, length);
|
||||
}
|
||||
wifData.setText(data.toString("UTF-8"));
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(
|
||||
getActivity(),
|
||||
R.string.error_loading_data,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public interface ListHolder {
|
||||
void updateList(Label label);
|
||||
|
||||
void setActivateOnItemClick(boolean activateOnItemClick);
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
/*
|
||||
* 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.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
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.Display;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.amlcurran.showcaseview.ShowcaseView;
|
||||
import com.github.amlcurran.showcaseview.targets.Target;
|
||||
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.interfaces.OnCheckedChangeListener;
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.SwitchDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Nameable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment;
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity;
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository;
|
||||
import ch.dissem.apps.abit.service.BitmessageService;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter;
|
||||
import ch.dissem.apps.abit.util.Drawables;
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
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 static android.widget.Toast.LENGTH_LONG;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo;
|
||||
import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE;
|
||||
import static ch.dissem.apps.abit.service.BitmessageService.isRunning;
|
||||
|
||||
|
||||
/**
|
||||
* 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 {@link MessageDetailActivity} representing
|
||||
* item details. On tablets, the activity presents the list of items and
|
||||
* item details side-by-side using two vertical panes.
|
||||
* <p>
|
||||
* The activity makes heavy use of fragments. The list of items is a
|
||||
* {@link MessageListFragment} and the item details
|
||||
* (if present) is a {@link MessageDetailFragment}.
|
||||
* </p><p>
|
||||
* This activity also implements the required
|
||||
* {@link ListSelectionListener} interface
|
||||
* to listen for item selections.
|
||||
* </p>
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements ListSelectionListener<Serializable>, ActionBarListener {
|
||||
public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage";
|
||||
public static final String EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel";
|
||||
public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage";
|
||||
public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox";
|
||||
|
||||
private static final int ADD_IDENTITY = 1;
|
||||
private static final int MANAGE_IDENTITY = 2;
|
||||
|
||||
private static final long ID_NODE_SWITCH = 1;
|
||||
|
||||
private static WeakReference<MainActivity> instance;
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
*/
|
||||
private boolean twoPane;
|
||||
|
||||
private Label selectedLabel;
|
||||
|
||||
private BitmessageContext bmc;
|
||||
private AccountHeader accountHeader;
|
||||
|
||||
private Drawer drawer;
|
||||
private SwitchDrawerItem nodeSwitch;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = new WeakReference<>(this);
|
||||
bmc = Singleton.getBitmessageContext(this);
|
||||
|
||||
setContentView(R.layout.activity_message_list);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
MessageListFragment listFragment = new MessageListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.commit();
|
||||
|
||||
if (findViewById(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.
|
||||
twoPane = true;
|
||||
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true);
|
||||
}
|
||||
|
||||
createDrawer(toolbar);
|
||||
|
||||
// handle intents
|
||||
Intent intent = getIntent();
|
||||
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE));
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
|
||||
Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE);
|
||||
launchReplyTo(this, item);
|
||||
}
|
||||
|
||||
if (Preferences.useTrustedNode(this)) {
|
||||
SyncAdapter.startSync(this);
|
||||
} else {
|
||||
SyncAdapter.stopSync(this);
|
||||
}
|
||||
if (drawer.isDrawerOpen()) {
|
||||
RelativeLayout.LayoutParams lps = new RelativeLayout.LayoutParams(ViewGroup
|
||||
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue();
|
||||
lps.setMargins(margin, margin, margin, margin);
|
||||
|
||||
new ShowcaseView.Builder(this)
|
||||
.withMaterialShowcase()
|
||||
.setStyle(R.style.CustomShowcaseTheme)
|
||||
.setContentTitle(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setTarget(new Target() {
|
||||
@Override
|
||||
public Point getPoint() {
|
||||
View view = drawer.getStickyFooter();
|
||||
int[] location = new int[2];
|
||||
view.getLocationInWindow(location);
|
||||
int x = location[0] + 7 * view.getWidth() / 8;
|
||||
int y = location[1] + view.getHeight() / 2;
|
||||
return new Point(x, y);
|
||||
}
|
||||
})
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps);
|
||||
}
|
||||
}
|
||||
|
||||
private <F extends Fragment & ListHolder> void changeList(F listFragment) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
||||
if (twoPane) {
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void createDrawer(Toolbar toolbar) {
|
||||
final ArrayList<IProfile> profiles = new ArrayList<>();
|
||||
profiles.add(new ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons))
|
||||
.withIdentifier(ADD_IDENTITY)
|
||||
);
|
||||
profiles.add(new ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY)
|
||||
);
|
||||
// Create the AccountHeader
|
||||
accountHeader = new AccountHeaderBuilder()
|
||||
.withActivity(this)
|
||||
.withHeaderBackground(R.drawable.header)
|
||||
.withProfiles(profiles)
|
||||
.withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
|
||||
@Override
|
||||
public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
|
||||
if (current) {
|
||||
// Show QR code in modal dialog
|
||||
final Dialog dialog = new Dialog(MainActivity.this);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
ImageView imageView = new ImageView(MainActivity.this);
|
||||
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this)));
|
||||
imageView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog.addContentView(imageView, new RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
Display display = window.getWindowManager().getDefaultDisplay();
|
||||
Point size = new Point();
|
||||
display.getSize(size);
|
||||
int dim = size.x < size.y ? size.x : size.y;
|
||||
|
||||
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
|
||||
lp.copyFrom(window.getAttributes());
|
||||
lp.width = dim;
|
||||
lp.height = dim;
|
||||
|
||||
window.setAttributes(lp);
|
||||
}
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() {
|
||||
@Override
|
||||
public boolean onProfileChanged(View view, IProfile profile, boolean current) {
|
||||
switch ((int) profile.getIdentifier()) {
|
||||
case ADD_IDENTITY:
|
||||
addIdentityDialog();
|
||||
break;
|
||||
case MANAGE_IDENTITY:
|
||||
BitmessageAddress identity = Singleton.getIdentity(MainActivity.this);
|
||||
if (identity == null) {
|
||||
Toast.makeText(MainActivity.this,
|
||||
R.string.no_identity_warning, LENGTH_LONG).show();
|
||||
} else {
|
||||
Intent show = new Intent(MainActivity.this,
|
||||
AddressDetailActivity.class);
|
||||
show.putExtra(AddressDetailFragment.ARG_ITEM, identity);
|
||||
startActivity(show);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
Object tag = ((ProfileDrawerItem) profile).getTag();
|
||||
if (tag instanceof BitmessageAddress) {
|
||||
Singleton.setIdentity((BitmessageAddress) tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// false if it should close the drawer
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.build();
|
||||
if (profiles.size() > 2) { // There's always the add and manage identity items
|
||||
accountHeader.setActiveProfile(profiles.get(0), true);
|
||||
}
|
||||
|
||||
final ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.archive)
|
||||
.withTag(LABEL_ARCHIVE)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_archive)
|
||||
);
|
||||
drawerItems.add(new DividerDrawerItem());
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts));
|
||||
drawerItems.add(new PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings));
|
||||
|
||||
nodeSwitch = new SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
.withName(R.string.full_node)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
||||
.withChecked(isRunning())
|
||||
.withOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView,
|
||||
boolean isChecked) {
|
||||
if (isChecked) {
|
||||
checkAndStartNode();
|
||||
} else {
|
||||
stopService(new Intent(MainActivity.this, BitmessageService.class));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawer = new DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(accountHeader)
|
||||
.withDrawerItems(drawerItems)
|
||||
.addStickyDrawerItems(nodeSwitch)
|
||||
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
|
||||
@Override
|
||||
public boolean onItemClick(View view, int position, IDrawerItem item) {
|
||||
if (item.getTag() instanceof Label) {
|
||||
selectedLabel = (Label) item.getTag();
|
||||
if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof
|
||||
MessageListFragment) {
|
||||
((MessageListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.item_list)).updateList(selectedLabel);
|
||||
} else {
|
||||
MessageListFragment listFragment = new MessageListFragment();
|
||||
changeList(listFragment);
|
||||
listFragment.updateList(selectedLabel);
|
||||
}
|
||||
return false;
|
||||
} else if (item instanceof Nameable<?>) {
|
||||
Nameable<?> ni = (Nameable<?>) item;
|
||||
switch (ni.getName().getTextRes()) {
|
||||
case R.string.contacts_and_subscriptions:
|
||||
if (!(getSupportFragmentManager().findFragmentById(R.id
|
||||
.item_list) instanceof AddressListFragment)) {
|
||||
changeList(new AddressListFragment());
|
||||
} else {
|
||||
((AddressListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.item_list)).updateList();
|
||||
}
|
||||
break;
|
||||
case R.string.settings:
|
||||
startActivity(new Intent(MainActivity.this, SettingsActivity
|
||||
.class));
|
||||
break;
|
||||
case R.string.full_node:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withShowDrawerOnFirstLaunch(true)
|
||||
.build();
|
||||
|
||||
new AsyncTask<Void, Void, List<BitmessageAddress>>() {
|
||||
@Override
|
||||
protected List<BitmessageAddress> doInBackground(Void... params) {
|
||||
List<BitmessageAddress> identities = bmc.addresses().getIdentities();
|
||||
if (identities.isEmpty()) {
|
||||
// Create an initial identity
|
||||
Singleton.getIdentity(MainActivity.this);
|
||||
}
|
||||
return identities;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<BitmessageAddress> identities) {
|
||||
for (BitmessageAddress identity : identities) {
|
||||
addIdentityEntry(identity);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
|
||||
new AsyncTask<Void, Void, List<Label>>() {
|
||||
@Override
|
||||
protected List<Label> doInBackground(Void... params) {
|
||||
return bmc.messages().getLabels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Label> labels) {
|
||||
if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) {
|
||||
selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL);
|
||||
} else if (selectedLabel == null) {
|
||||
selectedLabel = labels.get(0);
|
||||
}
|
||||
for (Label label : labels) {
|
||||
addLabelEntry(label);
|
||||
}
|
||||
IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel);
|
||||
if (selectedDrawerItem != null) {
|
||||
drawer.setSelection(selectedDrawerItem);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
savedInstanceState.putSerializable("selectedLabel", selectedLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel");
|
||||
|
||||
IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel);
|
||||
if (selectedItem != null) {
|
||||
drawer.setSelection(selectedItem);
|
||||
}
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
private void addIdentityDialog() {
|
||||
AddIdentityDialogFragment dialog = new AddIdentityDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
updateUnread();
|
||||
updateNodeSwitch();
|
||||
Singleton.getMessageListener(this).resetNotification();
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
public void addIdentityEntry(BitmessageAddress identity) {
|
||||
IProfile newProfile = new ProfileDrawerItem()
|
||||
.withIcon(new Identicon(identity))
|
||||
.withName(identity.toString())
|
||||
.withNameShown(true)
|
||||
.withEmail(identity.getAddress())
|
||||
.withTag(identity);
|
||||
if (accountHeader.getProfiles() != null) {
|
||||
// we know that there are 2 setting elements.
|
||||
// Set the new profile above them ;)
|
||||
accountHeader.addProfile(
|
||||
newProfile, accountHeader.getProfiles().size() - 2);
|
||||
} else {
|
||||
accountHeader.addProfiles(newProfile);
|
||||
}
|
||||
}
|
||||
|
||||
public void addLabelEntry(Label label) {
|
||||
PrimaryDrawerItem item = new PrimaryDrawerItem()
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(Labels.getIcon(label))
|
||||
.withIconColor(Labels.getColor(label));
|
||||
drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3);
|
||||
}
|
||||
|
||||
public void updateIdentityEntry(BitmessageAddress identity) {
|
||||
for (IProfile profile : accountHeader.getProfiles()) {
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
if (identity.equals(((ProfileDrawerItem) profile).getTag())) {
|
||||
((ProfileDrawerItem) profile)
|
||||
.withName(identity.toString())
|
||||
.withTag(identity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeIdentityEntry(BitmessageAddress identity) {
|
||||
for (IProfile profile : accountHeader.getProfiles()) {
|
||||
if (profile instanceof ProfileDrawerItem) {
|
||||
if (identity.equals(((ProfileDrawerItem) profile).getTag())) {
|
||||
accountHeader.removeProfile(profile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndStartNode() {
|
||||
if (Preferences.isConnectionAllowed(MainActivity.this)) {
|
||||
startService(new Intent(this, BitmessageService.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, FullNodeDialogActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUnread() {
|
||||
for (IDrawerItem item : drawer.getDrawerItems()) {
|
||||
if (item.getTag() instanceof Label) {
|
||||
Label label = (Label) item.getTag();
|
||||
if (label != LABEL_ARCHIVE) {
|
||||
int unread = bmc.messages().countUnread(label);
|
||||
if (unread > 0) {
|
||||
((PrimaryDrawerItem) item).withBadge(String.valueOf(unread));
|
||||
} else {
|
||||
((PrimaryDrawerItem) item).withBadge((String) null);
|
||||
}
|
||||
drawer.updateItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateNodeSwitch() {
|
||||
final MainActivity i = getInstance();
|
||||
if (i != null) {
|
||||
i.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
i.nodeSwitch.withChecked(i.bmc.isRunning());
|
||||
i.drawer.updateStickyFooterItem(i.nodeSwitch);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from {@link ListSelectionListener}
|
||||
* indicating that the item with the given ID was selected.
|
||||
*/
|
||||
@Override
|
||||
public void onItemSelected(Serializable item) {
|
||||
if (twoPane) {
|
||||
// In two-pane mode, show the detail view in this activity by
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item);
|
||||
Fragment fragment;
|
||||
if (item instanceof Plaintext)
|
||||
fragment = new MessageDetailFragment();
|
||||
else if (item instanceof BitmessageAddress)
|
||||
fragment = new AddressDetailFragment();
|
||||
else
|
||||
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
|
||||
"was "
|
||||
+ item.getClass().getSimpleName());
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit();
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
Intent detailIntent;
|
||||
if (item instanceof Plaintext) {
|
||||
detailIntent = new Intent(this, MessageDetailActivity.class);
|
||||
detailIntent.putExtra(EXTRA_SHOW_LABEL, selectedLabel);
|
||||
} else if (item instanceof BitmessageAddress) {
|
||||
detailIntent = new Intent(this, AddressDetailActivity.class);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
|
||||
"was "
|
||||
+ item.getClass().getSimpleName());
|
||||
}
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
|
||||
startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTitle(CharSequence title) {
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
public Label getSelectedLabel() {
|
||||
return selectedLabel;
|
||||
}
|
||||
|
||||
public static MainActivity getInstance() {
|
||||
if (instance == null) return null;
|
||||
return instance.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
|
||||
/**
|
||||
* 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 {@link MainActivity}.
|
||||
* <p/>
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a {@link MessageDetailFragment}.
|
||||
*/
|
||||
public class MessageDetailActivity extends DetailActivity {
|
||||
private Label label;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
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) {
|
||||
label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL);
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
|
||||
getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM));
|
||||
MessageDetailFragment fragment = new MessageDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
Intent parentIntent = new Intent(this, MainActivity.class);
|
||||
parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label);
|
||||
NavUtils.navigateUpTo(this, parentIntent);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* 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.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
import com.mikepenz.iconics.view.IconicsImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
import ch.dissem.apps.abit.util.Drawables;
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
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.MessageRepository;
|
||||
|
||||
import static android.text.util.Linkify.WEB_URLS;
|
||||
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN;
|
||||
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA;
|
||||
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a {@link MainActivity}
|
||||
* in two-pane mode (on tablets) or a {@link MessageDetailActivity}
|
||||
* on handsets.
|
||||
*/
|
||||
public class MessageDetailFragment extends Fragment {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM = "item";
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private Plaintext item;
|
||||
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public MessageDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments().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 = (Plaintext) getArguments().getSerializable(ARG_ITEM);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_message_detail, container, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
if (item != null) {
|
||||
((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject());
|
||||
ImageView status = (ImageView) rootView.findViewById(R.id.status);
|
||||
status.setImageResource(Assets.getStatusDrawable(item.getStatus()));
|
||||
status.setContentDescription(getString(Assets.getStatusString(item.getStatus())));
|
||||
BitmessageAddress sender = item.getFrom();
|
||||
((ImageView) rootView.findViewById(R.id.avatar))
|
||||
.setImageDrawable(new Identicon(sender));
|
||||
((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString());
|
||||
if (item.getTo() != null) {
|
||||
((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString());
|
||||
} else if (item.getType() == Plaintext.Type.BROADCAST) {
|
||||
((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast);
|
||||
}
|
||||
RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels);
|
||||
LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels());
|
||||
labelView.setAdapter(labelAdapter);
|
||||
labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
|
||||
|
||||
TextView messageBody = (TextView) rootView.findViewById(R.id.text);
|
||||
messageBody.setText(item.getText());
|
||||
|
||||
Linkify.addLinks(messageBody, WEB_URLS);
|
||||
Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
|
||||
new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(Matcher match, String url) {
|
||||
return match.group();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
messageBody.setLinksClickable(true);
|
||||
messageBody.setTextIsSelectable(true);
|
||||
|
||||
boolean removed = false;
|
||||
Iterator<Label> labels = item.getLabels().iterator();
|
||||
while (labels.hasNext()) {
|
||||
if (labels.next().getType() == Label.Type.UNREAD) {
|
||||
labels.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext());
|
||||
if (removed) {
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
messageRepo.save(item);
|
||||
}
|
||||
List<Plaintext> parents = new ArrayList<>(item.getParents().size());
|
||||
for (InventoryVector parentIV : item.getParents()) {
|
||||
Plaintext parent = messageRepo.getMessage(parentIV);
|
||||
if (parent != null) {
|
||||
parents.add(parent);
|
||||
}
|
||||
}
|
||||
showRelatedMessages(rootView, R.id.parents, parents);
|
||||
showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item));
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) {
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id);
|
||||
RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.message, menu);
|
||||
|
||||
Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon
|
||||
.gmd_markunread);
|
||||
Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
MessageRepository messageRepo = Singleton.getMessageRepository(getContext());
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.reply:
|
||||
ComposeMessageActivity.launchReplyTo(this, item);
|
||||
return true;
|
||||
case R.id.delete:
|
||||
if (isInTrash(item)) {
|
||||
messageRepo.remove(item);
|
||||
} else {
|
||||
item.getLabels().clear();
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
|
||||
messageRepo.save(item);
|
||||
}
|
||||
getActivity().onBackPressed();
|
||||
return true;
|
||||
case R.id.mark_unread:
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.UNREAD));
|
||||
messageRepo.save(item);
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
return true;
|
||||
case R.id.archive:
|
||||
if (item.isUnread() && getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateUnread();
|
||||
}
|
||||
item.getLabels().clear();
|
||||
messageRepo.save(item);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInTrash(Plaintext item) {
|
||||
for (Label label : item.getLabels()) {
|
||||
if (label.getType() == Label.Type.TRASH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> {
|
||||
private final List<Plaintext> messages;
|
||||
private final Context ctx;
|
||||
|
||||
private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) {
|
||||
this.messages = messages;
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
// Inflate the custom layout
|
||||
View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false);
|
||||
|
||||
// Return a new holder instance
|
||||
return new RelatedMessageAdapter.ViewHolder(contactView);
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
@Override
|
||||
public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) {
|
||||
// Get the data model based on position
|
||||
Plaintext message = messages.get(position);
|
||||
|
||||
viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom()));
|
||||
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus()));
|
||||
viewHolder.sender.setText(message.getFrom().toString());
|
||||
viewHolder.extract.setText(normalizeWhitespaces(message.getText()));
|
||||
viewHolder.item = message;
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ImageView avatar;
|
||||
private final ImageView status;
|
||||
private final TextView sender;
|
||||
private final TextView extract;
|
||||
private Plaintext item;
|
||||
|
||||
ViewHolder(final View itemView) {
|
||||
super(itemView);
|
||||
avatar = (ImageView) itemView.findViewById(R.id.avatar);
|
||||
status = (ImageView) itemView.findViewById(R.id.status);
|
||||
sender = (TextView) itemView.findViewById(R.id.sender);
|
||||
extract = (TextView) itemView.findViewById(R.id.text);
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (ctx instanceof MainActivity) {
|
||||
((MainActivity) ctx).onItemSelected(item);
|
||||
} else {
|
||||
Intent detailIntent;
|
||||
detailIntent = new Intent(ctx, MessageDetailActivity.class);
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
|
||||
ctx.startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelAdapter extends
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder> {
|
||||
|
||||
private final List<Label> labels;
|
||||
private final Context ctx;
|
||||
|
||||
private LabelAdapter(Context ctx, Set<Label> labels) {
|
||||
this.labels = new ArrayList<>(labels);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
// Inflate the custom layout
|
||||
View contactView = inflater.inflate(R.layout.item_label, parent, false);
|
||||
|
||||
// Return a new holder instance
|
||||
return new ViewHolder(contactView);
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
@Override
|
||||
public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) {
|
||||
// Get the data model based on position
|
||||
Label label = labels.get(position);
|
||||
|
||||
viewHolder.icon.setColor(Labels.getColor(label));
|
||||
viewHolder.icon.setIcon(Labels.getIcon(label));
|
||||
viewHolder.label.setText(Labels.getText(label, ctx));
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return labels.size();
|
||||
}
|
||||
|
||||
// Provide a direct reference to each of the views within a data item
|
||||
// Used to cache the views within the item layout for fast access
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
// Your holder should contain a member variable
|
||||
// for any view that will be set as you render a row
|
||||
public IconicsImageView icon;
|
||||
public TextView label;
|
||||
|
||||
// We also create a constructor that accepts the entire item row
|
||||
// and does the view lookups to find each subview
|
||||
ViewHolder(View itemView) {
|
||||
// Stores the itemView in a public final member variable that can be used
|
||||
// to access the context from any ViewHolder instance.
|
||||
super(itemView);
|
||||
|
||||
icon = (IconicsImageView) itemView.findViewById(R.id.icon);
|
||||
label = (TextView) itemView.findViewById(R.id.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* 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.AsyncTask;
|
||||
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.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
|
||||
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 java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter;
|
||||
import ch.dissem.apps.abit.listener.ActionBarListener;
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||
import io.github.yavski.fabspeeddial.FabSpeedDial;
|
||||
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
|
||||
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY;
|
||||
import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash;
|
||||
|
||||
/**
|
||||
* 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 {@link MessageDetailFragment}.
|
||||
* <p/>
|
||||
* Activities containing this fragment MUST implement the {@link ListSelectionListener}
|
||||
* interface.
|
||||
*/
|
||||
public class MessageListFragment extends Fragment implements ListHolder {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerView.LayoutManager layoutManager;
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private RecyclerView.Adapter wrappedAdapter;
|
||||
private RecyclerViewSwipeManager recyclerViewSwipeManager;
|
||||
private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager;
|
||||
|
||||
private Label currentLabel;
|
||||
private MenuItem emptyTrashMenuItem;
|
||||
private MessageRepository messageRepo;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public MessageListFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
messageRepo = Singleton.getMessageRepository(activity);
|
||||
|
||||
doUpdateList(activity.getSelectedLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateList(Label label) {
|
||||
if (!isResumed()) {
|
||||
currentLabel = label;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Objects.equals(currentLabel, label)) {
|
||||
adapter.setData(label, Collections.<Plaintext>emptyList());
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
doUpdateList(label);
|
||||
}
|
||||
|
||||
private void doUpdateList(final Label label) {
|
||||
if (label == null) {
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name));
|
||||
}
|
||||
adapter.setData(null, Collections.<Plaintext>emptyList());
|
||||
adapter.notifyDataSetChanged();
|
||||
return;
|
||||
}
|
||||
currentLabel = label;
|
||||
if (emptyTrashMenuItem != null) {
|
||||
emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH);
|
||||
}
|
||||
if (getActivity() instanceof ActionBarListener) {
|
||||
ActionBarListener actionBarListener = (ActionBarListener) getActivity();
|
||||
if ("archive".equals(label.toString())) {
|
||||
actionBarListener.updateTitle(getString(R.string.archive));
|
||||
} else {
|
||||
actionBarListener.updateTitle(label.toString());
|
||||
}
|
||||
}
|
||||
new AsyncTask<Void, Void, List<Plaintext>>() {
|
||||
|
||||
@Override
|
||||
protected List<Plaintext> doInBackground(Void... params) {
|
||||
return messageRepo.findMessages(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Plaintext> messages) {
|
||||
if (adapter != null) {
|
||||
adapter.setData(label, messages);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_message_list, container, false);
|
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id
|
||||
.fab_compose_message);
|
||||
fab.setMenuListener(new SimpleMenuListenerAdapter() {
|
||||
@Override
|
||||
public boolean onMenuItemSelected(MenuItem menuItem) {
|
||||
BitmessageAddress identity = Singleton.getIdentity(getActivity());
|
||||
if (identity == null) {
|
||||
Toast.makeText(getActivity(), R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
} else {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_compose_message: {
|
||||
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
|
||||
intent.putExtra(EXTRA_IDENTITY, identity);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_compose_broadcast: {
|
||||
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
|
||||
intent.putExtra(EXTRA_IDENTITY, identity);
|
||||
intent.putExtra(EXTRA_BROADCAST, true);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
|
||||
// animation is running)
|
||||
recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager();
|
||||
recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning
|
||||
(true);
|
||||
recyclerViewTouchActionGuardManager.setEnabled(true);
|
||||
|
||||
// swipe manager
|
||||
recyclerViewSwipeManager = new RecyclerViewSwipeManager();
|
||||
|
||||
//adapter
|
||||
adapter = new SwipeableMessageAdapter();
|
||||
adapter.setActivateOnItemClick(activateOnItemClick);
|
||||
adapter.setEventListener(new SwipeableMessageAdapter.EventListener() {
|
||||
@Override
|
||||
public void onItemDeleted(Plaintext item) {
|
||||
if (isInTrash(item)) {
|
||||
messageRepo.remove(item);
|
||||
} else {
|
||||
item.getLabels().clear();
|
||||
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
|
||||
messageRepo.save(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemArchived(Plaintext item) {
|
||||
item.getLabels().clear();
|
||||
messageRepo.save(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemViewClicked(View v) {
|
||||
int position = recyclerView.getChildAdapterPosition(v);
|
||||
adapter.setSelectedPosition(position);
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
Plaintext item = adapter.getItem(position);
|
||||
((MainActivity) getActivity()).onItemSelected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// wrap for swiping
|
||||
wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter);
|
||||
|
||||
final GeneralItemAnimator animator = new 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.setSupportsChangeAnimations(false);
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(wrappedAdapter); // requires *wrapped* adapter
|
||||
recyclerView.setItemAnimator(animator);
|
||||
|
||||
recyclerView.addItemDecoration(new SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(getContext(), 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
|
||||
recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView);
|
||||
recyclerViewSwipeManager.attachRecyclerView(recyclerView);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (recyclerViewSwipeManager != null) {
|
||||
recyclerViewSwipeManager.release();
|
||||
recyclerViewSwipeManager = null;
|
||||
}
|
||||
|
||||
if (recyclerViewTouchActionGuardManager != null) {
|
||||
recyclerViewTouchActionGuardManager.release();
|
||||
recyclerViewTouchActionGuardManager = null;
|
||||
}
|
||||
|
||||
if (recyclerView != null) {
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setAdapter(null);
|
||||
recyclerView = null;
|
||||
}
|
||||
|
||||
if (wrappedAdapter != null) {
|
||||
WrapperAdapterUtils.releaseAll(wrappedAdapter);
|
||||
wrappedAdapter = null;
|
||||
}
|
||||
adapter = null;
|
||||
layoutManager = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.message_list, menu);
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.empty_trash:
|
||||
if (currentLabel.getType() != Label.Type.TRASH) return true;
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
for (Plaintext message : messageRepo.findMessages(currentLabel)) {
|
||||
messageRepo.remove(message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
updateList(currentLabel);
|
||||
}
|
||||
}.execute();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
if (adapter != null) {
|
||||
adapter.setActivateOnItemClick(activateOnItemClick);
|
||||
}
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ch.dissem.apps.abit;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SettingsActivity extends DetailActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Display the fragment as the main content.
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mikepenz.aboutlibraries.Libs;
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder;
|
||||
|
||||
import ch.dissem.apps.abit.repository.AndroidNodeRegistry;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW;
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SettingsFragment
|
||||
extends PreferenceFragment
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
Preference about = findPreference("about");
|
||||
about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new LibsBuilder()
|
||||
.withActivityTitle(getActivity().getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutDescription(getString(R.string.about_app))
|
||||
.start(getActivity());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
final Preference cleanup = findPreference("cleanup");
|
||||
cleanup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
private Context ctx = getActivity().getApplicationContext();
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
cleanup.setEnabled(false);
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast
|
||||
.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
|
||||
bmc.cleanup();
|
||||
bmc.internals().getNodeRegistry().clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
R.string.cleanup_notification_end,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
cleanup.setEnabled(true);
|
||||
}
|
||||
}.execute();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Preference status = findPreference("status");
|
||||
status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivity(new Intent(getActivity(), StatusActivity.class));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context ctx) {
|
||||
super.onAttach(ctx);
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
switch (key) {
|
||||
case PREFERENCE_TRUSTED_NODE:
|
||||
String node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null);
|
||||
if (node != null) {
|
||||
SyncAdapter.startSync(getActivity());
|
||||
} else {
|
||||
SyncAdapter.stopSync(getActivity());
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_SERVER_POW:
|
||||
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
|
||||
SyncAdapter.startPowSync(getActivity());
|
||||
} else {
|
||||
SyncAdapter.stopPowSync(getActivity());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget.Toolbar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.materialize.MaterializeBuilder;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
public class StatusActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_status);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(false);
|
||||
|
||||
new MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build();
|
||||
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(this);
|
||||
StringBuilder status = new StringBuilder();
|
||||
for (BitmessageAddress address : bmc.addresses().getIdentities()) {
|
||||
status.append(address.getAddress()).append('\n');
|
||||
}
|
||||
status.append('\n');
|
||||
status.append(bmc.status());
|
||||
((TextView) findViewById(R.id.content)).setText(status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.CompoundButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AddressSelectorAdapter
|
||||
extends RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder> {
|
||||
|
||||
private final List<Selectable<BitmessageAddress>> data;
|
||||
|
||||
public AddressSelectorAdapter(List<BitmessageAddress> identities) {
|
||||
data = new ArrayList<>(identities.size());
|
||||
for (BitmessageAddress identity : identities) {
|
||||
data.add(new Selectable<>(identity));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View v = inflater.inflate(R.layout.select_identity_row, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
Selectable<BitmessageAddress> selectable = data.get(position);
|
||||
holder.data = selectable;
|
||||
holder.checkbox.setChecked(selectable.selected);
|
||||
holder.checkbox.setText(selectable.data.toString());
|
||||
holder.address.setText(selectable.data.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public Selectable<BitmessageAddress> data;
|
||||
public final CheckBox checkbox;
|
||||
public final TextView address;
|
||||
|
||||
private ViewHolder(View v) {
|
||||
super(v);
|
||||
checkbox = (CheckBox) v.findViewById(R.id.checkbox);
|
||||
address = (TextView) v.findViewById(R.id.address);
|
||||
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
|
||||
if (data != null) {
|
||||
data.selected = isChecked;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public List<BitmessageAddress> getSelected() {
|
||||
List<BitmessageAddress> result = new LinkedList<>();
|
||||
for (Selectable<BitmessageAddress> selectable : data) {
|
||||
if (selectable.selected) {
|
||||
result.add(selectable.data);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public class AndroidCryptography extends SpongyCryptography {
|
||||
public AndroidCryptography() {
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.BaseAdapter;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
|
||||
/**
|
||||
* An adapter for contacts. Can be filtered by alias or address.
|
||||
*/
|
||||
public class ContactAdapter extends BaseAdapter implements Filterable {
|
||||
private final LayoutInflater inflater;
|
||||
private final List<BitmessageAddress> originalData;
|
||||
private List<BitmessageAddress> data;
|
||||
|
||||
public ContactAdapter(Context ctx) {
|
||||
inflater = LayoutInflater.from(ctx);
|
||||
originalData = Singleton.getAddressRepository(ctx).getContacts();
|
||||
data = originalData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.contact_row, parent, false);
|
||||
}
|
||||
BitmessageAddress item = getItem(position);
|
||||
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item));
|
||||
((TextView) convertView.findViewById(R.id.name)).setText(item.toString());
|
||||
((TextView) convertView.findViewById(R.id.address)).setText(item.getAddress());
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return new ContactFilter();
|
||||
}
|
||||
|
||||
private class ContactFilter extends Filter {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence prefix) {
|
||||
FilterResults results = new FilterResults();
|
||||
|
||||
if (prefix == null || prefix.length() == 0) {
|
||||
results.values = originalData;
|
||||
results.count = originalData.size();
|
||||
} else {
|
||||
String prefixString = prefix.toString().toLowerCase();
|
||||
|
||||
final ArrayList<BitmessageAddress> newValues = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < originalData.size(); i++) {
|
||||
final BitmessageAddress value = originalData.get(i);
|
||||
|
||||
// First match against the whole, non-splitted value
|
||||
if (value.getAlias() != null) {
|
||||
String alias = value.getAlias().toLowerCase();
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value);
|
||||
} else {
|
||||
final String[] words = alias.split(" ");
|
||||
|
||||
for (String word : words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
newValues.add(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String address = value.getAddress().toLowerCase();
|
||||
if (address.contains(prefixString)) {
|
||||
newValues.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues;
|
||||
results.count = newValues.size();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
//noinspection unchecked
|
||||
data = (List<BitmessageAddress>) results.values;
|
||||
if (results.count > 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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<T> {
|
||||
final T data;
|
||||
boolean selected = false;
|
||||
|
||||
Selectable(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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 com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE;
|
||||
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
|
||||
|
||||
/**
|
||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||
*
|
||||
* @author Christian Basler
|
||||
* @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview">
|
||||
* https://github.com/h6ah4i/android-advancedrecyclerview</a>
|
||||
*/
|
||||
public class SwipeableMessageAdapter
|
||||
extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>
|
||||
implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
|
||||
|
||||
private List<Plaintext> data = Collections.emptyList();
|
||||
private EventListener eventListener;
|
||||
private final View.OnClickListener itemViewOnClickListener;
|
||||
private final View.OnClickListener swipeableViewContainerOnClickListener;
|
||||
|
||||
private Label label;
|
||||
private int selectedPosition;
|
||||
private boolean activateOnItemClick;
|
||||
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
this.activateOnItemClick = activateOnItemClick;
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onItemDeleted(Plaintext item);
|
||||
|
||||
void onItemArchived(Plaintext item);
|
||||
|
||||
void onItemViewClicked(View v);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static class ViewHolder extends AbstractSwipeableItemViewHolder {
|
||||
public final FrameLayout container;
|
||||
public final ImageView avatar;
|
||||
public final ImageView status;
|
||||
public final TextView sender;
|
||||
public final TextView subject;
|
||||
public final TextView extract;
|
||||
|
||||
ViewHolder(View v) {
|
||||
super(v);
|
||||
container = (FrameLayout) v.findViewById(R.id.container);
|
||||
avatar = (ImageView) v.findViewById(R.id.avatar);
|
||||
status = (ImageView) v.findViewById(R.id.status);
|
||||
sender = (TextView) v.findViewById(R.id.sender);
|
||||
subject = (TextView) v.findViewById(R.id.subject);
|
||||
extract = (TextView) v.findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getSwipeableContainerView() {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
public SwipeableMessageAdapter() {
|
||||
itemViewOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onItemViewClick(view);
|
||||
}
|
||||
};
|
||||
swipeableViewContainerOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onSwipeableViewContainerClick(view);
|
||||
}
|
||||
};
|
||||
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setData(Label label, List<Plaintext> data) {
|
||||
this.label = label;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private void onItemViewClick(View v) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onItemViewClicked(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSwipeableViewContainerClick(View v) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onItemViewClicked(
|
||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v));
|
||||
}
|
||||
}
|
||||
|
||||
public Plaintext getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return (long) data.get(position).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View v = inflater.inflate(R.layout.message_row, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
final Plaintext item = data.get(position);
|
||||
|
||||
if (activateOnItemClick) {
|
||||
holder.container.setBackgroundResource(
|
||||
position == selectedPosition
|
||||
? R.drawable.bg_item_selected_state
|
||||
: R.drawable.bg_item_normal_state
|
||||
);
|
||||
}
|
||||
|
||||
// set listeners
|
||||
// (if the item is *pinned*, click event comes to the itemView)
|
||||
holder.itemView.setOnClickListener(itemViewOnClickListener);
|
||||
// (if the item is *not pinned*, click event comes to the container)
|
||||
holder.container.setOnClickListener(swipeableViewContainerOnClickListener);
|
||||
|
||||
// set data
|
||||
holder.avatar.setImageDrawable(new Identicon(item.getFrom()));
|
||||
holder.status.setImageResource(Assets.getStatusDrawable(item.getStatus()));
|
||||
holder.status.setContentDescription(
|
||||
holder.status.getContext().getString(Assets.getStatusString(item.getStatus())));
|
||||
holder.sender.setText(item.getFrom().toString());
|
||||
holder.subject.setText(normalizeWhitespaces(item.getSubject()));
|
||||
holder.extract.setText(normalizeWhitespaces(item.getText()));
|
||||
if (item.isUnread()) {
|
||||
holder.sender.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
holder.subject.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
} else {
|
||||
holder.sender.setTypeface(Typeface.DEFAULT);
|
||||
holder.subject.setTypeface(Typeface.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onGetSwipeReactionType(ViewHolder holder, int position, int x, int y) {
|
||||
if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) {
|
||||
return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT;
|
||||
}
|
||||
return REACTION_CAN_SWIPE_BOTH_H;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SwitchIntDef")
|
||||
public void onSetSwipeBackground(ViewHolder holder, int position, int type) {
|
||||
int bgRes = 0;
|
||||
switch (type) {
|
||||
case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND:
|
||||
bgRes = R.drawable.bg_swipe_item_neutral;
|
||||
break;
|
||||
case DRAWABLE_SWIPE_LEFT_BACKGROUND:
|
||||
bgRes = R.drawable.bg_swipe_item_left;
|
||||
break;
|
||||
case DRAWABLE_SWIPE_RIGHT_BACKGROUND:
|
||||
if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) {
|
||||
bgRes = R.drawable.bg_swipe_item_neutral;
|
||||
} else {
|
||||
bgRes = R.drawable.bg_swipe_item_right;
|
||||
}
|
||||
break;
|
||||
}
|
||||
holder.itemView.setBackgroundResource(bgRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SwitchIntDef")
|
||||
public SwipeResultAction onSwipeItem(ViewHolder holder, final int position, int result) {
|
||||
switch (result) {
|
||||
// swipe right
|
||||
case RESULT_SWIPED_RIGHT:
|
||||
return new SwipeRightResultAction(this, position);
|
||||
case RESULT_SWIPED_LEFT:
|
||||
return new SwipeLeftResultAction(this, position);
|
||||
// other --- do nothing
|
||||
case RESULT_CANCELED:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEventListener(EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setSelectedPosition(int selectedPosition) {
|
||||
int oldPosition = this.selectedPosition;
|
||||
this.selectedPosition = selectedPosition;
|
||||
notifyItemChanged(oldPosition);
|
||||
notifyItemChanged(selectedPosition);
|
||||
}
|
||||
|
||||
private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection {
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private final int position;
|
||||
private final Plaintext item;
|
||||
|
||||
SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) {
|
||||
this.adapter = adapter;
|
||||
this.position = position;
|
||||
this.item = adapter.data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPerformAction() {
|
||||
super.onPerformAction();
|
||||
|
||||
adapter.data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSlideAnimationEnd() {
|
||||
super.onSlideAnimationEnd();
|
||||
|
||||
if (adapter.eventListener != null) {
|
||||
adapter.eventListener.onItemDeleted(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleanUp() {
|
||||
super.onCleanUp();
|
||||
// clear the references
|
||||
adapter = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SwipeRightResultAction extends SwipeResultActionRemoveItem {
|
||||
private SwipeableMessageAdapter adapter;
|
||||
private final int position;
|
||||
private final Plaintext item;
|
||||
|
||||
SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) {
|
||||
this.adapter = adapter;
|
||||
this.position = position;
|
||||
this.item = adapter.data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPerformAction() {
|
||||
super.onPerformAction();
|
||||
|
||||
adapter.data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSlideAnimationEnd() {
|
||||
super.onSlideAnimationEnd();
|
||||
|
||||
if (adapter.eventListener != null) {
|
||||
adapter.eventListener.onItemArchived(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleanUp() {
|
||||
super.onCleanUp();
|
||||
// clear the references
|
||||
adapter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
/**
|
||||
* Switches between two {@link ProofOfWorkEngine}s depending on the configuration.
|
||||
*
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class SwitchingProofOfWorkEngine implements ProofOfWorkEngine, InternalContext.ContextHolder {
|
||||
private final Context ctx;
|
||||
private final String preference;
|
||||
private final ProofOfWorkEngine option;
|
||||
private final ProofOfWorkEngine fallback;
|
||||
|
||||
public SwitchingProofOfWorkEngine(Context ctx, String preference,
|
||||
ProofOfWorkEngine option, ProofOfWorkEngine fallback) {
|
||||
this.ctx = ctx;
|
||||
this.preference = preference;
|
||||
this.option = option;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
if (preferences.getBoolean(preference, false)) {
|
||||
option.calculateNonce(initialHash, target, callback);
|
||||
} else {
|
||||
fallback.calculateNonce(initialHash, target, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext context) {
|
||||
for (ProofOfWorkEngine e : Arrays.asList(option, fallback)) {
|
||||
if (e instanceof InternalContext.ContextHolder) {
|
||||
((InternalContext.ContextHolder) e).setContext(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
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.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class AddIdentityDialogFragment extends AppCompatDialogFragment {
|
||||
private BitmessageContext bmc;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
bmc = Singleton.getBitmessageContext(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
getDialog().setTitle(R.string.add_identity);
|
||||
View view = inflater.inflate(R.layout.dialog_add_identity, container, false);
|
||||
final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup);
|
||||
view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final Context ctx = getActivity().getBaseContext();
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.create_identity:
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<Void, Void, BitmessageAddress>() {
|
||||
@Override
|
||||
protected BitmessageAddress doInBackground(Void... args) {
|
||||
return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(BitmessageAddress chan) {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
mainActivity.addIdentityEntry(chan);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
break;
|
||||
case R.id.import_identity:
|
||||
startActivity(new Intent(ctx, ImportIdentityActivity.class));
|
||||
break;
|
||||
case R.id.add_chan:
|
||||
addChanDialog();
|
||||
break;
|
||||
case R.id.add_deterministic_address:
|
||||
new DeterministicIdentityDialogFragment().show(getFragmentManager(),
|
||||
"dialog");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
private void addChanDialog() {
|
||||
FragmentActivity activity = getActivity();
|
||||
final Context ctx = activity.getBaseContext();
|
||||
@SuppressLint("InflateParams")
|
||||
final View dialogView = activity.getLayoutInflater()
|
||||
.inflate(R.layout.dialog_input_passphrase, null);
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.add_chan)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase);
|
||||
Toast.makeText(ctx, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<String, Void, BitmessageAddress>() {
|
||||
@Override
|
||||
protected BitmessageAddress doInBackground(String... args) {
|
||||
String pass = args[0];
|
||||
BitmessageAddress chan = bmc.createChan(pass);
|
||||
chan.setAlias(pass);
|
||||
bmc.addresses().save(chan);
|
||||
return chan;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(BitmessageAddress chan) {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_chan_created,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
mainActivity.addIdentityEntry(chan);
|
||||
}
|
||||
}
|
||||
}.execute(passphrase.getText().toString());
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTheme() {
|
||||
return R.style.FixedDialog;
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment {
|
||||
private BitmessageContext bmc;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
bmc = Singleton.getBitmessageContext(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
|
||||
savedInstanceState) {
|
||||
getDialog().setTitle(R.string.add_deterministic_address);
|
||||
View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false);
|
||||
view.findViewById(R.id.ok)
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
final Context context = getActivity().getBaseContext();
|
||||
View dialogView = getView();
|
||||
assert dialogView != null;
|
||||
TextView label = (TextView) dialogView.findViewById(R.id.label);
|
||||
TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase);
|
||||
TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id
|
||||
.number_of_identities);
|
||||
Switch shorter = (Switch) dialogView.findViewById(R.id.shorter);
|
||||
|
||||
Toast.makeText(context, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
new AsyncTask<Object, Void, List<BitmessageAddress>>() {
|
||||
@Override
|
||||
protected List<BitmessageAddress> doInBackground(Object... args) {
|
||||
String label = (String) args[0];
|
||||
String pass = (String) args[1];
|
||||
int numberOfAddresses = (int) args[2];
|
||||
boolean shorter = (boolean) args[3];
|
||||
List<BitmessageAddress> identities = bmc.createDeterministicAddresses
|
||||
(pass,
|
||||
numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter);
|
||||
int i = 0;
|
||||
for (BitmessageAddress identity : identities) {
|
||||
i++;
|
||||
if (identities.size() == 1) {
|
||||
identity.setAlias(label);
|
||||
} else {
|
||||
identity.setAlias(label + " (" + i + ")");
|
||||
}
|
||||
bmc.addresses().save(identity);
|
||||
}
|
||||
return identities;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<BitmessageAddress> identities) {
|
||||
int messageRes;
|
||||
if (identities.size() == 1) {
|
||||
messageRes = R.string.toast_identity_created;
|
||||
} else {
|
||||
messageRes = R.string.toast_identities_created;
|
||||
}
|
||||
Toast.makeText(context,
|
||||
messageRes,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
for (BitmessageAddress identity : identities) {
|
||||
mainActivity.addIdentityEntry(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.execute(
|
||||
label.getText().toString(),
|
||||
passphrase.getText().toString(),
|
||||
Integer.valueOf(numberOfAddresses.getText().toString()),
|
||||
shorter.isChecked()
|
||||
);
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTheme() {
|
||||
return R.style.FixedDialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.service.BitmessageService;
|
||||
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class FullNodeDialogActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.dialog_full_node);
|
||||
findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startService(new Intent(FullNodeDialogActivity.this, BitmessageService.class));
|
||||
updateNodeSwitch();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
|
||||
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class SelectEncodingDialogFragment extends AppCompatDialogFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment.class);
|
||||
private Plaintext.Encoding encoding;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) {
|
||||
encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING);
|
||||
}
|
||||
if (encoding == null) {
|
||||
encoding = SIMPLE;
|
||||
}
|
||||
getDialog().setTitle(R.string.select_encoding_title);
|
||||
View view = inflater.inflate(R.layout.dialog_select_message_encoding, container, false);
|
||||
final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup);
|
||||
switch (encoding) {
|
||||
case SIMPLE:
|
||||
radioGroup.check(R.id.simple);
|
||||
break;
|
||||
case EXTENDED:
|
||||
radioGroup.check(R.id.extended);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected encoding: " + encoding);
|
||||
break;
|
||||
}
|
||||
view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.extended:
|
||||
encoding = EXTENDED;
|
||||
break;
|
||||
case R.id.simple:
|
||||
encoding = SIMPLE;
|
||||
break;
|
||||
default:
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
Intent result = new Intent();
|
||||
result.putExtra(EXTRA_ENCODING, encoding);
|
||||
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, result);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public interface ActionBarListener {
|
||||
void updateTitle(CharSequence title);
|
||||
|
||||
void updateUnread();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
public interface ListSelectionListener<T> {
|
||||
/**
|
||||
* Callback for when an item has been selected.
|
||||
*/
|
||||
void onItemSelected(T item);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.notification.NewMessageNotification;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
/**
|
||||
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||
* <p>
|
||||
* Should show a notification when the app isn't running, but update the message list when it is.
|
||||
* Also,
|
||||
* notifications should be combined.
|
||||
* </p>
|
||||
*/
|
||||
public class MessageListener implements BitmessageContext.Listener {
|
||||
private final Deque<Plaintext> unacknowledged = new LinkedList<>();
|
||||
private int numberOfUnacknowledgedMessages = 0;
|
||||
private final NewMessageNotification notification;
|
||||
private final ExecutorService pool = Executors.newSingleThreadExecutor();
|
||||
|
||||
public MessageListener(Context ctx) {
|
||||
this.notification = new NewMessageNotification(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final Plaintext plaintext) {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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 main = MainActivity.getInstance();
|
||||
if (main != null) {
|
||||
main.updateUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void resetNotification() {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notification.hide();
|
||||
unacknowledged.clear();
|
||||
numberOfUnacknowledgedMessages = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
|
||||
public class WifiReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
if ("android.net.conn.CONNECTIVITY_CHANGE".equals(intent.getAction())) {
|
||||
if (Preferences.isWifiOnly(ctx)) {
|
||||
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
|
||||
|
||||
if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) {
|
||||
bmc.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isConnectedToMeteredNetwork(Context ctx) {
|
||||
NetworkInfo netInfo = getNetworkInfo(ctx);
|
||||
if (netInfo == null || !netInfo.isConnectedOrConnecting()) {
|
||||
return false;
|
||||
}
|
||||
switch (netInfo.getType()) {
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static NetworkInfo getNetworkInfo(Context ctx) {
|
||||
ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context
|
||||
.CONNECTIVITY_SERVICE);
|
||||
return conMan.getActiveNetworkInfo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Some base class to create and handle notifications.
|
||||
*/
|
||||
public abstract class AbstractNotification {
|
||||
protected final Context ctx;
|
||||
protected final NotificationManager manager;
|
||||
protected Notification notification;
|
||||
|
||||
|
||||
public AbstractNotification(Context ctx) {
|
||||
this.ctx = ctx.getApplicationContext();
|
||||
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an id unique to this notification class
|
||||
*/
|
||||
protected abstract int getNotificationId();
|
||||
|
||||
public Notification getNotification() {
|
||||
return notification;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
manager.notify(getNotificationId(), notification);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
manager.cancel(getNotificationId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.v7.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
|
||||
*/
|
||||
public class ErrorNotification extends AbstractNotification {
|
||||
public static final int ERROR_NOTIFICATION_ID = 4;
|
||||
|
||||
private final NotificationCompat.Builder builder;
|
||||
|
||||
public ErrorNotification(Context ctx) {
|
||||
super(ctx);
|
||||
builder = new NotificationCompat.Builder(ctx);
|
||||
builder.setContentTitle(ctx.getString(R.string.app_name))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
public ErrorNotification setWarning(@StringRes int resId, Object... args) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
||||
.setContentText(ctx.getString(resId, args));
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ErrorNotification setError(@StringRes int resId, Object... args) {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
||||
.setContentText(ctx.getString(resId, args));
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return ERROR_NOTIFICATION_ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
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 ch.dissem.bitmessage.utils.Property;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* Shows the network status (as long as the client is connected as a full node)
|
||||
*/
|
||||
public class NetworkNotification extends AbstractNotification {
|
||||
public static final int NETWORK_NOTIFICATION_ID = 2;
|
||||
|
||||
private final NotificationCompat.Builder builder;
|
||||
private Timer timer;
|
||||
|
||||
public NetworkNotification(Context ctx) {
|
||||
super(ctx);
|
||||
Intent showAppIntent = new Intent(ctx, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0);
|
||||
builder = new NotificationCompat.Builder(ctx);
|
||||
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")
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean update() {
|
||||
boolean running = BitmessageService.isRunning();
|
||||
builder.setOngoing(running);
|
||||
Property connections = BitmessageService.getStatus().getProperty("network", "connections");
|
||||
if (!running) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected));
|
||||
updateNodeSwitch();
|
||||
} else if (connections.getProperties().length == 0) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending));
|
||||
} else {
|
||||
StringBuilder info = new StringBuilder();
|
||||
for (Property stream : connections.getProperties()) {
|
||||
int streamNumber = Integer.parseInt(stream.getName().substring("stream ".length()));
|
||||
Integer nodeCount = (Integer) stream.getProperty("nodes").getValue();
|
||||
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();
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
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
|
||||
public void show() {
|
||||
super.show();
|
||||
|
||||
timer = new Timer();
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!update()) {
|
||||
cancel();
|
||||
ctx.stopService(new Intent(ctx, BitmessageService.class));
|
||||
}
|
||||
NetworkNotification.super.show();
|
||||
}
|
||||
}, 10_000, 10_000);
|
||||
}
|
||||
|
||||
public void showShutdown() {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
}
|
||||
update();
|
||||
super.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return NETWORK_NOTIFICATION_ID;
|
||||
}
|
||||
|
||||
public void connecting() {
|
||||
builder.setOngoing(true);
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending));
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.v7.app.NotificationCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
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 static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static ch.dissem.apps.abit.MainActivity.EXTRA_REPLY_TO_MESSAGE;
|
||||
import static ch.dissem.apps.abit.MainActivity.EXTRA_SHOW_MESSAGE;
|
||||
import static ch.dissem.apps.abit.service.BitmessageIntentService.EXTRA_DELETE_MESSAGE;
|
||||
import static ch.dissem.apps.abit.util.Drawables.toBitmap;
|
||||
|
||||
public class NewMessageNotification extends AbstractNotification {
|
||||
private static final int NEW_MESSAGE_NOTIFICATION_ID = 1;
|
||||
private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD);
|
||||
|
||||
public NewMessageNotification(Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public NewMessageNotification singleNotification(Plaintext plaintext) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText
|
||||
());
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192))
|
||||
.setContentTitle(plaintext.getFrom().toString())
|
||||
.setContentText(plaintext.getSubject())
|
||||
.setStyle(new NotificationCompat.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 PendingIntent createActivityIntent(String action, Plaintext message) {
|
||||
Intent intent = new Intent(ctx, MainActivity.class);
|
||||
intent.putExtra(action, message);
|
||||
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent createServiceIntent(Context ctx, String action, Plaintext message) {
|
||||
Intent intent = new Intent(ctx, BitmessageIntentService.class);
|
||||
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 <code>synchronized(unacknowledged)
|
||||
* {}</code> block
|
||||
*/
|
||||
public NewMessageNotification multiNotification(Collection<Plaintext> unacknowledged, int
|
||||
numberOfUnacknowledgedMessages) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
|
||||
.setContentText(ctx.getString(R.string.app_name));
|
||||
|
||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (unacknowledged) {
|
||||
for (Plaintext msg : unacknowledged) {
|
||||
Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject());
|
||||
sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
inboxStyle.addLine(sb);
|
||||
}
|
||||
}
|
||||
builder.setStyle(inboxStyle);
|
||||
|
||||
Intent intent = new Intent(ctx, MainActivity.class);
|
||||
intent.setAction(MainActivity.ACTION_SHOW_INBOX);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0);
|
||||
builder.setContentIntent(pendingIntent);
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return NEW_MESSAGE_NOTIFICATION_ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.v7.app.NotificationCompat;
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity;
|
||||
import ch.dissem.apps.abit.R;
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
public class ProofOfWorkNotification extends AbstractNotification {
|
||||
public static final int ONGOING_NOTIFICATION_ID = 3;
|
||||
|
||||
public ProofOfWorkNotification(Context ctx) {
|
||||
super(ctx);
|
||||
update(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return ONGOING_NOTIFICATION_ID;
|
||||
}
|
||||
|
||||
public ProofOfWorkNotification update(int numberOfItems) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||
|
||||
Intent showMessageIntent = new Intent(ctx, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
builder.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))
|
||||
.setContentText(numberOfItems == 0
|
||||
? ctx.getString(R.string.proof_of_work_text_0)
|
||||
: ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
|
||||
.setContentIntent(pendingIntent);
|
||||
|
||||
notification = builder.build();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
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.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE;
|
||||
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class ServerPowEngine implements ProofOfWorkEngine, InternalContext
|
||||
.ContextHolder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ServerPowEngine.class);
|
||||
|
||||
private final Context ctx;
|
||||
private InternalContext context;
|
||||
|
||||
private final ExecutorService pool;
|
||||
|
||||
public ServerPowEngine(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
pool = Executors.newCachedThreadPool(new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(@NonNull Runnable r) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(r);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) {
|
||||
pool.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BitmessageAddress identity = Singleton.getIdentity(ctx);
|
||||
if (identity == null) throw new RuntimeException("No Identity for calculating POW");
|
||||
|
||||
ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash,
|
||||
CALCULATE, target);
|
||||
SyncAdapter.startPowSync(ctx);
|
||||
try {
|
||||
CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>
|
||||
(request);
|
||||
cryptoMsg.signAndEncrypt(
|
||||
identity,
|
||||
cryptography().createPublicKey(identity.getPublicDecryptionKey())
|
||||
);
|
||||
context.getNetworkHandler().send(
|
||||
Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx),
|
||||
cryptoMsg);
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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.sqlite.SQLiteDatabase;
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
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.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AddressRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidAddressRepository implements AddressRepository {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "Address";
|
||||
private static final String COLUMN_ADDRESS = "address";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
private static final String COLUMN_ALIAS = "alias";
|
||||
private static final String COLUMN_PUBLIC_KEY = "public_key";
|
||||
private static final String COLUMN_PRIVATE_KEY = "private_key";
|
||||
private static final String COLUMN_SUBSCRIBED = "subscribed";
|
||||
private static final String COLUMN_CHAN = "chan";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
public AndroidAddressRepository(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findContact(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("public_key is null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findIdentity(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("private_key is not null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getIdentities() {
|
||||
return find("private_key IS NOT NULL");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getChans() {
|
||||
return find("chan = '1'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions() {
|
||||
return find("subscribed = '1'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions(long broadcastVersion) {
|
||||
if (broadcastVersion > 4) {
|
||||
return find("subscribed = '1' AND version > 3");
|
||||
} else {
|
||||
return find("subscribed = '1' AND version <= 3");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getContacts() {
|
||||
return find("private_key IS NULL OR chan = '1'");
|
||||
}
|
||||
|
||||
private List<BitmessageAddress> find(String where) {
|
||||
List<BitmessageAddress> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_ADDRESS,
|
||||
COLUMN_ALIAS,
|
||||
COLUMN_PUBLIC_KEY,
|
||||
COLUMN_PRIVATE_KEY,
|
||||
COLUMN_SUBSCRIBED,
|
||||
COLUMN_CHAN
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
BitmessageAddress address;
|
||||
|
||||
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
|
||||
if (privateKeyBytes != null) {
|
||||
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream
|
||||
(privateKeyBytes));
|
||||
address = new BitmessageAddress(privateKey);
|
||||
} else {
|
||||
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
|
||||
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
|
||||
if (publicKeyBytes != null) {
|
||||
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address
|
||||
.getStream(),
|
||||
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length,
|
||||
false);
|
||||
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
|
||||
pubkey = new V4Pubkey((V3Pubkey) pubkey);
|
||||
}
|
||||
address.setPubkey(pubkey);
|
||||
}
|
||||
}
|
||||
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS)));
|
||||
address.setChan(c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1);
|
||||
address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
|
||||
|
||||
result.add(address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(BitmessageAddress address) {
|
||||
if (exists(address)) {
|
||||
update(address);
|
||||
} else {
|
||||
insert(address);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exists(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor cursor = db.rawQuery(
|
||||
"SELECT COUNT(*) FROM Address WHERE address=?",
|
||||
new String[]{address.getAddress()}
|
||||
)) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void update(BitmessageAddress address) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
if (address.getAlias() != null) {
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
}
|
||||
if (address.getPubkey() != null) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
address.getPubkey().writeUnencrypted(out);
|
||||
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
|
||||
}
|
||||
if (address.getPrivateKey() != null) {
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
}
|
||||
if (address.isChan()) {
|
||||
values.put(COLUMN_CHAN, true);
|
||||
}
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
int update = db.update(TABLE_NAME, values, "address=?",
|
||||
new String[]{address.getAddress()});
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(BitmessageAddress address) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ADDRESS, address.getAddress());
|
||||
values.put(COLUMN_VERSION, address.getVersion());
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
if (address.getPubkey() != null) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
address.getPubkey().writeUnencrypted(out);
|
||||
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
|
||||
} else {
|
||||
values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
|
||||
}
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
values.put(COLUMN_CHAN, address.isChan());
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
long insert = db.insert(TABLE_NAME, null, values);
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()});
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress getAddress(String address) {
|
||||
List<BitmessageAddress> result = find("address = '" + address + "'");
|
||||
if (result.size() > 0) return result.get(0);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
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 static ch.dissem.apps.abit.repository.SqlHelper.join;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.now;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* {@link Inventory} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidInventory implements Inventory {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
|
||||
|
||||
private static final String TABLE_NAME = "Inventory";
|
||||
private static final String COLUMN_HASH = "hash";
|
||||
private static final String COLUMN_STREAM = "stream";
|
||||
private static final String COLUMN_EXPIRES = "expires";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public AndroidInventory(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getInventory(long... streams) {
|
||||
List<InventoryVector> result = new LinkedList<>();
|
||||
long now = now();
|
||||
for (long stream : streams) {
|
||||
for (Map.Entry<InventoryVector, Long> e : getCache(stream).entrySet()) {
|
||||
if (e.getValue() > now) {
|
||||
result.add(e.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<InventoryVector, Long> getCache(long stream) {
|
||||
Map<InventoryVector, Long> result = cache.get(stream);
|
||||
if (result == null) {
|
||||
synchronized (cache) {
|
||||
if (cache.get(stream) == null) {
|
||||
result = new ConcurrentHashMap<>();
|
||||
cache.put(stream, result);
|
||||
|
||||
String[] projection = {
|
||||
COLUMN_HASH, COLUMN_EXPIRES
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = " + stream,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
|
||||
long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES));
|
||||
result.put(new InventoryVector(blob), expires);
|
||||
}
|
||||
}
|
||||
LOG.info("Stream #" + stream + " inventory size: " + result.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) {
|
||||
for (long stream : streams) {
|
||||
offer.removeAll(getCache(stream).keySet());
|
||||
}
|
||||
LOG.info(offer.size() + " objects missing.");
|
||||
return offer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectMessage getObject(InventoryVector vector) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'" + vector + "'",
|
||||
null, null, null, null
|
||||
)) {
|
||||
if (!c.moveToFirst()) {
|
||||
LOG.info("Object requested that we don't have. IV: " + vector);
|
||||
return null;
|
||||
}
|
||||
|
||||
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
StringBuilder where = new StringBuilder("1=1");
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream);
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version);
|
||||
}
|
||||
if (types.length > 0) {
|
||||
where.append(" AND type IN (").append(join(types)).append(")");
|
||||
}
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
List<ObjectMessage> result = new LinkedList<>();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(),
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob),
|
||||
blob.length));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeObject(ObjectMessage object) {
|
||||
InventoryVector iv = object.getInventoryVector();
|
||||
|
||||
if (getCache(object.getStream()).containsKey(iv))
|
||||
return;
|
||||
|
||||
LOG.trace("Storing object " + iv);
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_HASH, object.getInventoryVector().getHash());
|
||||
values.put(COLUMN_STREAM, object.getStream());
|
||||
values.put(COLUMN_EXPIRES, object.getExpiresTime());
|
||||
values.put(COLUMN_DATA, Encode.bytes(object));
|
||||
values.put(COLUMN_TYPE, object.getType());
|
||||
values.put(COLUMN_VERSION, object.getVersion());
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
|
||||
getCache(object.getStream()).put(iv, object.getExpiresTime());
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(ObjectMessage object) {
|
||||
return getCache(object.getStream()).keySet().contains(object.getInventoryVector());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
long fiveMinutesAgo = now() - 5 * MINUTE;
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "expires < ?", new String[]{valueOf(fiveMinutesAgo)});
|
||||
|
||||
for (Map<InventoryVector, Long> c : cache.values()) {
|
||||
Iterator<Map.Entry<InventoryVector, Long>> iterator = c.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().getValue() < fiveMinutesAgo) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* 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 android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import ch.dissem.apps.abit.util.Labels;
|
||||
import ch.dissem.apps.abit.util.UuidUtils;
|
||||
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 static ch.dissem.apps.abit.util.UuidUtils.asUuid;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* {@link MessageRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidMessageRepository extends AbstractMessageRepository {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
|
||||
|
||||
public static final Label LABEL_ARCHIVE = new Label("archive", null, 0);
|
||||
|
||||
private static final String TABLE_NAME = "Message";
|
||||
private static final String COLUMN_ID = "id";
|
||||
private static final String COLUMN_IV = "iv";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_SENDER = "sender";
|
||||
private static final String COLUMN_RECIPIENT = "recipient";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_ACK_DATA = "ack_data";
|
||||
private static final String COLUMN_SENT = "sent";
|
||||
private static final String COLUMN_RECEIVED = "received";
|
||||
private static final String COLUMN_STATUS = "status";
|
||||
private static final String COLUMN_TTL = "ttl";
|
||||
private static final String COLUMN_RETRIES = "retries";
|
||||
private static final String COLUMN_NEXT_TRY = "next_try";
|
||||
private static final String COLUMN_INITIAL_HASH = "initial_hash";
|
||||
private static final String COLUMN_CONVERSATION = "conversation";
|
||||
|
||||
private static final String PARENTS_TABLE_NAME = "Message_Parent";
|
||||
|
||||
private static final String JOIN_TABLE_NAME = "Message_Label";
|
||||
private static final String JT_COLUMN_MESSAGE = "message_id";
|
||||
private static final String JT_COLUMN_LABEL = "label_id";
|
||||
|
||||
private static final String LBL_TABLE_NAME = "Label";
|
||||
private static final String LBL_COLUMN_ID = "id";
|
||||
private static final String LBL_COLUMN_LABEL = "label";
|
||||
private static final String LBL_COLUMN_TYPE = "type";
|
||||
private static final String LBL_COLUMN_COLOR = "color";
|
||||
private static final String LBL_COLUMN_ORDER = "ord";
|
||||
private final SqlHelper sql;
|
||||
private final Context context;
|
||||
|
||||
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
|
||||
this.sql = sql;
|
||||
this.context = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plaintext> findMessages(Label label) {
|
||||
if (label == LABEL_ARCHIVE) {
|
||||
return super.findMessages((Label) null);
|
||||
} else {
|
||||
return super.findMessages(label);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Label> findLabels(String where) {
|
||||
List<Label> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
LBL_COLUMN_ID,
|
||||
LBL_COLUMN_LABEL,
|
||||
LBL_COLUMN_TYPE,
|
||||
LBL_COLUMN_COLOR
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
LBL_TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null,
|
||||
LBL_COLUMN_ORDER
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
result.add(getLabel(c));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Label getLabel(Cursor c) {
|
||||
String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE));
|
||||
Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName);
|
||||
String text = Labels.getText(type, null, context);
|
||||
if (text == null) {
|
||||
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
|
||||
}
|
||||
Label label = new Label(
|
||||
text,
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)));
|
||||
label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID)));
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUnread(Label label) {
|
||||
String[] args;
|
||||
String where;
|
||||
if (label == null) {
|
||||
return 0;
|
||||
}
|
||||
if (label == LABEL_ARCHIVE) {
|
||||
where = "";
|
||||
args = new String[]{
|
||||
Label.Type.UNREAD.name()
|
||||
};
|
||||
} else {
|
||||
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND ";
|
||||
args = new String[]{
|
||||
label.getId().toString(),
|
||||
Label.Type.UNREAD.name()
|
||||
};
|
||||
}
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME,
|
||||
where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" +
|
||||
"SELECT id FROM Label WHERE type=?))",
|
||||
args
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UUID> findConversations(Label label) {
|
||||
String[] projection = {
|
||||
COLUMN_CONVERSATION,
|
||||
};
|
||||
|
||||
String where;
|
||||
if (label == null) {
|
||||
where = "id NOT IN (SELECT message_id FROM Message_Label)";
|
||||
} else {
|
||||
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")";
|
||||
}
|
||||
List<UUID> result = new LinkedList<>();
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION));
|
||||
result.add(asUuid(uuidBytes));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private void updateParents(SQLiteDatabase db, Plaintext message) {
|
||||
if (message.getInventoryVector() == null || message.getParents().isEmpty()) {
|
||||
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
|
||||
return;
|
||||
}
|
||||
byte[] childIV = message.getInventoryVector().getHash();
|
||||
db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()});
|
||||
|
||||
// save new parents
|
||||
int order = 0;
|
||||
for (InventoryVector parentIV : message.getParents()) {
|
||||
Plaintext parent = getMessage(parentIV);
|
||||
mergeConversations(db, parent.getConversationId(), message.getConversationId());
|
||||
order++;
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("parent", parentIV.getHash());
|
||||
values.put("child", childIV);
|
||||
values.put("pos", order);
|
||||
values.put("conversation", UuidUtils.asBytes(message.getConversationId()));
|
||||
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 void mergeConversations(SQLiteDatabase db, UUID source, UUID target) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("conversation", UuidUtils.asBytes(target));
|
||||
String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()};
|
||||
db.update(TABLE_NAME, values, "conversation=?", whereArgs);
|
||||
db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs);
|
||||
}
|
||||
|
||||
protected List<Plaintext> find(String where) {
|
||||
List<Plaintext> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
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
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null,
|
||||
COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC"
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV));
|
||||
byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex
|
||||
(COLUMN_TYPE)));
|
||||
Plaintext.Builder builder = Plaintext.readWithoutSignature(type,
|
||||
new ByteArrayInputStream(data));
|
||||
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
|
||||
builder.id(id);
|
||||
builder.IV(new InventoryVector(iv));
|
||||
builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex
|
||||
(COLUMN_SENDER))));
|
||||
builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex
|
||||
(COLUMN_RECIPIENT))));
|
||||
builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA)));
|
||||
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)));
|
||||
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)));
|
||||
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex
|
||||
(COLUMN_STATUS))));
|
||||
builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL)));
|
||||
builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES)));
|
||||
int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY);
|
||||
if (!c.isNull(nextTryColumn)) {
|
||||
builder.nextTry(c.getLong(nextTryColumn));
|
||||
}
|
||||
builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))));
|
||||
builder.labels(findLabels(id));
|
||||
result.add(builder.build());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Collection<Label> findLabels(long id) {
|
||||
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Plaintext message) {
|
||||
saveContactIfNecessary(message.getFrom());
|
||||
saveContactIfNecessary(message.getTo());
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
// save message
|
||||
if (message.getId() == null) {
|
||||
insert(db, message);
|
||||
} else {
|
||||
update(db, message);
|
||||
}
|
||||
|
||||
updateParents(db, message);
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())});
|
||||
|
||||
// save labels
|
||||
ContentValues values = new ContentValues();
|
||||
for (Label label : message.getLabels()) {
|
||||
values.put(JT_COLUMN_LABEL, (Long) label.getId());
|
||||
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(SQLiteDatabase db, Plaintext message) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message
|
||||
.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_ACK_DATA, message.getAckData());
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
values.put(COLUMN_INITIAL_HASH, message.getInitialHash());
|
||||
values.put(COLUMN_TTL, message.getTTL());
|
||||
values.put(COLUMN_RETRIES, message.getRetries());
|
||||
values.put(COLUMN_NEXT_TRY, message.getNextTry());
|
||||
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
|
||||
long id = db.insertOrThrow(TABLE_NAME, null, values);
|
||||
message.setId(id);
|
||||
}
|
||||
|
||||
private void update(SQLiteDatabase db, Plaintext message) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message
|
||||
.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_ACK_DATA, message.getAckData());
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
values.put(COLUMN_INITIAL_HASH, message.getInitialHash());
|
||||
values.put(COLUMN_TTL, message.getTTL());
|
||||
values.put(COLUMN_RETRIES, message.getRetries());
|
||||
values.put(COLUMN_NEXT_TRY, message.getNextTry());
|
||||
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
|
||||
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Plaintext message) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "id = " + message.getId(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package ch.dissem.apps.abit.repository;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
|
||||
import ch.dissem.bitmessage.exception.ApplicationException;
|
||||
import ch.dissem.bitmessage.ports.NodeRegistry;
|
||||
import ch.dissem.bitmessage.utils.Collections;
|
||||
import ch.dissem.bitmessage.utils.SqlStrings;
|
||||
|
||||
import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.now;
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AndroidNodeRegistry implements NodeRegistry {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
|
||||
|
||||
private static final String TABLE_NAME = "Node";
|
||||
private static final String COLUMN_STREAM = "stream";
|
||||
private static final String COLUMN_ADDRESS = "address";
|
||||
private static final String COLUMN_PORT = "port";
|
||||
private static final String COLUMN_SERVICES = "services";
|
||||
private static final String COLUMN_TIME = "time";
|
||||
|
||||
private final ThreadLocal<SQLiteStatement> loadExistingStatement = new ThreadLocal<>();
|
||||
|
||||
private final SqlHelper sql;
|
||||
private Map<Long, Set<NetworkAddress>> stableNodes;
|
||||
|
||||
public AndroidNodeRegistry(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
cleanUp();
|
||||
}
|
||||
|
||||
private void cleanUp() {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
private Long loadExistingTime(NetworkAddress node) {
|
||||
SQLiteStatement statement = loadExistingStatement.get();
|
||||
if (statement == null) {
|
||||
statement = sql.getWritableDatabase().compileStatement(
|
||||
"SELECT " + COLUMN_TIME +
|
||||
" FROM " + TABLE_NAME +
|
||||
" WHERE stream=? AND address=? AND port=?"
|
||||
);
|
||||
loadExistingStatement.set(statement);
|
||||
}
|
||||
statement.bindLong(1, node.getStream());
|
||||
statement.bindBlob(2, node.getIPv6());
|
||||
statement.bindLong(3, node.getPort());
|
||||
try {
|
||||
return statement.simpleQueryForLong();
|
||||
} catch (SQLiteDoneException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NetworkAddress> getKnownAddresses(int limit, long... streams) {
|
||||
String[] projection = {
|
||||
COLUMN_STREAM,
|
||||
COLUMN_ADDRESS,
|
||||
COLUMN_PORT,
|
||||
COLUMN_SERVICES,
|
||||
COLUMN_TIME
|
||||
};
|
||||
|
||||
List<NetworkAddress> result = new LinkedList<>();
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream IN (?)",
|
||||
new String[]{SqlStrings.join(streams).toString()},
|
||||
null, null,
|
||||
"time DESC",
|
||||
valueOf(limit)
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
result.add(
|
||||
new NetworkAddress.Builder()
|
||||
.stream(c.getLong(c.getColumnIndex(COLUMN_STREAM)))
|
||||
.ipv6(c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)))
|
||||
.port(c.getInt(c.getColumnIndex(COLUMN_PORT)))
|
||||
.services(c.getLong(c.getColumnIndex(COLUMN_SERVICES)))
|
||||
.time(c.getLong(c.getColumnIndex(COLUMN_TIME)))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
throw new ApplicationException(e);
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
synchronized (this) {
|
||||
if (stableNodes == null) {
|
||||
stableNodes = loadStableNodes();
|
||||
}
|
||||
}
|
||||
for (long stream : streams) {
|
||||
Set<NetworkAddress> nodes = stableNodes.get(stream);
|
||||
if (nodes != null && !nodes.isEmpty()) {
|
||||
result.add(Collections.selectRandom(nodes));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offerAddresses(List<NetworkAddress> nodes) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
cleanUp();
|
||||
for (NetworkAddress node : nodes) {
|
||||
if (node.getTime() < now(+5 * MINUTE) && node.getTime() > now(-28 * DAY)) {
|
||||
synchronized (this) {
|
||||
Long existing = loadExistingTime(node);
|
||||
if (existing == null) {
|
||||
insert(node);
|
||||
} else if (node.getTime() > existing) {
|
||||
update(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(NetworkAddress node) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_STREAM, node.getStream());
|
||||
values.put(COLUMN_ADDRESS, node.getIPv6());
|
||||
values.put(COLUMN_PORT, node.getPort());
|
||||
values.put(COLUMN_SERVICES, node.getServices());
|
||||
values.put(COLUMN_TIME, node.getTime());
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void update(NetworkAddress node) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_SERVICES, node.getServices());
|
||||
values.put(COLUMN_TIME, node.getTime());
|
||||
|
||||
db.update(TABLE_NAME, values,
|
||||
"stream=" + node.getStream() + " AND address=X'" + hex(node.getIPv6()) + "' AND " +
|
||||
"port=" + node.getPort(),
|
||||
null);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
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 static ch.dissem.bitmessage.utils.Singleton.cryptography;
|
||||
import static ch.dissem.bitmessage.utils.Strings.hex;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext
|
||||
.ContextHolder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "POW";
|
||||
private static final String COLUMN_INITIAL_HASH = "initial_hash";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte";
|
||||
private static final String COLUMN_EXTRA_BYTES = "extra_bytes";
|
||||
private static final String COLUMN_EXPIRATION_TIME = "expiration_time";
|
||||
private static final String COLUMN_MESSAGE_ID = "message_id";
|
||||
|
||||
private final SqlHelper sql;
|
||||
private InternalContext bmc;
|
||||
|
||||
public AndroidProofOfWorkRepository(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext internalContext) {
|
||||
this.bmc = internalContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item getItem(byte[] initialHash) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_DATA,
|
||||
COLUMN_VERSION,
|
||||
COLUMN_NONCE_TRIALS_PER_BYTE,
|
||||
COLUMN_EXTRA_BYTES,
|
||||
COLUMN_EXPIRATION_TIME,
|
||||
COLUMN_MESSAGE_ID
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'" + hex(initialHash) + "'",
|
||||
null, null, null, null
|
||||
)) {
|
||||
if (c.moveToFirst()) {
|
||||
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
|
||||
return new Item(
|
||||
Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob
|
||||
.length),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
|
||||
);
|
||||
} else {
|
||||
return new Item(
|
||||
Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob
|
||||
.length),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
|
||||
bmc.getMessageRepository().getMessage(
|
||||
c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Object requested that we don't have. Initial hash: " +
|
||||
hex(initialHash));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> getItems() {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_INITIAL_HASH
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
List<byte[]> result = new LinkedList<>();
|
||||
try (Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
null, null, null, null, null
|
||||
)) {
|
||||
while (c.moveToNext()) {
|
||||
byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH));
|
||||
result.add(initialHash);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(Item item) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.object));
|
||||
values.put(COLUMN_DATA, Encode.bytes(item.object));
|
||||
values.put(COLUMN_VERSION, item.object.getVersion());
|
||||
values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte);
|
||||
values.put(COLUMN_EXTRA_BYTES, item.extraBytes);
|
||||
if (item.message != null) {
|
||||
values.put(COLUMN_EXPIRATION_TIME, item.expirationTime);
|
||||
values.put(COLUMN_MESSAGE_ID, (Long) item.message.getId());
|
||||
}
|
||||
|
||||
db.insertOrThrow(TABLE_NAME, null, values);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
LOG.trace(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
|
||||
putObject(new Item(object, nonceTrialsPerByte, extraBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObject(byte[] initialHash) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(
|
||||
TABLE_NAME,
|
||||
"initial_hash=X'" + hex(initialHash) + "'",
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import ch.dissem.apps.abit.util.Assets;
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
public class SqlHelper extends SQLiteOpenHelper {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
private static final int DATABASE_VERSION = 7;
|
||||
private static final String DATABASE_NAME = "jabit.db";
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
public SqlHelper(Context ctx) {
|
||||
super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
executeMigration(db, "V1.0__Create_table_inventory");
|
||||
executeMigration(db, "V1.1__Create_table_address");
|
||||
executeMigration(db, "V1.2__Create_table_message");
|
||||
case 1:
|
||||
// executeMigration(db, "V2.0__Update_table_message");
|
||||
executeMigration(db, "V2.1__Create_table_POW");
|
||||
case 2:
|
||||
executeMigration(db, "V3.0__Update_table_address");
|
||||
case 3:
|
||||
executeMigration(db, "V3.1__Update_table_POW");
|
||||
executeMigration(db, "V3.2__Update_table_message");
|
||||
case 4:
|
||||
executeMigration(db, "V3.3__Create_table_node");
|
||||
case 5:
|
||||
executeMigration(db, "V3.4__Add_label_outbox");
|
||||
case 6:
|
||||
executeMigration(db, "V4.0__Create_table_message_parent");
|
||||
default:
|
||||
// Nothing to do. Let's assume we won't upgrade from a version that's newer than
|
||||
// DATABASE_VERSION.
|
||||
}
|
||||
}
|
||||
|
||||
private void executeMigration(SQLiteDatabase db, String name) {
|
||||
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
|
||||
db.execSQL(statement);
|
||||
}
|
||||
}
|
||||
|
||||
static StringBuilder join(long... numbers) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append(numbers[i]);
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
|
||||
static StringBuilder join(Enum<?>... types) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append('\'').append(types[i].name()).append('\'');
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.dialog.FullNodeDialogActivity;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
public class BitmessageIntentService extends IntentService {
|
||||
public static final String EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage";
|
||||
public static final String EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode";
|
||||
public static final String EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode";
|
||||
|
||||
private BitmessageContext bmc;
|
||||
|
||||
public BitmessageIntentService() {
|
||||
super("BitmessageIntentService");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
bmc = Singleton.getBitmessageContext(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (intent.hasExtra(EXTRA_DELETE_MESSAGE)) {
|
||||
Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_DELETE_MESSAGE);
|
||||
bmc.labeler().delete(item);
|
||||
bmc.messages().save(item);
|
||||
Singleton.getMessageListener(this).resetNotification();
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_STARTUP_NODE)) {
|
||||
if (Preferences.isConnectionAllowed(this)) {
|
||||
startService(new Intent(this, BitmessageService.class));
|
||||
updateNodeSwitch();
|
||||
} else {
|
||||
Intent dialogIntent = new Intent(this, FullNodeDialogActivity.class);
|
||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(dialogIntent);
|
||||
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
||||
stopService(new Intent(this, BitmessageService.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.utils.Property;
|
||||
|
||||
import static ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIFICATION_ID;
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
* sync adapter class, allowing the sync adapter framework to call
|
||||
* onPerformSync().
|
||||
*/
|
||||
public class BitmessageService extends Service {
|
||||
private static BitmessageContext bmc = null;
|
||||
private static volatile boolean running = false;
|
||||
|
||||
private NetworkNotification notification = null;
|
||||
|
||||
private final Handler cleanupHandler = new Handler();
|
||||
private final Runnable cleanupTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
bmc.cleanup();
|
||||
if (isRunning()) {
|
||||
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running && bmc.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (bmc == null) {
|
||||
bmc = Singleton.getBitmessageContext(this);
|
||||
}
|
||||
notification = new NetworkNotification(this);
|
||||
running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (!isRunning()) {
|
||||
running = true;
|
||||
notification.connecting();
|
||||
startForeground(NETWORK_NOTIFICATION_ID, notification.getNotification());
|
||||
if (!bmc.isRunning()) {
|
||||
bmc.startup();
|
||||
}
|
||||
notification.show();
|
||||
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L);
|
||||
}
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (bmc.isRunning()) {
|
||||
bmc.shutdown();
|
||||
}
|
||||
running = false;
|
||||
notification.showShutdown();
|
||||
cleanupHandler.removeCallbacks(cleanupTask);
|
||||
bmc.cleanup();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Property getStatus() {
|
||||
if (bmc != null) {
|
||||
return bmc.status();
|
||||
} else {
|
||||
return new Property("bitmessage context", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification;
|
||||
import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_NOTIFICATION_ID;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class ProofOfWorkService extends Service {
|
||||
// Object to use as a thread-safe lock
|
||||
private static final ProofOfWorkEngine engine = new MultiThreadedPOWEngine();
|
||||
private static final Queue<PowItem> queue = new LinkedList<>();
|
||||
private static boolean calculating;
|
||||
private ProofOfWorkNotification notification;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
notification = new ProofOfWorkNotification(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new PowBinder(this);
|
||||
}
|
||||
|
||||
public static class PowBinder extends Binder {
|
||||
private final ProofOfWorkService service;
|
||||
private final ProofOfWorkNotification notification;
|
||||
|
||||
private PowBinder(ProofOfWorkService service) {
|
||||
this.service = service;
|
||||
this.notification = service.notification;
|
||||
}
|
||||
|
||||
void process(PowItem item) {
|
||||
synchronized (queue) {
|
||||
service.startService(new Intent(service, ProofOfWorkService.class));
|
||||
service.startForeground(ONGOING_NOTIFICATION_ID,
|
||||
notification.getNotification());
|
||||
if (!calculating) {
|
||||
calculating = true;
|
||||
service.calculateNonce(item);
|
||||
} else {
|
||||
queue.add(item);
|
||||
notification.update(queue.size()).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class PowItem {
|
||||
private final byte[] initialHash;
|
||||
private final byte[] targetValue;
|
||||
private final ProofOfWorkEngine.Callback callback;
|
||||
|
||||
PowItem(byte[] initialHash, byte[] targetValue, ProofOfWorkEngine.Callback callback) {
|
||||
this.initialHash = initialHash;
|
||||
this.targetValue = targetValue;
|
||||
this.callback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateNonce(final PowItem item) {
|
||||
engine.calculateNonce(item.initialHash, item.targetValue, new ProofOfWorkEngine.Callback() {
|
||||
@Override
|
||||
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
|
||||
try {
|
||||
item.callback.onNonceCalculated(initialHash, nonce);
|
||||
} finally {
|
||||
PowItem next;
|
||||
synchronized (queue) {
|
||||
next = queue.poll();
|
||||
if (next == null) {
|
||||
calculating = false;
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
} else {
|
||||
notification.update(queue.size()).show();
|
||||
}
|
||||
}
|
||||
if (next != null) {
|
||||
calculateNonce(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 java.util.Queue;
|
||||
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder;
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
|
||||
|
||||
import static android.content.Context.BIND_AUTO_CREATE;
|
||||
|
||||
/**
|
||||
* Proof of Work engine that uses the Proof of Work service.
|
||||
*/
|
||||
public class ServicePowEngine implements ProofOfWorkEngine {
|
||||
private final Context ctx;
|
||||
|
||||
private static final Object lock = new Object();
|
||||
private final Queue<PowItem> queue = new LinkedList<>();
|
||||
private PowBinder service;
|
||||
|
||||
public ServicePowEngine(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
private final ServiceConnection connection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
synchronized (lock) {
|
||||
ServicePowEngine.this.service = (PowBinder) service;
|
||||
while (!queue.isEmpty()) {
|
||||
ServicePowEngine.this.service.process(queue.poll());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
service = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void calculateNonce(byte[] initialHash, byte[] targetValue, Callback callback) {
|
||||
PowItem item = new PowItem(initialHash, targetValue, callback);
|
||||
synchronized (lock) {
|
||||
if (service != null) {
|
||||
service.process(item);
|
||||
} else {
|
||||
queue.add(item);
|
||||
ctx.bindService(new Intent(ctx, ProofOfWorkService.class), connection,
|
||||
BIND_AUTO_CREATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.SwitchingProofOfWorkEngine;
|
||||
import ch.dissem.apps.abit.listener.MessageListener;
|
||||
import ch.dissem.apps.abit.pow.ServerPowEngine;
|
||||
import ch.dissem.apps.abit.repository.AndroidAddressRepository;
|
||||
import ch.dissem.apps.abit.repository.AndroidInventory;
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository;
|
||||
import ch.dissem.apps.abit.repository.AndroidNodeRegistry;
|
||||
import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository;
|
||||
import ch.dissem.apps.abit.repository.SqlHelper;
|
||||
import ch.dissem.apps.abit.util.Constants;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler;
|
||||
import ch.dissem.bitmessage.ports.AddressRepository;
|
||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
|
||||
import ch.dissem.bitmessage.utils.ConversationService;
|
||||
import ch.dissem.bitmessage.utils.TTL;
|
||||
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
|
||||
|
||||
/**
|
||||
* Provides singleton objects across the application.
|
||||
*/
|
||||
public class Singleton {
|
||||
private static BitmessageContext bitmessageContext;
|
||||
private static ConversationService conversationService;
|
||||
private static MessageListener messageListener;
|
||||
private static BitmessageAddress identity;
|
||||
private static AndroidProofOfWorkRepository powRepo;
|
||||
private static boolean creatingIdentity;
|
||||
|
||||
public static BitmessageContext getBitmessageContext(Context context) {
|
||||
if (bitmessageContext == null) {
|
||||
synchronized (Singleton.class) {
|
||||
if (bitmessageContext == null) {
|
||||
final Context ctx = context.getApplicationContext();
|
||||
SqlHelper sqlHelper = new SqlHelper(ctx);
|
||||
powRepo = new AndroidProofOfWorkRepository(sqlHelper);
|
||||
TTL.pubkey(2 * DAY);
|
||||
bitmessageContext = new BitmessageContext.Builder()
|
||||
.proofOfWorkEngine(new SwitchingProofOfWorkEngine(
|
||||
ctx, Constants.PREFERENCE_SERVER_POW,
|
||||
new ServerPowEngine(ctx),
|
||||
new ServicePowEngine(ctx)
|
||||
))
|
||||
.cryptography(new AndroidCryptography())
|
||||
.nodeRegistry(new AndroidNodeRegistry(sqlHelper))
|
||||
.inventory(new AndroidInventory(sqlHelper))
|
||||
.addressRepo(new AndroidAddressRepository(sqlHelper))
|
||||
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
|
||||
.powRepo(powRepo)
|
||||
.networkHandler(new NioNetworkHandler())
|
||||
.listener(getMessageListener(ctx))
|
||||
.doNotSendPubkeyOnIdentityCreation()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitmessageContext;
|
||||
}
|
||||
|
||||
public static MessageListener getMessageListener(Context ctx) {
|
||||
if (messageListener == null) {
|
||||
synchronized (Singleton.class) {
|
||||
if (messageListener == null) {
|
||||
messageListener = new MessageListener(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return messageListener;
|
||||
}
|
||||
|
||||
public static MessageRepository getMessageRepository(Context ctx) {
|
||||
return getBitmessageContext(ctx).messages();
|
||||
}
|
||||
|
||||
public static AddressRepository getAddressRepository(Context ctx) {
|
||||
return getBitmessageContext(ctx).addresses();
|
||||
}
|
||||
|
||||
public static ProofOfWorkRepository getProofOfWorkRepository(Context ctx) {
|
||||
if (powRepo == null) getBitmessageContext(ctx);
|
||||
return powRepo;
|
||||
}
|
||||
|
||||
public static BitmessageAddress getIdentity(final Context ctx) {
|
||||
if (identity == null) {
|
||||
final BitmessageContext bmc = getBitmessageContext(ctx);
|
||||
synchronized (Singleton.class) {
|
||||
if (identity == null) {
|
||||
List<BitmessageAddress> identities = bmc.addresses()
|
||||
.getIdentities();
|
||||
if (identities.size() > 0) {
|
||||
identity = identities.get(0);
|
||||
} else {
|
||||
if (!creatingIdentity) {
|
||||
creatingIdentity = true;
|
||||
new AsyncTask<Void, Void, BitmessageAddress>() {
|
||||
@Override
|
||||
protected BitmessageAddress doInBackground(Void... args) {
|
||||
BitmessageAddress identity = bmc.createIdentity(false,
|
||||
Pubkey.Feature.DOES_ACK);
|
||||
identity.setAlias(
|
||||
ctx.getString(R.string.alias_default_identity)
|
||||
);
|
||||
bmc.addresses().save(identity);
|
||||
return identity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(BitmessageAddress identity) {
|
||||
Singleton.identity = identity;
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
MainActivity mainActivity = MainActivity.getInstance();
|
||||
if (mainActivity != null) {
|
||||
mainActivity.addIdentityEntry(identity);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
public static void setIdentity(BitmessageAddress identity) {
|
||||
if (identity.getPrivateKey() == null)
|
||||
throw new IllegalArgumentException("Identity expected, but no private key available");
|
||||
Singleton.identity = identity;
|
||||
}
|
||||
|
||||
public static ConversationService getConversationService(Context ctx) {
|
||||
if (conversationService == null) {
|
||||
final BitmessageContext bmc = getBitmessageContext(ctx);
|
||||
synchronized (Singleton.class) {
|
||||
if (conversationService == null) {
|
||||
conversationService = new ConversationService(bmc.messages());
|
||||
}
|
||||
}
|
||||
}
|
||||
return conversationService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public class Authenticator extends AbstractAccountAuthenticator {
|
||||
public static final Account ACCOUNT_SYNC = new Account("Bitmessage", "ch.dissem.bitmessage");
|
||||
public static final Account ACCOUNT_POW = new Account("Proof of Work ", "ch.dissem.bitmessage");
|
||||
|
||||
// Simple constructor
|
||||
public Authenticator(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
// Editing properties is not supported
|
||||
@Override
|
||||
public Bundle editProperties(
|
||||
AccountAuthenticatorResponse r, String s) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// Don't add additional accounts
|
||||
@Override
|
||||
public Bundle addAccount(
|
||||
AccountAuthenticatorResponse r,
|
||||
String s,
|
||||
String s2,
|
||||
String[] strings,
|
||||
Bundle bundle) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore attempts to confirm credentials
|
||||
@Override
|
||||
public Bundle confirmCredentials(
|
||||
AccountAuthenticatorResponse r,
|
||||
Account account,
|
||||
Bundle bundle) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Getting an authentication token is not supported
|
||||
@Override
|
||||
public Bundle getAuthToken(
|
||||
AccountAuthenticatorResponse r,
|
||||
Account account,
|
||||
String s,
|
||||
Bundle bundle) throws NetworkErrorException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// Getting a label for the auth token is not supported
|
||||
@Override
|
||||
public String getAuthTokenLabel(String s) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// Updating user credentials is not supported
|
||||
@Override
|
||||
public Bundle updateCredentials(
|
||||
AccountAuthenticatorResponse r,
|
||||
Account account,
|
||||
String s, Bundle bundle) throws NetworkErrorException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// Checking features for the account is not supported
|
||||
@Override
|
||||
public Bundle hasFeatures(
|
||||
AccountAuthenticatorResponse r,
|
||||
Account account, String[] strings) throws NetworkErrorException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
import android.os.IBinder;
|
||||
|
||||
/**
|
||||
* A bound Service that instantiates the authenticator
|
||||
* when started.
|
||||
*/
|
||||
public class AuthenticatorService extends Service {
|
||||
/**
|
||||
* Instance field that stores the authenticator object
|
||||
*/
|
||||
private Authenticator authenticator;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Create a new authenticator object
|
||||
authenticator = new Authenticator(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* When the system binds to this Service to make the RPC call
|
||||
* return the authenticator's IBinder.
|
||||
*/
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return authenticator.getIBinder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/*
|
||||
* Define an implementation of ContentProvider that stubs out
|
||||
* all methods
|
||||
*/
|
||||
public class StubProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "ch.dissem.apps.abit.provider";
|
||||
|
||||
/*
|
||||
* Always return true, indicating that the
|
||||
* provider loaded correctly.
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return no type for MIME type
|
||||
*/
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* query() always returns no results
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(
|
||||
@NonNull Uri uri,
|
||||
String[] projection,
|
||||
String selection,
|
||||
String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* insert() always returns null (no URI)
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* delete() always returns "no rows affected" (0)
|
||||
*/
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* update() always returns "no rows affected" (0)
|
||||
*/
|
||||
public int update(
|
||||
@NonNull Uri uri,
|
||||
ContentValues values,
|
||||
String selection,
|
||||
String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.apps.abit.util.Preferences;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.CustomMessage;
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException;
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
|
||||
|
||||
import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_POW;
|
||||
import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_SYNC;
|
||||
import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY;
|
||||
import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE;
|
||||
import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE;
|
||||
import static ch.dissem.bitmessage.utils.Singleton.cryptography;
|
||||
|
||||
/**
|
||||
* Sync Adapter to synchronize with the Bitmessage network - fetches
|
||||
* new objects and then disconnects.
|
||||
*/
|
||||
public class SyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
private final static Logger LOG = LoggerFactory.getLogger(SyncAdapter.class);
|
||||
|
||||
private static final long SYNC_FREQUENCY = 15 * 60; // seconds
|
||||
|
||||
private final BitmessageContext bmc;
|
||||
|
||||
/**
|
||||
* Set up the sync adapter
|
||||
*/
|
||||
public SyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
bmc = Singleton.getBitmessageContext(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority,
|
||||
ContentProviderClient provider, SyncResult syncResult) {
|
||||
try {
|
||||
if (account.equals(Authenticator.ACCOUNT_SYNC)) {
|
||||
if (Preferences.isConnectionAllowed(getContext())) {
|
||||
syncData();
|
||||
}
|
||||
} else if (account.equals(Authenticator.ACCOUNT_POW)) {
|
||||
syncPOW();
|
||||
} else {
|
||||
syncResult.stats.numAuthExceptions++;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
syncResult.stats.numIoExceptions++;
|
||||
} catch (DecryptionFailedException e) {
|
||||
syncResult.stats.numAuthExceptions++;
|
||||
}
|
||||
}
|
||||
|
||||
private void syncData() throws IOException {
|
||||
// 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;
|
||||
}
|
||||
LOG.info("Synchronizing Bitmessage");
|
||||
|
||||
LOG.info("Synchronization started");
|
||||
bmc.synchronize(
|
||||
Preferences.getTrustedNode(getContext()),
|
||||
Preferences.getTrustedNodePort(getContext()),
|
||||
Preferences.getTimeoutInSeconds(getContext()),
|
||||
true);
|
||||
LOG.info("Synchronization finished");
|
||||
}
|
||||
|
||||
private void syncPOW() throws IOException, DecryptionFailedException {
|
||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||
LOG.info("Looking for completed POW");
|
||||
|
||||
BitmessageAddress identity = Singleton.getIdentity(getContext());
|
||||
byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey();
|
||||
byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey());
|
||||
ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity);
|
||||
ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext());
|
||||
List<byte[]> items = powRepo.getItems();
|
||||
for (byte[] initialHash : items) {
|
||||
ProofOfWorkRepository.Item item = powRepo.getItem(initialHash);
|
||||
byte[] target = cryptography().getProofOfWorkTarget(item.object, item
|
||||
.nonceTrialsPerByte, item.extraBytes);
|
||||
CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>(
|
||||
new ProofOfWorkRequest(identity, initialHash, CALCULATE, target));
|
||||
cryptoMsg.signAndEncrypt(identity, signingKey);
|
||||
CustomMessage response = bmc.send(
|
||||
Preferences.getTrustedNode(getContext()),
|
||||
Preferences.getTrustedNodePort(getContext()),
|
||||
cryptoMsg
|
||||
);
|
||||
if (response.isError()) {
|
||||
LOG.error("Server responded with error: " + new String(response.getData(),
|
||||
"UTF-8"));
|
||||
} else {
|
||||
ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read(
|
||||
response, reader).decrypt(privateKey);
|
||||
if (decryptedResponse.getRequest() == COMPLETE) {
|
||||
bmc.internals().getProofOfWorkService().onNonceCalculated(
|
||||
initialHash, decryptedResponse.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.size() == 0) {
|
||||
stopPowSync(getContext());
|
||||
}
|
||||
LOG.info("Synchronization finished");
|
||||
}
|
||||
|
||||
public static void startSync(Context ctx) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
Account 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, new Bundle(), SYNC_FREQUENCY);
|
||||
}
|
||||
|
||||
public static void stopSync(Context ctx) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
Account account = addAccount(ctx, ACCOUNT_SYNC);
|
||||
|
||||
ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle());
|
||||
}
|
||||
|
||||
|
||||
public static void startPowSync(Context ctx) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
Account 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, new Bundle(), SYNC_FREQUENCY);
|
||||
}
|
||||
|
||||
public static void stopPowSync(Context ctx) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
Account account = addAccount(ctx, ACCOUNT_POW);
|
||||
|
||||
ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle());
|
||||
}
|
||||
|
||||
private static Account addAccount(Context ctx, 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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;
|
||||
import android.os.IBinder;
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
* sync adapter class, allowing the sync adapter framework to call
|
||||
* onPerformSync().
|
||||
*/
|
||||
public class SyncService extends Service {
|
||||
// Storage for an instance of the sync adapter
|
||||
private static SyncAdapter syncAdapter = null;
|
||||
// Object to use as a thread-safe lock
|
||||
private static final Object syncAdapterLock = new Object();
|
||||
|
||||
/**
|
||||
* Instantiate the sync adapter object.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
/*
|
||||
* Create the sync adapter as a singleton.
|
||||
* Set the sync adapter as syncable
|
||||
* Disallow parallel syncs
|
||||
*/
|
||||
synchronized (syncAdapterLock) {
|
||||
if (syncAdapter == null) {
|
||||
syncAdapter = new SyncAdapter(this, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that allows the system to invoke
|
||||
* the sync adapter.
|
||||
*/
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
/*
|
||||
* Get the object that allows external processes
|
||||
* to call onPerformSync(). The object is created
|
||||
* in the base class code when the SyncAdapter
|
||||
* constructors call super()
|
||||
*/
|
||||
return syncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
|
||||
/**
|
||||
* Helper class to work with Assets.
|
||||
*/
|
||||
public class Assets {
|
||||
public static List<String> readSqlStatements(Context ctx, String name) {
|
||||
try {
|
||||
InputStream in = ctx.getAssets().open(name);
|
||||
Scanner scanner = new Scanner(in, "UTF-8").useDelimiter(";");
|
||||
List<String> result = new LinkedList<>();
|
||||
while (scanner.hasNext()) {
|
||||
String statement = scanner.next().trim();
|
||||
if (!"".equals(statement)) {
|
||||
result.add(statement);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public static int getStatusDrawable(Plaintext.Status status) {
|
||||
switch (status) {
|
||||
case RECEIVED:
|
||||
return 0;
|
||||
case DRAFT:
|
||||
return R.drawable.draft;
|
||||
case PUBKEY_REQUESTED:
|
||||
return R.drawable.public_key;
|
||||
case DOING_PROOF_OF_WORK:
|
||||
return R.drawable.ic_notification_proof_of_work;
|
||||
case SENT:
|
||||
return R.drawable.sent;
|
||||
case SENT_ACKNOWLEDGED:
|
||||
return R.drawable.sent_acknowledged;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public static int getStatusString(Plaintext.Status status) {
|
||||
switch (status) {
|
||||
case RECEIVED:
|
||||
return R.string.status_received;
|
||||
case DRAFT:
|
||||
return R.string.status_draft;
|
||||
case PUBKEY_REQUESTED:
|
||||
return R.string.status_public_key;
|
||||
case DOING_PROOF_OF_WORK:
|
||||
return R.string.proof_of_work_title;
|
||||
case SENT:
|
||||
return R.string.status_sent;
|
||||
case SENT_ACKNOWLEDGED:
|
||||
return R.string.status_sent_acknowledged;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public class Constants {
|
||||
public static final String PREFERENCE_WIFI_ONLY = "wifi_only";
|
||||
public static final String PREFERENCE_TRUSTED_NODE = "trusted_node";
|
||||
public static final String PREFERENCE_SYNC_TIMEOUT = "sync_timeout";
|
||||
public static final String PREFERENCE_SERVER_POW = "server_pow";
|
||||
|
||||
public static final String BITMESSAGE_URL_SCHEMA = "bitmessage:";
|
||||
public static final Pattern BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b");
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.util.Base64;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
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.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import ch.dissem.apps.abit.Identicon;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.exception.ApplicationException;
|
||||
|
||||
import static android.graphics.Color.BLACK;
|
||||
import static android.graphics.Color.WHITE;
|
||||
import static android.util.Base64.NO_WRAP;
|
||||
import static android.util.Base64.URL_SAFE;
|
||||
|
||||
/**
|
||||
* Some helper methods to work with drawables.
|
||||
*/
|
||||
public class Drawables {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Drawables.class);
|
||||
|
||||
private static final int QR_CODE_SIZE = 350;
|
||||
|
||||
public static MenuItem addIcon(Context ctx, Menu menu, int menuItem, IIcon icon) {
|
||||
MenuItem item = menu.findItem(menuItem);
|
||||
item.setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar());
|
||||
return item;
|
||||
}
|
||||
|
||||
public static Bitmap toBitmap(Identicon identicon, int size) {
|
||||
return toBitmap(identicon, size, size);
|
||||
}
|
||||
|
||||
public static Bitmap toBitmap(Identicon identicon, int width, int height) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
identicon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
identicon.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static Bitmap qrCode(BitmessageAddress address) {
|
||||
StringBuilder link = new StringBuilder("bitmessage:");
|
||||
link.append(address.getAddress());
|
||||
if (address.getAlias() != null) {
|
||||
link.append("?label=").append(address.getAlias());
|
||||
}
|
||||
if (address.getPubkey() != null) {
|
||||
link.append(address.getAlias() == null ? '?' : '&');
|
||||
ByteArrayOutputStream pubkey = new ByteArrayOutputStream();
|
||||
try {
|
||||
address.getPubkey().writeUnencrypted(pubkey);
|
||||
} catch (IOException e) {
|
||||
throw new ApplicationException(e);
|
||||
}
|
||||
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE | NO_WRAP));
|
||||
}
|
||||
BitMatrix result;
|
||||
try {
|
||||
result = new MultiFormatWriter().encode(link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null);
|
||||
} catch (WriterException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
int w = result.getWidth();
|
||||
int h = result.getHeight();
|
||||
int[] pixels = new int[w * h];
|
||||
for (int y = 0; y < h; y++) {
|
||||
int offset = y * w;
|
||||
for (int x = 0; x < w; x++) {
|
||||
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
|
||||
}
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h);
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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.
|
||||
*/
|
||||
public class Labels {
|
||||
public static String getText(Label label, Context ctx) {
|
||||
return getText(label.getType(), label.toString(), ctx);
|
||||
}
|
||||
|
||||
public static String getText(Label.Type type, String alternative, Context ctx) {
|
||||
if (type == null) {
|
||||
return alternative;
|
||||
} else {
|
||||
switch (type) {
|
||||
case INBOX:
|
||||
return ctx.getString(R.string.inbox);
|
||||
case DRAFT:
|
||||
return ctx.getString(R.string.draft);
|
||||
case OUTBOX:
|
||||
return ctx.getString(R.string.outbox);
|
||||
case SENT:
|
||||
return ctx.getString(R.string.sent);
|
||||
case UNREAD:
|
||||
return ctx.getString(R.string.unread);
|
||||
case TRASH:
|
||||
return ctx.getString(R.string.trash);
|
||||
case BROADCAST:
|
||||
return ctx.getString(R.string.broadcasts);
|
||||
default:
|
||||
return alternative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IIcon getIcon(Label label) {
|
||||
if (label.getType() == null) {
|
||||
return CommunityMaterial.Icon.cmd_label;
|
||||
}
|
||||
switch (label.getType()) {
|
||||
case INBOX:
|
||||
return GoogleMaterial.Icon.gmd_inbox;
|
||||
case DRAFT:
|
||||
return CommunityMaterial.Icon.cmd_file;
|
||||
case OUTBOX:
|
||||
return CommunityMaterial.Icon.cmd_inbox_arrow_up;
|
||||
case SENT:
|
||||
return CommunityMaterial.Icon.cmd_send;
|
||||
case BROADCAST:
|
||||
return CommunityMaterial.Icon.cmd_rss;
|
||||
case UNREAD:
|
||||
return GoogleMaterial.Icon.gmd_markunread_mailbox;
|
||||
case TRASH:
|
||||
return GoogleMaterial.Icon.gmd_delete;
|
||||
default:
|
||||
return CommunityMaterial.Icon.cmd_label;
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getColor(Label label) {
|
||||
if (label.getType() == null) {
|
||||
return label.getColor();
|
||||
}
|
||||
return 0xFF000000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
* <p/>
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*
|
||||
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
|
||||
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final int VERSION_CODE_JELLY_BEAN = 16;
|
||||
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/**
|
||||
* Hidden constructor to prevent instantiation.
|
||||
*/
|
||||
private PRNGFixes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, (Object) generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
public LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
@SuppressWarnings("JavaDoc")
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// On a small fraction of devices /dev/urandom is not writable.
|
||||
// Log and ignore.
|
||||
Log.w(PRNGFixes.class.getSimpleName(),
|
||||
"Failed to mix seed into " + URANDOM_FILE);
|
||||
} finally {
|
||||
mSeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
//noinspection SynchronizationOnLocalVariableOrMethodParameter
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() throws IOException {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.apps.abit.listener.WifiReceiver;
|
||||
import ch.dissem.apps.abit.notification.ErrorNotification;
|
||||
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT;
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE;
|
||||
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY;
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
public class Preferences {
|
||||
public static boolean useTrustedNode(Context ctx) {
|
||||
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
|
||||
return trustedNode != null && !trustedNode.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning, this method might do a network call and therefore can't be called from
|
||||
* the UI thread.
|
||||
*/
|
||||
public static InetAddress getTrustedNode(Context ctx) throws IOException {
|
||||
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
|
||||
if (trustedNode == null) return null;
|
||||
trustedNode = trustedNode.trim();
|
||||
if (trustedNode.isEmpty()) return null;
|
||||
|
||||
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) {
|
||||
int index = trustedNode.lastIndexOf(':');
|
||||
trustedNode = trustedNode.substring(0, index);
|
||||
}
|
||||
return InetAddress.getByName(trustedNode);
|
||||
}
|
||||
|
||||
public static int getTrustedNodePort(Context ctx) {
|
||||
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
|
||||
if (trustedNode == null) return 8444;
|
||||
trustedNode = trustedNode.trim();
|
||||
|
||||
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) {
|
||||
int index = trustedNode.lastIndexOf(':');
|
||||
String portString = trustedNode.substring(index + 1);
|
||||
try {
|
||||
return Integer.parseInt(portString);
|
||||
} catch (NumberFormatException e) {
|
||||
new ErrorNotification(ctx)
|
||||
.setError(R.string.error_invalid_sync_port, portString)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
return 8444;
|
||||
}
|
||||
|
||||
public static long getTimeoutInSeconds(Context ctx) {
|
||||
String preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT);
|
||||
return preference == null ? 120 : Long.parseLong(preference);
|
||||
}
|
||||
|
||||
private static String getPreference(Context ctx, String name) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
|
||||
return preferences.getString(name, null);
|
||||
}
|
||||
|
||||
public static boolean isConnectionAllowed(Context ctx) {
|
||||
return !isWifiOnly(ctx) || !WifiReceiver.isConnectedToMeteredNetwork(ctx);
|
||||
}
|
||||
|
||||
public static boolean isWifiOnly(Context ctx) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public class Strings {
|
||||
private final static Pattern WHITESPACES = Pattern.compile("\\s+");
|
||||
|
||||
public static String normalizeWhitespaces(CharSequence string) {
|
||||
string = string.subSequence(0, Math.min(string.length(), 200));
|
||||
return WHITESPACES.matcher(string).replaceAll(" ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package ch.dissem.apps.abit.util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* SQLite has no UUID data type, and UUIDs are therefore best saved as BINARY[16]. This class
|
||||
* takes care of conversion between byte[16] and UUID.
|
||||
* <p>
|
||||
* Thanks to Brice Roncace on
|
||||
* <a href="http://stackoverflow.com/questions/17893609/convert-uuid-to-byte-that-works-when-using-uuid-nameuuidfrombytesb">
|
||||
* Stack Overflow
|
||||
* </a>
|
||||
* for providing the UUID <-> byte[] conversions.
|
||||
* </p>
|
||||
*/
|
||||
public class UuidUtils {
|
||||
public static UUID asUuid(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes);
|
||||
long firstLong = bb.getLong();
|
||||
long secondLong = bb.getLong();
|
||||
return new UUID(firstLong, secondLong);
|
||||
}
|
||||
|
||||
public static byte[] asBytes(UUID uuid) {
|
||||
if (uuid == null) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||
bb.putLong(uuid.getMostSignificantBits());
|
||||
bb.putLong(uuid.getLeastSignificantBits());
|
||||
return bb.array();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_trash"/>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_trash"
|
||||
android:gravity="right|center_vertical"
|
||||
android:right="16dp"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 Haruki Hasegawa
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/bg_swipe_item_archive"/>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@drawable/ic_item_swipe_archive"
|
||||
android:gravity="left|center_vertical"
|
||||
android:left="16dp"/>
|
||||
</layer-list>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user