Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
177
.gitignore
vendored
177
.gitignore
vendored
@ -1,148 +1,4 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Android ###
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
/*/build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
### Android Patch ###
|
||||
gen-external-apklibs
|
||||
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
|
||||
### Eclipse ###
|
||||
*.pydevproject
|
||||
.metadata
|
||||
.gradle
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
|
||||
### OSX ###
|
||||
# Mac Specific
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
@ -150,16 +6,13 @@ local.properties
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
@ -168,24 +21,8 @@ Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Jekyll Specific
|
||||
_site/
|
||||
|
||||
### Windows ###
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Ruby
|
||||
Gemfile.lock
|
||||
|
48
404.html
Normal file
48
404.html
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
layout: default
|
||||
title: 404
|
||||
permalink: /404.html
|
||||
---
|
||||
|
||||
<div class="container-fluid index error">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12 content-panel articles">
|
||||
<h1 class="header author-header">404</h1>
|
||||
|
||||
<div class="error-text">
|
||||
Sorry but this page doesn't seem to exist.
|
||||
</div>
|
||||
|
||||
<div class="error-text">
|
||||
<a href="{{ site.baseurl }}/">Home</a> |
|
||||
<a href="{{ site.baseurl }}/posts/">All Posts</a> |
|
||||
<a href="{{ site.baseurl }}/search/">Search</a>
|
||||
</div>
|
||||
|
||||
{% include social_links.html %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content-panel related">
|
||||
{% for post in site.posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Suggested Article</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
28
LICENSE
Normal file
28
LICENSE
Normal file
@ -0,0 +1,28 @@
|
||||
Copyright (C) 2014 Jacob Tomlinson
|
||||
|
||||
The contents of this website, which consists of the files in
|
||||
* _config.yml
|
||||
* _data
|
||||
* _drafts
|
||||
* _posts
|
||||
|
||||
are copyrighted and sole property of its author Jacob Tomlinson. It may not be
|
||||
used, modified, syndicated or distributed without expressed permission from
|
||||
the author.
|
||||
|
||||
However the website theme built using jekyll is Open Source under the following
|
||||
GPLv3 license.
|
||||
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
62
README.md
62
README.md
@ -1,14 +1,56 @@
|
||||
# Abit
|
||||
# Carte Noire
|
||||
|
||||
A Bitmessage client for Android.
|
||||
A simple Jekyll theme for blogging. Not named after the coffee.
|
||||
|
||||
**Please be aware that due to the protocol, sending messages might suck your
|
||||
battery dry. Also, it causes a lot of traffic and may use up your data plan
|
||||
rather quickly. Use at your own peril.**
|
||||
![Homepage](http://i.imgur.com/xlmHArV.png)
|
||||
|
||||
Abit uses the [Jabit Bitmessage library](https://github.com/Dissem/Jabit).
|
||||
### Article
|
||||
![Article](http://i.imgur.com/8rD8FfC.png)
|
||||
|
||||
## Requirements
|
||||
You'll need at least Android 4.4 KitKat. Due to the Proof of Work that comes
|
||||
with the protocol you might want as fast a processor and as many cores as
|
||||
you can get.
|
||||
### Disqus Comments
|
||||
![Comments](http://i.imgur.com/TBZHQwF.png)
|
||||
|
||||
### Posts grouped by year
|
||||
![All Posts](http://i.imgur.com/9bNs2Sc.png)
|
||||
|
||||
### JavaScript Search
|
||||
![Search](http://i.imgur.com/yQqMeSl.png)
|
||||
|
||||
### Menu by mmenu
|
||||
![Menu](http://i.imgur.com/SClrNSH.png)
|
||||
|
||||
## Contact
|
||||
If you wish to contact me regarding this theme please raise an issue on GitHub,
|
||||
tweet me [@_jacobtomlinson](http://www.twitter.com/_jacobtomlinson) or email me
|
||||
[jacob@jacobtomlinson.co.uk](mailto:jacob@jacobtomlinson.co.uk).
|
||||
|
||||
## Contribution
|
||||
Pull requests are very welcome.
|
||||
|
||||
## Theme
|
||||
This jekyll theme has been created from scratch. Ideas and inspiration are taken
|
||||
from other places but the code is my own.
|
||||
|
||||
## Tools and Libraries
|
||||
The following tools and libraries are used in this theme
|
||||
|
||||
### JavaScript
|
||||
* [jQuery](http://jquery.com/)
|
||||
* [MMenu](http://mmenu.frebsite.nl/)
|
||||
* [HighlightJS](https://highlightjs.org/)
|
||||
* [Simple Jekyll Search](https://github.com/christian-fei/Simple-Jekyll-Search)
|
||||
|
||||
### CSS
|
||||
* [Bootstrap](http://getbootstrap.com/)
|
||||
* [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
|
||||
|
||||
### Social
|
||||
* [AddThis](http://www.addthis.com/)
|
||||
* [Disqus](https://disqus.com/)
|
||||
|
||||
### Other
|
||||
* [Real Favicon Generator](http://realfavicongenerator.net/)
|
||||
* [Google Analytics](http://www.google.com/analytics/)
|
||||
|
||||
## License
|
||||
The jekyll theme, HTML, CSS and JavaScript is licensed under GPLv3 (unless stated otherwise in the file).
|
||||
|
40
_config.yml
Normal file
40
_config.yml
Normal file
@ -0,0 +1,40 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely need to edit after that.
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site settings
|
||||
title: Abit
|
||||
email: chrigu.meyer@gmail.com
|
||||
description: > # this means to ignore newlines until "baseurl:"
|
||||
Write an awesome description for your new site here. You can edit this
|
||||
line in _config.yml. It will appear in your document head meta (for
|
||||
Google search results) and in your feed.xml site description.
|
||||
baseurl: "/Abit" # the subpath of your site, e.g. /blog
|
||||
url: "https://dissem.github.io" # the base hostname & protocol for your site
|
||||
title_description: "A Bitmessage client for Android™"
|
||||
title_image: "ic_launcher-web.png"
|
||||
#google_analytics: "UA-20365477-4"
|
||||
#disqus_account: cartenoire
|
||||
github_repository: https://github.com/Dissem/Abit
|
||||
|
||||
|
||||
# Social usernames/URLs
|
||||
#twitter_username: jekyllrb
|
||||
github_username: Dissem
|
||||
|
||||
# Footer
|
||||
footer_left: "Made with <i class=\"fa fa-heart\"></i> by <a href=\"https://twitter.com/Dissem\">Christian Basler</a>"
|
||||
footer_right: "</> on <a href=\"https://github.com/Dissem/Abit\">Github</a> <i class=\"fa fa-github-alt\"></i>"
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
|
||||
kramdown:
|
||||
input: GFM
|
||||
syntax_highlighter: rouge
|
||||
|
||||
permalink: pretty
|
||||
exclude: [vendor]
|
2
_data/thumbnail.yml
Normal file
2
_data/thumbnail.yml
Normal file
@ -0,0 +1,2 @@
|
||||
gravatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?s=500&d=mm"
|
||||
jekyll: "https://i.imgur.com/aRQcGSi.png"
|
86
_includes/footer.html
Normal file
86
_includes/footer.html
Normal file
@ -0,0 +1,86 @@
|
||||
<div class="footer clearfix">
|
||||
<div class="col-md-6">
|
||||
{{ site.footer_left }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ site.footer_right }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
|
||||
<script src="{{ site.baseurl }}/js/jquery.mmenu.min.all.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$("#my-menu").mmenu().on( "closed.mm", function() {
|
||||
$(".menu-button").show();
|
||||
});
|
||||
$(".menu-button").click(function() {
|
||||
$(".menu-button").hide();
|
||||
$("#my-menu").trigger("open.mm");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if page.make-smaller-titles %}
|
||||
<script type="text/javascript">
|
||||
|
||||
function setFontSize() {
|
||||
|
||||
var title, dateOfTitle, fontSizeOfTitle, listOfA, listOfSmall, listOfArticlesDiv, divWidth;
|
||||
|
||||
listOfArticlesDiv = document.getElementsByClassName("articles");
|
||||
|
||||
for (i = 0; i < listOfArticlesDiv.length; i++) {
|
||||
|
||||
listOfA = document.getElementsByClassName("articles")[i].getElementsByTagName("a");
|
||||
listOfSmall = document.getElementsByClassName("articles")[i].getElementsByTagName("small");
|
||||
|
||||
divWidth = document.getElementsByClassName("articles")[i].offsetWidth;
|
||||
|
||||
for (k = 0; k < listOfSmall.length; k++) {
|
||||
|
||||
title = $(listOfA[k]);
|
||||
dateOfTitle = $(listOfSmall[k]);
|
||||
|
||||
fontSizeOfTitle = startingFontSize;
|
||||
title.css("font-size", fontSizeOfTitle);
|
||||
|
||||
while (title.width() + dateOfTitle.width() >= divWidth)
|
||||
title.css("font-size", fontSizeOfTitle -= 0.5);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStartFontSize() {
|
||||
try {
|
||||
startingFontSize = parseInt($(document.getElementsByClassName("articles")[0].getElementsByTagName("a")[0]).css("font-size"));
|
||||
setFontSize();
|
||||
window.addEventListener('resize', setFontSize, true);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
$(document).ready(getStartFontSize);
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if site.google_analytics %}
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '{{ site.google_analytics }}']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
(function () {
|
||||
var ga = document.createElement('script');
|
||||
ga.type = 'text/javascript';
|
||||
ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
45
_includes/head.html
Normal file
45
_includes/head.html
Normal file
@ -0,0 +1,45 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
|
||||
<meta name="description" content="{{ site.description }}">
|
||||
|
||||
<link rel="profile" href="https://gmpg.org/xfn/11" />
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/style.css" />
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{{ site.baseurl }}/css/jquery.mmenu.all.css" />
|
||||
<link rel="stylesheet" href="{{ site.baseurl }}/css/idea.css">
|
||||
|
||||
<!-- Favicons generated at http://realfavicongenerator.net/ -->
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ site.baseurl }}/favicons/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ site.baseurl }}/favicons/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ site.baseurl }}/favicons/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ site.baseurl }}/favicons/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ site.baseurl }}/favicons/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ site.baseurl }}/favicons/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ site.baseurl }}/favicons/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ site.baseurl }}/favicons/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ site.baseurl }}/favicons/apple-touch-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/android-chrome-192x192.png" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-96x96.png" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="{{ site.baseurl }}/favicons/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="manifest" href="{{ site.baseurl }}/favicons/manifest.json">
|
||||
<link rel="shortcut icon" href="{{ site.baseurl }}/favicons/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-TileImage" content="{{ site.baseurl }}/favicons/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="{{ site.baseurl }}/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid={{ site.addthis_id }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if page.content contains '<script type="math/tex' %}
|
||||
<!-- MathJax for LaTeX -->
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{% endif %}
|
||||
|
||||
</head>
|
14
_includes/header.html
Normal file
14
_includes/header.html
Normal file
@ -0,0 +1,14 @@
|
||||
<nav id="my-menu">
|
||||
<div>
|
||||
<p>{{ site.title }}</p>
|
||||
|
||||
<ul class="pages">
|
||||
<li><a href="{{ site.baseurl }}/"><i class="fa fa-home"></i> Home</a></li>
|
||||
<li><a href="{{ site.baseurl }}/posts/"><i class="fa fa-archive"></i> All Posts</a></li>
|
||||
<li><a href="{{ site.baseurl }}/search/"><i class="fa fa-search"></i> Search</a></li>
|
||||
</ul>
|
||||
|
||||
{% include social_links.html %}
|
||||
</div>
|
||||
</nav>
|
||||
<div class="menu-button" href="#menu"><i class="fa fa-bars"></i></div>
|
8
_includes/social_links.html
Normal file
8
_includes/social_links.html
Normal file
@ -0,0 +1,8 @@
|
||||
<p class="links">
|
||||
{% if site.twitter_username %}<a href="https://www.twitter.com/{{ site.twitter_username }}" target="_new"><i class="fa fa-twitter"></i></a>{% endif %}
|
||||
{% if site.linkedin_link %}<a href="{{ site.linkedin_link }}" target="_new"><i class="fa fa-linkedin"></i></a>{% endif %}
|
||||
{% if site.google_plus_link %}<a href="{{ site.google_plus_link }}" target="_new"><i class="fa fa-google-plus"></i></a>{% endif %}
|
||||
{% if site.github_username %}<a href="https://github.com/{{ site.github_username }}" target="_new"><i class="fa fa-github-alt"></i></a>{% endif %}
|
||||
{% if site.stackoverflow_link %}<a href="{{ site.stackoverflow_link }}" target="_new"><i class="fa fa-stack-overflow"></i></a>{% endif %}
|
||||
<a href="{{ site.baseurl }}/feed.xml" target="_new"><i class="fa fa-rss"></i></a>
|
||||
</p>
|
19
_layouts/default.html
Normal file
19
_layouts/default.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
{% include head.html %}
|
||||
|
||||
<body>
|
||||
|
||||
{% include header.html %}
|
||||
|
||||
<div class="page-content">
|
||||
<div class="wrap">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include footer.html %}
|
||||
|
||||
</body>
|
||||
</html>
|
1
_layouts/none.html
Normal file
1
_layouts/none.html
Normal file
@ -0,0 +1 @@
|
||||
{{ content }}
|
153
_layouts/page.html
Normal file
153
_layouts/page.html
Normal file
@ -0,0 +1,153 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<div class="container-fluid single">
|
||||
<div class="row">
|
||||
|
||||
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
|
||||
{% if site.data.thumbnail[page.thumbnail] %}
|
||||
<div class="thumb">
|
||||
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
|
||||
</div>
|
||||
{% elsif page.thumbnail %}
|
||||
<div class="thumb">
|
||||
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="header" itemprop="name">{{ page.title }}</h1>
|
||||
|
||||
<div class="content-panel content">
|
||||
|
||||
{% if page.series %}
|
||||
This post is part of the series '{{ page.series }}':
|
||||
<ol class="series">
|
||||
{% for apost in site.posts reversed %}
|
||||
{% if page.series == apost.series %}
|
||||
<li>
|
||||
{% if page.title == apost.title %}
|
||||
{% assign nextpost = true %}
|
||||
{{ apost.title }}
|
||||
{% else %}
|
||||
{% if nextpost == true %}
|
||||
{% assign seriesnext = apost %}
|
||||
{% endif %}
|
||||
{% assign nextpost = false %}
|
||||
<a href="{{ apost.url }}">{{ apost.title }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
<span itemprop="articleBody">{{ content }}</span>
|
||||
|
||||
{% if page.series %}
|
||||
{% if seriesnext %}
|
||||
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<div class="share">
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<div class="addthis_sharing_toolbox"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page.tags and page.tags.size > 0 %}
|
||||
<div class="tags">
|
||||
<small>
|
||||
<i class="fa fa-tags"></i>
|
||||
{{ page.tags | join: ', ' }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.twitter_username and site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
|
||||
</div>
|
||||
{% elsif site.twitter_username %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
|
||||
</div>
|
||||
{% elsif site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article in the comment section below!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<div class="content-panel comments">
|
||||
<div id="disqus_thread">
|
||||
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
|
||||
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.related_posts.length > 0 %}
|
||||
<div class="content-panel related clearfix">
|
||||
{% for post in site.related_posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<hr />
|
||||
<div class="previous previous-next">
|
||||
{% if page.previous %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="next previous-next">
|
||||
{% if page.next %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<script type="text/javascript">
|
||||
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
|
||||
function disqus_config() { this.experiment.enable_scroll_container = true; }
|
||||
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
|
||||
/* * * DON'T EDIT BELOW THIS LINE * * */
|
||||
(function() {
|
||||
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
|
||||
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
|
||||
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
187
_layouts/post.html
Normal file
187
_layouts/post.html
Normal file
@ -0,0 +1,187 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
{% if page.minutes %}
|
||||
{% assign minutes = page.minutes %}
|
||||
{% else %}
|
||||
{% assign minutes = content | number_of_words | divided_by: 180 %}
|
||||
{% if minutes == 0 %}{% assign minutes = 1 %}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid single">
|
||||
<div class="row">
|
||||
|
||||
<div itemscope itemtype="http://schema.org/Article" class="col-md-12 article">
|
||||
{% if site.data.thumbnail[page.thumbnail] %}
|
||||
<div class="thumb">
|
||||
<img itemprop="image" src="{{ site.data.thumbnail[page.thumbnail] }}" alt="Thumbnail: {{ page.thumbnail }}" />
|
||||
</div>
|
||||
{% elsif page.thumbnail %}
|
||||
<div class="thumb">
|
||||
<i class="fa fa-{{ page.thumbnail }} fa-4x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="header" itemprop="name">{{ page.title }}</h1>
|
||||
|
||||
<div class="author">
|
||||
<small><i>
|
||||
{% if page.author %}
|
||||
by
|
||||
<span itemprop="author">
|
||||
{% if site.google_plus_link %}
|
||||
<a rel="author" href="{{ site.google_plus_link }}">
|
||||
{% endif %}
|
||||
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
|
||||
<span itemprop="name">{{ page.author }}</span>
|
||||
</span>
|
||||
{% if site.google_plus_link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
on <span itemprop="datePublished" content="2014-08-28">{{ page.date | date: "%B %-d, %Y" }}</span>
|
||||
{% if page.categories != empty %} under {% for category in page.categories limit:1 %}{{ category }}{% endfor %}{% endif %}
|
||||
</i></small>
|
||||
</div>
|
||||
|
||||
<div class="read-time">
|
||||
<small>
|
||||
{{ minutes }} minute read
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="content-panel content">
|
||||
|
||||
{% if page.series %}
|
||||
This post is part of the series '{{ page.series }}':
|
||||
<ol class="series">
|
||||
{% for apost in site.posts reversed %}
|
||||
{% if page.series == apost.series %}
|
||||
<li>
|
||||
{% if page.title == apost.title %}
|
||||
{% assign nextpost = true %}
|
||||
{{ apost.title }}
|
||||
{% else %}
|
||||
{% if nextpost == true %}
|
||||
{% assign seriesnext = apost %}
|
||||
{% endif %}
|
||||
{% assign nextpost = false %}
|
||||
<a href="{{ apost.url }}">{{ apost.title }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
<span itemprop="articleBody">{{ content }}</span>
|
||||
|
||||
{% if page.series %}
|
||||
{% if seriesnext %}
|
||||
<i>Next post in the series:</i> <a href="{{ seriesnext.url }}">{{ seriesnext.title }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if site.addthis_id %}
|
||||
<div class="share">
|
||||
<!-- Go to www.addthis.com/dashboard to customize your tools -->
|
||||
<div class="addthis_sharing_toolbox"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page.tags and page.tags.size > 0 %}
|
||||
<div class="tags">
|
||||
<small>
|
||||
<i class="fa fa-tags"></i>
|
||||
{{ page.tags | join: ', ' }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.twitter_username and site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a> or leave a comment below!
|
||||
</div>
|
||||
{% elsif site.twitter_username %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article on twitter <a href="http://www.twitter.com/{{ site.twitter_username }}">@{{ site.twitter_username }}</a>!
|
||||
</div>
|
||||
{% elsif site.disqus_account %}
|
||||
<div class="content-panel feedback">
|
||||
I <i class="fa fa-heart"></i> feedback.<br />
|
||||
Let me know what you think of this article in the comment section below!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<div class="content-panel comments">
|
||||
<div id="disqus_thread">
|
||||
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
|
||||
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if site.related_posts.length > 0 %}
|
||||
<div class="content-panel related clearfix">
|
||||
{% for post in site.related_posts limit:1 %}
|
||||
<div class="related-header">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Read More</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
|
||||
</div>
|
||||
<div class="excerpt">
|
||||
{% if post.summary %}
|
||||
{{ post.summary | strip_html | truncatewords:30 }}
|
||||
{% else %}
|
||||
{{ post.excerpt | strip_html | truncatewords:30 }}
|
||||
{% endif %}
|
||||
<a href="{{ site.baseurl }}{{ post.url }}">Continue Reading</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<hr />
|
||||
<div class="previous previous-next">
|
||||
{% if page.previous %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.previous.url }}">{{ page.previous.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.previous.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="next previous-next">
|
||||
{% if page.next %}
|
||||
<p>
|
||||
<a href="{{ site.baseurl }}{{ page.next.url }}">{{ page.next.title }}</a>
|
||||
</p>
|
||||
<p class="date">Published {{ page.next.date | date: "%B %-d, %Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if site.disqus_account %}
|
||||
<script type="text/javascript">
|
||||
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
|
||||
function disqus_config() { this.experiment.enable_scroll_container = true; }
|
||||
var disqus_shortname = "{{ site.disqus_account }}"; // required: replace example with your forum shortname
|
||||
/* * * DON'T EDIT BELOW THIS LINE * * */
|
||||
(function() {
|
||||
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
|
||||
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
|
||||
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
59
_posts/2014-06-08-using-thumbnails.md
Normal file
59
_posts/2014-06-08-using-thumbnails.md
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: post
|
||||
title: Using thumbnails
|
||||
date: 2014-06-08 12:32:18
|
||||
summary: Using thumbnails in your Carte Noire articles.
|
||||
categories: jekyll
|
||||
thumbnail: jekyll
|
||||
tags:
|
||||
- thumbnails
|
||||
- carte noire
|
||||
---
|
||||
|
||||
Carte Noire is designed to start each article with an all-white image as a
|
||||
thumbnail. These are created by adding a `thumbnail` parameter to the article's
|
||||
[YAML frontmatter][1]. This thumbnail parameter is processed in one of two ways,
|
||||
images specified in `_data/thumbnails.yml` or using [Font Awesome][2].
|
||||
|
||||
## Images
|
||||
|
||||
To use your own custom images as a thumbnail you must upload them to a web available
|
||||
location (I use [Imgur][3]) and then you need to add the url to `_data/thumbnail.yml`
|
||||
with an associated keyword.
|
||||
|
||||
```
|
||||
jekyll: "http://i.imgur.com/aRQcGSi.png"
|
||||
```
|
||||
|
||||
You then add a `thumbnail` option to the article's frontmatter and provide the keyword
|
||||
for that thumbnail.
|
||||
|
||||
```
|
||||
thumbnail: jekyll
|
||||
```
|
||||
|
||||
This allows you to re-use thumbnails across multiple articles without having to
|
||||
specify the url each time.
|
||||
|
||||
## Font Awesome
|
||||
|
||||
If jekyll can't find a corresponding image in your `thumbnail.yml` file then it
|
||||
will assume you want to use a Font Awesome icon instead. You can find the full
|
||||
list of Font Awesome icons [here][4].
|
||||
|
||||
So for example if your article is about android and you want to use the [android icon][5]
|
||||
from font awesome you can just specify the following in your frontmatter.
|
||||
|
||||
```
|
||||
thumbnail: android
|
||||
```
|
||||
|
||||
Then in the future if you decide you want to use your own android icon you can just
|
||||
add it to `_data/thumbnails.yml` which will override it for all articles using
|
||||
the android thumbnail.
|
||||
|
||||
[1]: http://jekyllrb.com/docs/frontmatter/
|
||||
[2]: http://fortawesome.github.io/Font-Awesome/
|
||||
[3]: http://imgur.com/
|
||||
[4]: http://fortawesome.github.io/Font-Awesome/icons/
|
||||
[5]: http://fortawesome.github.io/Font-Awesome/icon/android/
|
21
_posts/2014-06-09-so-what-is-jekyll.md
Normal file
21
_posts/2014-06-09-so-what-is-jekyll.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: post
|
||||
title: So, What is Jekyll?
|
||||
date: 2014-06-09 12:32:18
|
||||
summary: Transform your plain text into static websites and blogs. Simple, static, and blog-aware.
|
||||
categories: jekyll
|
||||
thumbnail: jekyll
|
||||
tags:
|
||||
- about
|
||||
- jekyll
|
||||
---
|
||||
|
||||
Jekyll is a tool for transforming your plain text into static websites and
|
||||
blogs. It is simple, static, and blog-aware. Jekyll uses the
|
||||
[Liquid](http://docs.shopify.com/themes/liquid-basics) templating
|
||||
language and has builtin [Markdown](http://daringfireball.net/projects/markdown/)
|
||||
and [Textile](http://en.wikipedia.org/wiki/Textile_(markup_language)) support.
|
||||
|
||||
It also ties in nicely to [Github Pages](https://pages.github.com/).
|
||||
|
||||
Learn more about Jekyll on their [website](http://jekyllrb.com/).
|
105
_posts/2014-06-10-carte-noire-in-action.md
Normal file
105
_posts/2014-06-10-carte-noire-in-action.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
layout: post
|
||||
title: Carte Noire in Action
|
||||
date: 2014-06-10 12:31:19
|
||||
summary: See what the different elements looks like.
|
||||
categories: jekyll
|
||||
thumbnail: cogs
|
||||
tags:
|
||||
- demo
|
||||
- action
|
||||
- carte
|
||||
- noire
|
||||
---
|
||||
|
||||
**Note** - This article is a derivative of ["See pixyll in action"][1], taken from the lovely jekyll theme [pixyll][4].
|
||||
|
||||
All links are easy to [locate and discern](#), yet don't detract from the harmony
|
||||
of a paragraph. The _same_ goes for italics and __bold__ elements. Even the the strikeout
|
||||
works if <del>for some reason you need to update your post</del>. For consistency's sake,
|
||||
<ins>The same goes for insertions</ins>, of course.
|
||||
|
||||
### Code, with syntax highlighting
|
||||
|
||||
Code blocks use the [peppermint][2] theme.
|
||||
|
||||
{% highlight ruby %}
|
||||
class Awesome < ActiveRecord::Base
|
||||
include EvenMoreAwesome
|
||||
|
||||
validates_presence_of :something
|
||||
validates :email, email_format: true
|
||||
|
||||
def initialize(email, name = nil)
|
||||
self.email = email
|
||||
self.name = name
|
||||
end
|
||||
end
|
||||
{% endhighlight %}
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<title>Title</title>
|
||||
|
||||
<style>body {width: 500px;}</style>
|
||||
|
||||
<script type="application/javascript">
|
||||
function $init() {return true;}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<p checked class="title" id='title'>Title</p>
|
||||
<!-- here goes the rest of the page -->
|
||||
</body>
|
||||
```
|
||||
|
||||
# Headings!
|
||||
|
||||
They're responsive, and well-proportioned (in `padding`, `line-height`, `margin`, and `font-size`).
|
||||
|
||||
##### They draw the perfect amount of attention
|
||||
|
||||
This allows your content to have the proper informational and contextual hierarchy. Yay.
|
||||
|
||||
### There are lists, too
|
||||
|
||||
* Apples
|
||||
* Oranges
|
||||
* Potatoes
|
||||
* Milk
|
||||
|
||||
1. Mow the lawn
|
||||
2. Feed the dog
|
||||
3. Dance
|
||||
|
||||
### Images look great, too
|
||||
|
||||
![Thumper](https://i.imgur.com/DMCHDqF.jpg)
|
||||
|
||||
|
||||
### Stylish blockquotes included
|
||||
|
||||
You can use the markdown quote syntax, `>` for simple quotes.
|
||||
|
||||
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse quis porta mauris.
|
||||
|
||||
### LaTeX support
|
||||
|
||||
The default math delimiters are \$\$. Hence `$$ E = m \cdot c^2 $$` yields $$ E = m \cdot c^2 $$
|
||||
|
||||
And here's something more fancy:
|
||||
|
||||
$$ \zeta(s) = \frac{1}{\Gamma(s)} \int \limits_0^\infty x^{s-1} \sum_{n=1}^\infty e^{-nx} \mathrm{d}x = \frac{1}{\Gamma(s)} \int \limits_0^\infty \frac{x^{s-1}}{e^x - 1} \mathrm{d}x $$
|
||||
|
||||
|
||||
### There's more being added all the time
|
||||
|
||||
Checkout the [Github repository][3] to request,
|
||||
or add, features.
|
||||
|
||||
Happy writing.
|
||||
|
||||
[1]: http://pixyll.com/jekyll/pixyll/2014/06/10/see-pixyll-in-action/
|
||||
[2]: https://noahfrederick.com/log/lion-terminal-theme-peppermint/
|
||||
[3]: https://github.com/jacobtomlinson/carte-noire
|
||||
[4]: http://pixyll.com/
|
28
_posts/2015-03-23-welcome-to-carte-noir.md
Normal file
28
_posts/2015-03-23-welcome-to-carte-noir.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
layout: post
|
||||
title: Welcome to Carte Noire
|
||||
date: 2015-03-23 15:31:19
|
||||
author: Jacob Tomlinson
|
||||
summary: Carte Noire is a dark blog theme for Jekyll focusing on a clear reading experience.
|
||||
categories: jekyll
|
||||
thumbnail: heart
|
||||
tags:
|
||||
- welcome
|
||||
- to
|
||||
- carte
|
||||
- noire
|
||||
---
|
||||
|
||||
Welcome to Carte Noire.
|
||||
|
||||
Carte Noire began as a new theme for [my personal blog][1], but has now taken
|
||||
on a life of its own as a free theme for Jekyll.
|
||||
|
||||
The theme has been designed with simplicity and readability in mind. It makes
|
||||
use of third party services such as Disqus ad AddThis to ensure the blog has
|
||||
all the features you would expect from a dynamic application such as Wordpress
|
||||
but with the hosting and maintenance simplicity of Jekyll.
|
||||
|
||||
Please use/copy/share Carte Noire!
|
||||
|
||||
[1]: http://www.jacobtomlinson.co.uk/
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/build
|
116
app/build.gradle
116
app/build.gradle
@ -1,116 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'idea'
|
||||
|
||||
ext {
|
||||
appName = "Abit"
|
||||
}
|
||||
if (project.hasProperty("project.configs")
|
||||
&& new File(project.property("project.configs") + appName + ".gradle").exists()) {
|
||||
apply from: project.property("project.configs") + appName + ".gradle"
|
||||
}
|
||||
|
||||
//noinspection GroovyMissingReturnStatement
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "26.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 23
|
||||
versionName "1.0-rc1"
|
||||
multiDexEnabled true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/core.kotlin_module'
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ext.jabitVersion = '2.0.4'
|
||||
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
|
||||
ext.supportVersion = '27.0.2'
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
||||
|
||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
||||
implementation "com.android.support:preference-v7:$supportVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
||||
implementation "com.android.support:support-v13:$supportVersion"
|
||||
implementation "com.android.support:preference-v14:$supportVersion"
|
||||
implementation "com.android.support:design:$supportVersion"
|
||||
implementation "com.android.support:multidex:1.0.2"
|
||||
|
||||
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
|
||||
|
||||
implementation 'org.slf4j:slf4j-android:1.7.25'
|
||||
|
||||
implementation 'com.mikepenz:materialize:1.1.2@aar'
|
||||
implementation('com.mikepenz:materialdrawer:6.0.2@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation "com.mikepenz:iconics-core:3.0.0@aar"
|
||||
implementation "com.mikepenz:iconics-views:3.0.0@aar"
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
||||
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
|
||||
|
||||
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.1'
|
||||
|
||||
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8'
|
||||
implementation 'com.github.amlcurran.showcaseview:library:5.4.3'
|
||||
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation 'com.github.angads25:filepicker:1.1.1'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.13.0'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:1.3'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation 'org.robolectric:robolectric:3.6.1'
|
||||
testImplementation "org.robolectric:shadows-multidex:3.6.1"
|
||||
|
||||
androidTestImplementation "com.android.support:multidex:1.0.2"
|
||||
}
|
||||
|
||||
idea.module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@ -1,17 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/chris/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
@ -1,211 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="ch.dissem.apps.abit">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
|
||||
<application
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MessageDetailActivity"
|
||||
android:label="@string/title_message_detail"
|
||||
android:parentActivityName=".MainActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AddressDetailActivity"
|
||||
android:label="@string/title_subscription_detail"
|
||||
android:parentActivityName=".MainActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".dialog.FullNodeDialogActivity"
|
||||
android:label="@string/full_node"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog" />
|
||||
<activity
|
||||
android:name=".ComposeMessageActivity"
|
||||
android:label="@string/compose_message"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".CreateAddressActivity"
|
||||
android:label="@string/title_activity_open_bitmessage_link"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="bitmessage" />
|
||||
<data android:scheme="bitmsg" />
|
||||
<data android:scheme="bm" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ImportIdentityActivity"
|
||||
android:label="@string/title_import_identity"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data
|
||||
android:host="*"
|
||||
android:mimeType="*/*"
|
||||
android:pathPattern=".*\\.dat"
|
||||
android:scheme="file" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.BitmessageService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".service.ProofOfWorkService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Synchronization -->
|
||||
<provider
|
||||
android:name=".synchronization.StubProvider"
|
||||
android:authorities="ch.dissem.apps.abit.provider"
|
||||
android:exported="false"
|
||||
android:syncable="true" />
|
||||
|
||||
<!-- Exports -->
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="ch.dissem.apps.abit.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".synchronization.AuthenticatorService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".synchronization.SyncService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.BitmessageIntentService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Receive Wi-Fi connection state changes -->
|
||||
<receiver
|
||||
android:name=".listener.WifiReceiver"
|
||||
android:enabled="@bool/is_pre_api_21">
|
||||
<intent-filter>
|
||||
<!-- This is bad for battery life, but needed on older devices to check
|
||||
if WiFi is available. Let's be honest, the whole app is bad for
|
||||
battery life. -->
|
||||
<action
|
||||
android:name="android.net.conn.CONNECTIVITY_CHANGE"
|
||||
tools:ignore="BatteryLife" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".service.StartServiceReceiver"
|
||||
android:enabled="@bool/is_post_api_21">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".service.StartupNodeOnWifiService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,8 +0,0 @@
|
||||
CREATE TABLE Inventory (
|
||||
hash BINARY(32) NOT NULL PRIMARY KEY,
|
||||
stream INTEGER NOT NULL,
|
||||
expires INTEGER NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
version INTEGER NOT NULL
|
||||
);
|
@ -1,8 +0,0 @@
|
||||
CREATE TABLE Address (
|
||||
address VARCHAR(40) NOT NULL PRIMARY KEY,
|
||||
version INTEGER NOT NULL,
|
||||
alias VARCHAR(255),
|
||||
public_key BLOB,
|
||||
private_key BLOB,
|
||||
subscribed BIT DEFAULT '0'
|
||||
);
|
@ -1,42 +0,0 @@
|
||||
CREATE TABLE Message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
iv BINARY(32) UNIQUE,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
sender VARCHAR(40) NOT NULL,
|
||||
recipient VARCHAR(40),
|
||||
data BLOB NOT NULL,
|
||||
sent INTEGER,
|
||||
received INTEGER,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
initial_hash BINARY(64) UNIQUE,
|
||||
|
||||
FOREIGN KEY (sender) REFERENCES Address (address),
|
||||
FOREIGN KEY (recipient) REFERENCES Address (address)
|
||||
);
|
||||
|
||||
CREATE TABLE Label (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(20),
|
||||
color INT NOT NULL DEFAULT 4278190080, -- FF000000
|
||||
ord INTEGER,
|
||||
|
||||
CONSTRAINT UC_label UNIQUE (label),
|
||||
CONSTRAINT UC_order UNIQUE (ord)
|
||||
);
|
||||
|
||||
CREATE TABLE Message_Label (
|
||||
message_id INTEGER NOT NULL,
|
||||
label_id INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (message_id, label_id),
|
||||
FOREIGN KEY (message_id) REFERENCES Message (id),
|
||||
FOREIGN KEY (label_id) REFERENCES Label (id)
|
||||
);
|
||||
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Broadcast', 'BROADCAST', 50);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100);
|
@ -1,7 +0,0 @@
|
||||
-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration
|
||||
-- wasn't really necessary yet.
|
||||
--
|
||||
-- This file is here to reduce confusion regarding to the original migration files.
|
||||
|
||||
--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64);
|
||||
--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash);
|
@ -1,7 +0,0 @@
|
||||
CREATE TABLE POW (
|
||||
initial_hash BINARY(64) PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
version BIGINT NOT NULL,
|
||||
nonce_trials_per_byte BIGINT NOT NULL,
|
||||
extra_bytes BIGINT NOT NULL
|
||||
);
|
@ -1 +0,0 @@
|
||||
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
|
||||
ALTER TABLE POW ADD COLUMN message_id BIGINT;
|
@ -1,4 +0,0 @@
|
||||
ALTER TABLE Message ADD COLUMN ack_data BINARY(32);
|
||||
ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Message ADD COLUMN next_try BIGINT;
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE Node (
|
||||
stream BIGINT NOT NULL,
|
||||
address BINARY(32) NOT NULL,
|
||||
port INT NOT NULL,
|
||||
services BIGINT NOT NULL,
|
||||
time BIGINT NOT NULL,
|
||||
PRIMARY KEY (stream, address, port)
|
||||
);
|
||||
CREATE INDEX idx_time on Node(time);
|
@ -1 +0,0 @@
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);
|
@ -1,11 +0,0 @@
|
||||
ALTER TABLE Message ADD COLUMN conversation BINARY[16];
|
||||
|
||||
CREATE TABLE Message_Parent (
|
||||
parent BINARY(64) NOT NULL,
|
||||
child BINARY(64) NOT NULL,
|
||||
pos INT NOT NULL,
|
||||
conversation BINARY[16] NOT NULL,
|
||||
|
||||
PRIMARY KEY (parent, child),
|
||||
FOREIGN KEY (child) REFERENCES Message (iv)
|
||||
);
|
@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.ListFragment
|
||||
import android.view.View
|
||||
import android.widget.ListView
|
||||
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> {
|
||||
/**
|
||||
* The fragment's current callback object, which is notified of list item
|
||||
* clicks.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T>
|
||||
/**
|
||||
* The current activated item position. Only used on tablets.
|
||||
*/
|
||||
private var activatedPosition = ListView.INVALID_POSITION
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (context is ListSelectionListener<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = context as ListSelectionListener<T>
|
||||
} else {
|
||||
throw IllegalStateException("Activity must implement fragment's callbacks.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
// Reset the active callbacks interface to the dummy implementation.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbacks = DummyCallback as ListSelectionListener<T>
|
||||
}
|
||||
|
||||
override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) {
|
||||
super.onListItemClick(listView, view, position, id)
|
||||
|
||||
// Notify the active callbacks interface (the activity, if the
|
||||
// fragment is attached to one) that an item has been selected.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(listView.getItemAtPosition(position) as? T)?.let {
|
||||
callbacks.onItemSelected(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (activatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
|
||||
if (isVisible) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
listView.choiceMode = if (activateOnItemClick)
|
||||
ListView.CHOICE_MODE_SINGLE
|
||||
else
|
||||
ListView.CHOICE_MODE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActivatedPosition(position: Int) {
|
||||
if (position == ListView.INVALID_POSITION) {
|
||||
listView.setItemChecked(activatedPosition, false)
|
||||
} else {
|
||||
listView.setItemChecked(position, true)
|
||||
}
|
||||
|
||||
activatedPosition = position
|
||||
}
|
||||
|
||||
override fun showPreviousList() = false
|
||||
|
||||
/**
|
||||
* A dummy implementation of the [ListSelectionListener] interface that does
|
||||
* nothing. Used only when this fragment is not attached to an activity.
|
||||
*/
|
||||
internal object DummyCallback : ListSelectionListener<Any> {
|
||||
override fun onItemSelected(item: Any) = Unit // NO OP
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
*/
|
||||
internal const val STATE_ACTIVATED_POSITION = "activated_position"
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Subscription detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a [MainActivity].
|
||||
*
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a [AddressDetailFragment].
|
||||
*/
|
||||
class AddressDetailActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
|
||||
intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM))
|
||||
val fragment = AddressDetailFragment()
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.wif.WifExporter
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_address_detail.*
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class AddressDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: BitmessageAddress? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.address, menu)
|
||||
|
||||
val ctx = activity!!
|
||||
Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail)
|
||||
Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share)
|
||||
Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val item = item ?: return false
|
||||
val ctx = activity ?: return false
|
||||
when (menuItem.itemId) {
|
||||
R.id.write_message -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
val intent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
val warning = if (item.privateKey != null)
|
||||
R.string.delete_identity_warning
|
||||
else
|
||||
R.string.delete_contact_warning
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(warning)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
Singleton.getAddressRepository(ctx).remove(item)
|
||||
MainActivity.apply {
|
||||
if (item.privateKey != null) {
|
||||
removeIdentityEntry(item)
|
||||
}
|
||||
}
|
||||
this.item = null
|
||||
ctx.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.export -> {
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(R.string.confirm_export)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"$item$EXPORT_POSTFIX"
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
WifExporter(Singleton.getBitmessageContext(ctx)).apply {
|
||||
addIdentity(item)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
R.id.share -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, item.address)
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
|
||||
= inflater.inflate(R.layout.fragment_address_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
activity?.let { activity ->
|
||||
when {
|
||||
item.isChan -> activity.setTitle(R.string.title_chan_detail)
|
||||
item.privateKey != null -> activity.setTitle(R.string.title_identity_detail)
|
||||
item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail)
|
||||
else -> activity.setTitle(R.string.title_contact_detail)
|
||||
}
|
||||
}
|
||||
|
||||
avatar.setImageDrawable(Identicon(item))
|
||||
name.setText(item.toString())
|
||||
name.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
item.alias = s.toString()
|
||||
}
|
||||
})
|
||||
address.text = item.address
|
||||
address.isSelected = true
|
||||
stream_number.text = getString(R.string.stream_number, item.stream)
|
||||
if (item.privateKey == null) {
|
||||
active.isChecked = item.isSubscribed
|
||||
active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked }
|
||||
|
||||
if (item.pubkey == null) {
|
||||
pubkey_available.alpha = 0.3f
|
||||
pubkey_available_desc.setText(R.string.pubkey_not_available)
|
||||
}
|
||||
} else {
|
||||
active.visibility = View.GONE
|
||||
pubkey_available.visibility = View.GONE
|
||||
pubkey_available_desc.visibility = View.GONE
|
||||
}
|
||||
|
||||
// QR code
|
||||
qr_code.setImageBitmap(Drawables.qrCode(item))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
item?.let { item ->
|
||||
Singleton.getAddressRepository(context!!).save(item)
|
||||
if (item.privateKey != null) {
|
||||
MainActivity.apply { updateIdentityEntry(item) }
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
const val EXPORT_POSTFIX = ".keys.dat"
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.FabUtils
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Fragment that shows a list of all contacts, the ones we subscribed to first.
|
||||
*/
|
||||
class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() {
|
||||
private lateinit var adapter: ArrayAdapter<BitmessageAddress>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = object : ArrayAdapter<BitmessageAddress>(
|
||||
activity,
|
||||
R.layout.subscription_row,
|
||||
R.id.name,
|
||||
LinkedList()) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val result: View
|
||||
val v: ViewHolder
|
||||
if (convertView == null) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.subscription_row, parent, false)
|
||||
v = ViewHolder(
|
||||
ctx = context,
|
||||
avatar = view.findViewById(R.id.avatar),
|
||||
name = view.findViewById(R.id.name),
|
||||
streamNumber = view.findViewById(R.id.stream_number),
|
||||
subscribed = view.findViewById(R.id.subscribed)
|
||||
)
|
||||
view.tag = v
|
||||
result = view
|
||||
} else {
|
||||
v = convertView.tag as ViewHolder
|
||||
result = convertView
|
||||
}
|
||||
getItem(position)?.let { item ->
|
||||
v.avatar.setImageDrawable(Identicon(item))
|
||||
v.name.text = item.toString()
|
||||
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
|
||||
v.subscribed.visibility = if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
listAdapter = adapter
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
initFab(activity as MainActivity)
|
||||
updateList()
|
||||
}
|
||||
|
||||
fun updateList() {
|
||||
adapter.clear()
|
||||
context?.let { context ->
|
||||
val addressRepo = Singleton.getAddressRepository(context)
|
||||
doAsync {
|
||||
addressRepo.getContactIds()
|
||||
.map { addressRepo.getAddress(it) }
|
||||
.forEach { address -> uiThread { adapter.add(address) } }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFab(activity: MainActivity) {
|
||||
activity.updateTitle(getString(R.string.contacts_and_subscriptions))
|
||||
val menu = FabSpeedDialMenu(activity)
|
||||
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
|
||||
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
|
||||
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
when (itemId) {
|
||||
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
|
||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
|
||||
.initiateScan()
|
||||
2 -> {
|
||||
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_address_list, container, false)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (data != null && data.hasExtra("SCAN_RESULT")) {
|
||||
val uri = Uri.parse(data.getStringExtra("SCAN_RESULT"))
|
||||
val intent = Intent(activity, CreateAddressActivity::class.java)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Void) = updateList()
|
||||
|
||||
private data class ViewHolder(
|
||||
val ctx: Context,
|
||||
val avatar: ImageView,
|
||||
val name: TextView,
|
||||
val streamNumber: TextView,
|
||||
val subscribed: View
|
||||
)
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setHomeAsUpIndicator(R.drawable.ic_action_close)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.content) == null) {
|
||||
// Display the fragment as the main content.
|
||||
val fragment = ComposeMessageFragment()
|
||||
fragment.arguments = intent.extras
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DRAFT = "ch.dissem.abit.Message.DRAFT"
|
||||
const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"
|
||||
const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"
|
||||
const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"
|
||||
const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"
|
||||
const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"
|
||||
const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"
|
||||
const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"
|
||||
|
||||
fun launchReplyTo(fragment: Fragment, item: Plaintext) =
|
||||
fragment.startActivity(
|
||||
getReplyIntent(
|
||||
ctx = fragment.activity
|
||||
?: throw IllegalStateException("Fragment not attached to an activity"),
|
||||
item = item
|
||||
)
|
||||
)
|
||||
|
||||
fun launchReplyTo(activity: Activity, item: Plaintext) =
|
||||
activity.startActivity(getReplyIntent(activity, item))
|
||||
|
||||
private fun getReplyIntent(ctx: Context, item: Plaintext): Intent {
|
||||
val replyIntent = Intent(ctx, ComposeMessageActivity::class.java)
|
||||
val receivingIdentity = item.to
|
||||
if (receivingIdentity?.isChan == true) {
|
||||
// reply to chan, not to the sender of the message
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity)
|
||||
// I hate when people send as chan, so it won't be the default behaviour.
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx))
|
||||
} else {
|
||||
replyIntent.putExtra(EXTRA_RECIPIENT, item.from)
|
||||
replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity)
|
||||
}
|
||||
// if the original message was sent using extended encoding, use it as well
|
||||
// so features like threading can be supported
|
||||
if (item.encoding == EXTENDED) {
|
||||
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED)
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_PARENT, item)
|
||||
item.subject?.let { subject ->
|
||||
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
|
||||
"RE:",
|
||||
ignoreCase = true
|
||||
)) {
|
||||
""
|
||||
} else {
|
||||
"RE: "
|
||||
}
|
||||
replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject)
|
||||
}
|
||||
replyIntent.putExtra(
|
||||
EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n" + item.text!!
|
||||
)
|
||||
return replyIntent
|
||||
}
|
||||
}
|
||||
}
|
@ -1,293 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_DRAFT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
|
||||
import ch.dissem.apps.abit.adapter.ContactAdapter
|
||||
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
||||
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.entity.valueobject.extended.Message
|
||||
import kotlinx.android.synthetic.main.fragment_compose_message.*
|
||||
|
||||
/**
|
||||
* Compose a new message.
|
||||
*/
|
||||
class ComposeMessageFragment : Fragment() {
|
||||
private lateinit var identity: BitmessageAddress
|
||||
private var recipient: BitmessageAddress? = null
|
||||
private var subject: String = ""
|
||||
private var content: String = ""
|
||||
|
||||
private var broadcast: Boolean = false
|
||||
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
|
||||
private val parents = mutableListOf<InventoryVector>()
|
||||
|
||||
private var draft: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
arguments?.apply {
|
||||
val draft = getSerializable(EXTRA_DRAFT) as Plaintext?
|
||||
if (draft != null) {
|
||||
this@ComposeMessageFragment.draft = draft
|
||||
identity = draft.from
|
||||
recipient = draft.to
|
||||
subject = draft.subject ?: ""
|
||||
content = draft.text ?: ""
|
||||
encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE
|
||||
parents.addAll(draft.parents)
|
||||
} else {
|
||||
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
|
||||
if (context != null && (id == null || id.privateKey == null)) {
|
||||
id = Singleton.getIdentity(context!!)
|
||||
}
|
||||
if (id?.privateKey != null) {
|
||||
identity = id
|
||||
} else {
|
||||
throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
}
|
||||
broadcast = getBoolean(EXTRA_BROADCAST, false)
|
||||
if (containsKey(EXTRA_RECIPIENT)) {
|
||||
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
|
||||
}
|
||||
if (containsKey(EXTRA_SUBJECT)) {
|
||||
subject = getString(EXTRA_SUBJECT)
|
||||
}
|
||||
if (containsKey(EXTRA_CONTENT)) {
|
||||
content = getString(EXTRA_CONTENT)
|
||||
}
|
||||
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?:
|
||||
Plaintext.Encoding.SIMPLE
|
||||
|
||||
if (containsKey(EXTRA_PARENT)) {
|
||||
val parent = getSerializable(EXTRA_PARENT) as Plaintext
|
||||
parent.inventoryVector?.let { parents.add(it) }
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalStateException("No identity set for ComposeMessageFragment")
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(R.layout.fragment_compose_message, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { ctx ->
|
||||
val identities = Singleton.getAddressRepository(ctx).getIdentities()
|
||||
sender_input.adapter = ContactAdapter(ctx, identities, true)
|
||||
val index = identities.indexOf(Singleton.getIdentity(ctx))
|
||||
if (index >= 0) {
|
||||
sender_input.setSelection(index)
|
||||
}
|
||||
|
||||
if (broadcast) {
|
||||
recipient_input.visibility = View.GONE
|
||||
} else {
|
||||
val adapter = ContactAdapter(
|
||||
ctx,
|
||||
Singleton.getAddressRepository(ctx).getContacts()
|
||||
)
|
||||
recipient_input.setAdapter(adapter)
|
||||
recipient_input.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _, _, pos, _ -> recipient = adapter.getItem(pos) }
|
||||
|
||||
recipient_input.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
recipient = adapter.getItem(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) =
|
||||
Unit // leave current selection
|
||||
}
|
||||
recipient?.let { recipient_input.setText(it.toString()) }
|
||||
}
|
||||
subject_input.setText(subject)
|
||||
body_input.setText(content)
|
||||
|
||||
when {
|
||||
recipient == null -> recipient_input.requestFocus()
|
||||
subject.isEmpty() -> subject_input.requestFocus()
|
||||
else -> {
|
||||
body_input.requestFocus()
|
||||
body_input.setSelection(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.compose, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.send -> {
|
||||
send()
|
||||
return true
|
||||
}
|
||||
R.id.select_encoding -> {
|
||||
val encodingDialog = SelectEncodingDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(EXTRA_ENCODING, encoding)
|
||||
encodingDialog.arguments = args
|
||||
encodingDialog.setTargetFragment(this, 0)
|
||||
encodingDialog.show(fragmentManager, "select encoding dialog")
|
||||
return true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) =
|
||||
if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
|
||||
encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun build(ctx: Context): Plaintext {
|
||||
val builder: Plaintext.Builder
|
||||
if (broadcast) {
|
||||
builder = Plaintext.Builder(BROADCAST)
|
||||
} else {
|
||||
val inputString = recipient_input.text.toString()
|
||||
if (recipient == null || recipient?.toString() != inputString) {
|
||||
try {
|
||||
recipient = BitmessageAddress(inputString)
|
||||
} catch (e: Exception) {
|
||||
val contacts = Singleton.getAddressRepository(ctx).getContacts()
|
||||
for (contact in contacts) {
|
||||
if (inputString.equals(contact.alias, ignoreCase = true)) {
|
||||
recipient = contact
|
||||
if (inputString == contact.alias)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
builder = Plaintext.Builder(MSG)
|
||||
.to(recipient)
|
||||
}
|
||||
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
sender?.let { builder.from(it) }
|
||||
if (!Preferences.requestAcknowledgements(ctx)) {
|
||||
builder.preventAck()
|
||||
}
|
||||
when (encoding) {
|
||||
Plaintext.Encoding.SIMPLE -> builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
Plaintext.Encoding.EXTENDED -> builder.message(
|
||||
ExtendedEncoding(
|
||||
Message(
|
||||
subject = subject_input.text.toString(),
|
||||
body = body_input.text.toString(),
|
||||
parents = parents,
|
||||
files = emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
ctx.getString(R.string.error_unsupported_encoding, encoding),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
builder.message(
|
||||
subject_input.text.toString(),
|
||||
body_input.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
draft?.id?.let { builder.id(it) }
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (draft?.labels?.any { it.type == Label.Type.DRAFT } != false) {
|
||||
context?.let { ctx ->
|
||||
draft = build(ctx).also { msg ->
|
||||
Singleton.labeler.markAsDraft(msg)
|
||||
Singleton.getMessageRepository(ctx).save(msg)
|
||||
}
|
||||
Toast.makeText(ctx, "Message saved as draft", Toast.LENGTH_LONG).show()
|
||||
} ?: throw IllegalStateException("Context is not available")
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
identity = sender_input.selectedItem as BitmessageAddress
|
||||
// recipient is set when one is selected
|
||||
subject = subject_input.text?.toString() ?: ""
|
||||
content = body_input.text?.toString() ?: ""
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
if (recipient == null) {
|
||||
Toast.makeText(ctx, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
build(ctx).let { message ->
|
||||
draft = message
|
||||
Singleton.getBitmessageContext(ctx).send(message)
|
||||
}
|
||||
ctx.finish()
|
||||
}
|
||||
}
|
||||
|
@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Base64
|
||||
import android.util.Base64.URL_SAFE
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V2Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class CreateAddressActivity : AppCompatActivity() {
|
||||
private var pubkeyBytes: ByteArray? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val uri = intent.data
|
||||
if (uri != null)
|
||||
setContentView(R.layout.activity_open_bitmessage_link)
|
||||
else
|
||||
setContentView(R.layout.activity_create_bitmessage_address)
|
||||
|
||||
val address = findViewById<TextView>(R.id.address)
|
||||
val label = findViewById<EditText>(R.id.label)
|
||||
val subscribe = findViewById<Switch>(R.id.subscribe)
|
||||
|
||||
if (uri != null) {
|
||||
val addressText = getAddress(uri)
|
||||
val parameters = getParameters(uri)
|
||||
for (parameter in parameters) {
|
||||
val matcher = KEY_VALUE_PATTERN.matcher(parameter)
|
||||
if (matcher.find()) {
|
||||
val key = matcher.group(1).toLowerCase()
|
||||
val value = matcher.group(2)
|
||||
when (key) {
|
||||
"label" -> label.setText(value.trim { it <= ' ' })
|
||||
"action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true)
|
||||
"pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE)
|
||||
else -> LOG.debug("Unknown attribute: $key=$value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address.text = addressText
|
||||
}
|
||||
|
||||
val cancel = findViewById<Button>(R.id.cancel)
|
||||
cancel.setOnClickListener {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) }
|
||||
}
|
||||
|
||||
|
||||
private fun onOK(address: TextView, label: EditText, subscribe: Switch) {
|
||||
val addressText = address.text.toString().trim { it <= ' ' }
|
||||
try {
|
||||
val bmAddress = BitmessageAddress(addressText)
|
||||
bmAddress.alias = label.text.toString()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(applicationContext)
|
||||
bmc.addContact(bmAddress)
|
||||
if (subscribe.isChecked) {
|
||||
bmc.addSubscribtion(bmAddress)
|
||||
}
|
||||
pubkeyBytes?.let { pubkeyBytes ->
|
||||
try {
|
||||
val pubkeyStream = ByteArrayInputStream(pubkeyBytes)
|
||||
val stream = bmAddress.stream
|
||||
when (bmAddress.version.toInt()) {
|
||||
2 -> V2Pubkey.read(pubkeyStream, stream)
|
||||
3 -> V3Pubkey.read(pubkeyStream, stream)
|
||||
4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream))
|
||||
else -> null
|
||||
}?.let { bmAddress.pubkey = it }
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} catch (e: RuntimeException) {
|
||||
address.error = getString(R.string.error_illegal_address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAddress(uri: Uri): String {
|
||||
val result = StringBuilder()
|
||||
val schemeSpecificPart = uri.schemeSpecificPart
|
||||
if (!schemeSpecificPart.startsWith("BM-")) {
|
||||
result.append("BM-")
|
||||
}
|
||||
when {
|
||||
schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')))
|
||||
schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')))
|
||||
else -> result.append(schemeSpecificPart)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun getParameters(uri: Uri): Array<String> {
|
||||
val index = uri.schemeSpecificPart.indexOf('?')
|
||||
return if (index >= 0) {
|
||||
uri.schemeSpecificPart
|
||||
.substring(index + 1)
|
||||
.split("&".toRegex())
|
||||
.dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
} else {
|
||||
emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java)
|
||||
|
||||
private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$")
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.MenuItem
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
abstract class DetailActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.scrolling_toolbar_layout)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
// Show the Up button in the action bar.
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.TextPaint
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val hash = input.ripe
|
||||
private val isChan = input.isChan
|
||||
private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply {
|
||||
for (row in 0 until SIZE) {
|
||||
if (!isChan || row < 5 || row > 6) {
|
||||
for (column in 0..CENTER_COLUMN) {
|
||||
if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) {
|
||||
this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0
|
||||
this[row][SIZE - column - 1] = this[row][column]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f))
|
||||
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f))
|
||||
private val textPaint = TextPaint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = 0xFF607D8B.toInt()
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
var x: Float
|
||||
var y: Float
|
||||
val width = canvas.width.toFloat()
|
||||
val height = canvas.height.toFloat()
|
||||
val cellWidth = width / SIZE.toFloat()
|
||||
val cellHeight = height / SIZE.toFloat()
|
||||
paint.color = background
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
paint.color = color
|
||||
for (row in 0 until SIZE) {
|
||||
for (column in 0 until SIZE) {
|
||||
if (fields[row][column]) {
|
||||
x = cellWidth * column
|
||||
y = cellHeight * row
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isChan) {
|
||||
textPaint.textSize = 2 * cellHeight
|
||||
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
paint.colorFilter = cf
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
|
||||
companion object {
|
||||
private const val SIZE = 9
|
||||
private const val CENTER_COLUMN = 5
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Fragment
|
||||
import android.os.Bundle
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.wif.WifImporter
|
||||
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
|
||||
import org.ini4j.InvalidFileFormatException
|
||||
import org.jetbrains.anko.longToast
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentitiesFragment : Fragment() {
|
||||
private lateinit var adapter: AddressSelectorAdapter
|
||||
private lateinit var importer: WifImporter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val wifData = arguments.getString(WIF_DATA)
|
||||
val bmc = Singleton.getBitmessageContext(activity)
|
||||
|
||||
try {
|
||||
importer = WifImporter(bmc, wifData)
|
||||
} catch (e: InvalidFileFormatException) {
|
||||
longToast(R.string.invalid_wif_file)
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = AddressSelectorAdapter(importer.getIdentities())
|
||||
val layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
recyclerView.addItemDecoration(
|
||||
SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true
|
||||
)
|
||||
)
|
||||
|
||||
view.findViewById<Button>(R.id.finish).setOnClickListener {
|
||||
importer.importAll(adapter.selected)
|
||||
MainActivity.apply {
|
||||
for (selected in adapter.selected) {
|
||||
addIdentityEntry(selected)
|
||||
}
|
||||
}
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WIF_DATA = "wif_data"
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ImportIdentityActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
|
||||
|
||||
if (wifData == null) {
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.content, InputWifFragment())
|
||||
.commit()
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
|
||||
|
||||
val fragment = ImportIdentitiesFragment()
|
||||
fragment.arguments = bundle
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Fragment
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import com.github.angads25.filepicker.model.DialogConfigs
|
||||
import com.github.angads25.filepicker.model.DialogProperties
|
||||
import com.github.angads25.filepicker.view.FilePickerDialog
|
||||
import kotlinx.android.synthetic.main.fragment_import_input.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class InputWifFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_import_input, container, false)
|
||||
|
||||
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
next.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString())
|
||||
|
||||
val fragment = ImportIdentitiesFragment().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
|
||||
inflater.inflate(R.menu.import_input_data, menu)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val properties = DialogProperties()
|
||||
properties.selection_mode = DialogConfigs.SINGLE_MODE
|
||||
properties.selection_type = DialogConfigs.FILE_SELECT
|
||||
properties.root = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.error_dir = File(DialogConfigs.DEFAULT_DIR)
|
||||
properties.extensions = null
|
||||
val dialog = FilePickerDialog(activity, properties)
|
||||
dialog.setTitle(getString(R.string.select_file_title))
|
||||
dialog.setDialogSelectionListener { files ->
|
||||
if (files.isNotEmpty()) {
|
||||
try {
|
||||
FileInputStream(files[0]).use { inputStream ->
|
||||
val data = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
|
||||
var length: Int = inputStream.read(buffer)
|
||||
|
||||
while (length != -1) {
|
||||
data.write(buffer, 0, length)
|
||||
length = inputStream.read(buffer)
|
||||
}
|
||||
wif_input.setText(data.toByteArray().toString())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.error_loading_data,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
interface ListHolder<in L> {
|
||||
fun updateList(label: L)
|
||||
|
||||
fun setActivateOnItemClick(activateOnItemClick: Boolean)
|
||||
|
||||
fun showPreviousList(): Boolean
|
||||
}
|
@ -1,551 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Point
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.drawer.ProfileImageListener
|
||||
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
||||
import ch.dissem.apps.abit.util.Labels
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.github.amlcurran.showcaseview.ShowcaseView
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.AccountHeaderBuilder
|
||||
import com.mikepenz.materialdrawer.Drawer
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Nameable
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a list of Messages. This activity
|
||||
* has different presentations for handset and tablet-size devices. On
|
||||
* handsets, the activity presents a list of items, which when touched,
|
||||
* lead to a [MessageDetailActivity] representing
|
||||
* item details. On tablets, the activity presents the list of items and
|
||||
* item details side-by-side using two vertical panes.
|
||||
*
|
||||
*
|
||||
* The activity makes heavy use of fragments. The list of items is a
|
||||
* [MessageListFragment] and the item details
|
||||
* (if present) is a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* This activity also implements the required
|
||||
* [ListSelectionListener] interface
|
||||
* to listen for item selections.
|
||||
*
|
||||
*/
|
||||
class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
|
||||
private var active: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
*/
|
||||
var hasDetailPane: Boolean = false
|
||||
private set
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private lateinit var accountHeader: AccountHeader
|
||||
|
||||
private lateinit var drawer: Drawer
|
||||
private lateinit var nodeSwitch: SwitchDrawerItem
|
||||
|
||||
val floatingActionButton: FabSpeedDial?
|
||||
get() = findViewById(R.id.fab)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
instance = WeakReference(this)
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
fab.hide()
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val listFragment = MessageListFragment()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
.commit()
|
||||
|
||||
if (findViewById<View>(R.id.message_detail_container) != null) {
|
||||
// The detail container view will be present only in the
|
||||
// large-screen layouts (res/values-large and
|
||||
// res/values-sw600dp). If this view is present, then the
|
||||
// activity should be in two-pane mode.
|
||||
hasDetailPane = true
|
||||
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
|
||||
createDrawer(toolbar)
|
||||
|
||||
// handle intents
|
||||
val intent = intent
|
||||
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE))
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
|
||||
val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
}
|
||||
|
||||
if (Preferences.useTrustedNode(this)) {
|
||||
SyncAdapter.startSync(this)
|
||||
} else {
|
||||
SyncAdapter.stopSync(this)
|
||||
}
|
||||
if (drawer.isDrawerOpen) {
|
||||
val lps = RelativeLayout.LayoutParams(
|
||||
ViewGroup
|
||||
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
|
||||
addRule(RelativeLayout.ALIGN_PARENT_LEFT)
|
||||
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
|
||||
setMargins(margin, margin, margin, margin)
|
||||
}
|
||||
|
||||
ShowcaseView.Builder(this)
|
||||
.withMaterialShowcase()
|
||||
.setStyle(R.style.CustomShowcaseTheme)
|
||||
.setContentTitle(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setTarget {
|
||||
val view = drawer.stickyFooter
|
||||
val location = IntArray(2)
|
||||
view.getLocationInWindow(location)
|
||||
val x = location[0] + 7 * view.width / 8
|
||||
val y = location[1] + view.height / 2
|
||||
Point(x, y)
|
||||
}
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
|
||||
if (active) {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.item_list, listFragment)
|
||||
supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let {
|
||||
transaction.remove(it)
|
||||
}
|
||||
transaction.addToBackStack(null).commit()
|
||||
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, list items should be given the
|
||||
// 'activated' state when touched.
|
||||
listFragment.setActivateOnItemClick(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDrawer(toolbar: Toolbar) {
|
||||
val profiles = ArrayList<IProfile<*>>()
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.add_identity))
|
||||
.withDescription(getString(R.string.add_identity_summary))
|
||||
.withIcon(
|
||||
IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
|
||||
.actionBar()
|
||||
.paddingDp(5)
|
||||
.colorRes(R.color.icons)
|
||||
)
|
||||
.withIdentifier(ADD_IDENTITY.toLong())
|
||||
)
|
||||
profiles.add(
|
||||
ProfileSettingDrawerItem()
|
||||
.withName(getString(R.string.manage_identity))
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
.withIdentifier(MANAGE_IDENTITY.toLong())
|
||||
)
|
||||
// Create the AccountHeader
|
||||
accountHeader = AccountHeaderBuilder()
|
||||
.withActivity(this)
|
||||
.withHeaderBackground(R.drawable.header)
|
||||
.withProfiles(profiles)
|
||||
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
|
||||
.withOnAccountHeaderListener(
|
||||
ProfileSelectionListener(
|
||||
this@MainActivity,
|
||||
supportFragmentManager
|
||||
)
|
||||
)
|
||||
.build()
|
||||
if (profiles.size > 2) { // There's always the add and manage identity items
|
||||
accountHeader.setActiveProfile(profiles[0], true)
|
||||
}
|
||||
|
||||
val drawerItems = ArrayList<IDrawerItem<*, *>>()
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withIdentifier(LABEL_ARCHIVE.id as Long)
|
||||
.withName(R.string.archive)
|
||||
.withTag(LABEL_ARCHIVE)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_archive)
|
||||
)
|
||||
drawerItems.add(DividerDrawerItem())
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.contacts_and_subscriptions)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_contacts)
|
||||
)
|
||||
drawerItems.add(
|
||||
PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
)
|
||||
|
||||
nodeSwitch = SwitchDrawerItem()
|
||||
.withIdentifier(ID_NODE_SWITCH)
|
||||
.withName(R.string.full_node)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
|
||||
.withChecked(Preferences.isFullNodeActive(this))
|
||||
.withOnCheckedChangeListener { _, _, isChecked ->
|
||||
if (isChecked) {
|
||||
NetworkUtils.enableNode(this@MainActivity)
|
||||
} else {
|
||||
NetworkUtils.disableNode(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
drawer = DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(accountHeader)
|
||||
.withDrawerItems(drawerItems)
|
||||
.addStickyDrawerItems(nodeSwitch)
|
||||
.withOnDrawerItemClickListener(DrawerItemClickListener())
|
||||
.withShowDrawerOnFirstLaunch(true)
|
||||
.build()
|
||||
|
||||
loadDrawerItemsAsynchronously()
|
||||
}
|
||||
|
||||
private fun loadDrawerItemsAsynchronously() {
|
||||
doAsync {
|
||||
val identities = bmc.addresses.getIdentities()
|
||||
if (identities.isEmpty()) {
|
||||
// Create an initial identity
|
||||
Singleton.getIdentity(this@MainActivity)
|
||||
}
|
||||
|
||||
uiThread {
|
||||
for (identity in identities) {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAsync {
|
||||
val labels = bmc.labels.getLabels()
|
||||
|
||||
uiThread {
|
||||
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
|
||||
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
|
||||
} else if (currentLabel.value == null) {
|
||||
currentLabel.value = labels[0]
|
||||
}
|
||||
for (label in labels) {
|
||||
addLabelEntry(label)
|
||||
}
|
||||
currentLabel.value?.let {
|
||||
drawer.setSelection(it.id as Long)
|
||||
}
|
||||
updateUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
|
||||
override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean {
|
||||
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
|
||||
val tag = item.tag
|
||||
if (tag is Label) {
|
||||
currentLabel.value = tag
|
||||
if (itemList !is MessageListFragment) {
|
||||
changeList(MessageListFragment())
|
||||
}
|
||||
return false
|
||||
} else if (item is Nameable<*>) {
|
||||
when (item.name.textRes) {
|
||||
R.string.contacts_and_subscriptions -> {
|
||||
if (itemList is AddressListFragment) {
|
||||
itemList.updateList()
|
||||
} else {
|
||||
changeList(AddressListFragment())
|
||||
}
|
||||
return false
|
||||
}
|
||||
R.string.settings -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, SettingsFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
return false
|
||||
}
|
||||
R.string.full_node -> return true
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
updateUnread()
|
||||
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
|
||||
NetworkUtils.enableNode(this, false)
|
||||
}
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
currentLabel.addObserver(this) { label ->
|
||||
if (label != null && label.id is Long) {
|
||||
drawer.setSelection(label.id as Long)
|
||||
}
|
||||
}
|
||||
active = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
currentLabel.removeObserver(this)
|
||||
super.onPause()
|
||||
active = false
|
||||
}
|
||||
|
||||
fun addIdentityEntry(identity: BitmessageAddress) {
|
||||
val newProfile = ProfileDrawerItem()
|
||||
.withIcon(Identicon(identity))
|
||||
.withName(identity.toString())
|
||||
.withNameShown(true)
|
||||
.withEmail(identity.address)
|
||||
.withTag(identity)
|
||||
if (accountHeader.profiles != null) {
|
||||
// we know that there are 2 setting elements.
|
||||
// Set the new profile above them ;)
|
||||
accountHeader.addProfile(
|
||||
newProfile, accountHeader.profiles.size - 2
|
||||
)
|
||||
} else {
|
||||
accountHeader.addProfiles(newProfile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLabelEntry(label: Label) {
|
||||
val item = PrimaryDrawerItem()
|
||||
.withIdentifier(label.id as Long)
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(Labels.getIcon(label))
|
||||
.withIconColor(Labels.getColor(label))
|
||||
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
|
||||
}
|
||||
|
||||
fun updateIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
profile
|
||||
.withName(identity.toString())
|
||||
.withTag(identity)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIdentityEntry(identity: BitmessageAddress) {
|
||||
for (profile in accountHeader.profiles) {
|
||||
if (profile is ProfileDrawerItem) {
|
||||
if (identity == profile.tag) {
|
||||
accountHeader.removeProfile(profile)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUnread() {
|
||||
for (item in drawer.drawerItems) {
|
||||
if (item.tag is Label) {
|
||||
val label = item.tag as Label
|
||||
if (label !== LABEL_ARCHIVE) {
|
||||
val unread = bmc.messages.countUnread(label)
|
||||
if (unread > 0) {
|
||||
(item as PrimaryDrawerItem).withBadge(unread.toString())
|
||||
} else {
|
||||
(item as PrimaryDrawerItem).withBadge(null as String?)
|
||||
}
|
||||
drawer.updateItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method from [ListSelectionListener]
|
||||
* indicating that the item with the given ID was selected.
|
||||
*/
|
||||
override fun onItemSelected(item: Serializable) {
|
||||
if (hasDetailPane) {
|
||||
// In two-pane mode, show the detail view in this activity by
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
val fragment = when (item) {
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
ComposeMessageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessageDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> {
|
||||
AddressDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
val detailIntent = when (item) {
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
Intent(this, ComposeMessageActivity::class.java).apply {
|
||||
putExtra(ComposeMessageActivity.EXTRA_DRAFT, item)
|
||||
}
|
||||
} else {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java).apply {
|
||||
putExtra(AddressDetailFragment.ARG_ITEM, item)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
|
||||
}
|
||||
startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDetailView(fragment: Fragment) {
|
||||
if (hasDetailPane) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.message_detail_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(title: CharSequence) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
|
||||
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
|
||||
const val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
|
||||
const val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
|
||||
|
||||
const val ADD_IDENTITY = 1
|
||||
const val MANAGE_IDENTITY = 2
|
||||
|
||||
private const val ID_NODE_SWITCH: Long = 1
|
||||
|
||||
private var instance: WeakReference<MainActivity>? = null
|
||||
|
||||
fun updateNodeSwitch() {
|
||||
apply {
|
||||
runOnUiThread {
|
||||
nodeSwitch.withChecked(Preferences.isFullNodeActive(this))
|
||||
drawer.updateStickyFooterItem(nodeSwitch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given code in the main activity context, if it currently exists. Otherwise,
|
||||
* it's ignored.
|
||||
*/
|
||||
fun apply(run: MainActivity.() -> Unit) {
|
||||
instance?.get()?.let { run.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.view.MenuItem
|
||||
|
||||
|
||||
/**
|
||||
* An activity representing a single Message detail screen. This
|
||||
* activity is only used on handset devices. On tablet-size devices,
|
||||
* item details are presented side-by-side with a list of items
|
||||
* in a [MainActivity].
|
||||
*
|
||||
* This activity is mostly just a 'shell' activity containing nothing
|
||||
* more than a [MessageDetailFragment].
|
||||
*/
|
||||
class MessageDetailActivity : DetailActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
if (savedInstanceState == null) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
|
||||
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM))
|
||||
val fragment = MessageDetailFragment()
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
@ -1,305 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.IdRes
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.GridLayoutManager
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.util.Linkify
|
||||
import android.text.util.Linkify.WEB_URLS
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.Labels
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
import kotlinx.android.synthetic.main.fragment_message_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class MessageDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var item: Plaintext? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
item = arguments.getSerializable(ARG_ITEM) as Plaintext
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
status.setImageResource(Assets.getStatusDrawable(item.status))
|
||||
status.contentDescription = getString(Assets.getStatusString(item.status))
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(item.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
sender.text = item.from.toString()
|
||||
item.to?.let { to ->
|
||||
recipient.text = to.toString()
|
||||
} ?: {
|
||||
if (item.type == Plaintext.Type.BROADCAST) {
|
||||
recipient.setText(R.string.broadcast)
|
||||
}
|
||||
}.invoke()
|
||||
val labelAdapter = LabelAdapter(ctx, item.labels)
|
||||
labels.adapter = labelAdapter
|
||||
labels.layoutManager = GridLayoutManager(activity, 2)
|
||||
|
||||
text.text = item.text
|
||||
|
||||
Linkify.addLinks(text, WEB_URLS)
|
||||
Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
text.linksClickable = true
|
||||
text.setTextIsSelectable(true)
|
||||
|
||||
val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
if (item.isUnread()) {
|
||||
Singleton.labeler.markAsRead(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
messageRepo.save(item)
|
||||
}
|
||||
val parents = ArrayList<Plaintext>(item.parents.size)
|
||||
for (parentIV in item.parents) {
|
||||
val parent = messageRepo.getMessage(parentIV)
|
||||
if (parent != null) {
|
||||
parents.add(parent)
|
||||
}
|
||||
}
|
||||
showRelatedMessages(ctx, view, R.id.parents, parents)
|
||||
showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRelatedMessages(
|
||||
ctx: Context,
|
||||
rootView: View, @IdRes id: Int,
|
||||
messages: List<Plaintext>
|
||||
) {
|
||||
val recyclerView = rootView.findViewById<RecyclerView>(id)
|
||||
val adapter = RelatedMessageAdapter(ctx, messages)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply)
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(
|
||||
activity, menu, R.id.mark_unread, GoogleMaterial.Icon
|
||||
.gmd_markunread
|
||||
)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(this, item)
|
||||
return true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
if (item.isUnread() && activity is MainActivity) {
|
||||
(activity as MainActivity).updateUnread()
|
||||
}
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
(activity as? MainActivity)?.updateUnread()
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private class RelatedMessageAdapter internal constructor(
|
||||
private val ctx: Context,
|
||||
private val messages: List<Plaintext>
|
||||
) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RelatedMessageAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = messages[position]
|
||||
|
||||
viewHolder.avatar.setImageDrawable(Identicon(message.from))
|
||||
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status))
|
||||
viewHolder.sender.text = message.from.toString()
|
||||
viewHolder.extract.text = prepareMessageExtract(message.text)
|
||||
viewHolder.item = message
|
||||
}
|
||||
|
||||
// Returns the total count of items in the list
|
||||
override fun getItemCount() = messages.size
|
||||
|
||||
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
internal val avatar = itemView.findViewById<ImageView>(R.id.avatar)
|
||||
internal val status = itemView.findViewById<ImageView>(R.id.status)
|
||||
internal val sender = itemView.findViewById<TextView>(R.id.sender)
|
||||
internal val extract = itemView.findViewById<TextView>(R.id.text)
|
||||
internal var item: Plaintext? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (ctx is MainActivity) {
|
||||
item?.let { ctx.onItemSelected(it) }
|
||||
} else {
|
||||
val detailIntent = Intent(ctx, MessageDetailActivity::class.java)
|
||||
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
|
||||
ctx.startActivity(detailIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
private val labels = labels.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.color(Labels.getColor(label))
|
||||
viewHolder.icon.icon?.icon(Labels.getIcon(label))
|
||||
viewHolder.label.text = Labels.getText(label, ctx)
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM = "item"
|
||||
|
||||
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
|
||||
}
|
||||
}
|
@ -1,324 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.RecyclerView.OnScrollListener
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||
import ch.dissem.apps.abit.util.FabUtils
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
|
||||
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
|
||||
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
|
||||
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.support.v4.onUiThread
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
|
||||
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
|
||||
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
|
||||
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
|
||||
|
||||
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||
&& firstVisibleItemPosition >= 0) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableMessageAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
|
||||
onUiThread {
|
||||
messageAdapter.addAll(messages)
|
||||
isLoading = false
|
||||
isLastPage = messages.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
|
||||
currentLabel.addObserver(this) { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
currentLabel.removeObserver(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableMessageAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableMessageAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||
mainActivity?.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val context = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
|
||||
// animation is running)
|
||||
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
|
||||
setInterceptVerticalScrollingWhileAnimationRunning(true)
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
// swipe manager
|
||||
val swipeManager = RecyclerViewSwipeManager()
|
||||
|
||||
//swipeableMessageAdapter
|
||||
val adapter = SwipeableMessageAdapter().apply {
|
||||
setActivateOnItemClick(activateOnItemClick)
|
||||
}
|
||||
adapter.eventListener = object : SwipeableMessageAdapter.EventListener {
|
||||
override fun onItemDeleted(item: Plaintext) {
|
||||
if (MessageDetailFragment.isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemArchived(item: Plaintext) {
|
||||
Singleton.labeler.archive(item)
|
||||
}
|
||||
|
||||
override fun onItemViewClicked(v: View?) {
|
||||
val position = recycler_view.getChildAdapterPosition(v)
|
||||
adapter.setSelectedPosition(position)
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val item = adapter.getItem(position)
|
||||
(activity as MainActivity).onItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wrap for swiping
|
||||
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
|
||||
|
||||
val animator = SwipeDismissItemAnimator()
|
||||
|
||||
// Change animations are enabled by default since support-v7-recyclerview v22.
|
||||
// Disable the change animation in order to make turning back animation of swiped item
|
||||
// works properly.
|
||||
animator.supportsChangeAnimations = false
|
||||
|
||||
recycler_view.layoutManager = layoutManager
|
||||
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableMessageAdapter
|
||||
recycler_view.itemAnimator = animator
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
recycler_view.addItemDecoration(SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true))
|
||||
|
||||
// NOTE:
|
||||
// The initialization order is very important! This order determines the priority of
|
||||
// touch event handling.
|
||||
//
|
||||
// priority: TouchActionGuard > Swipe > DragAndDrop
|
||||
touchActionGuardManager.attachRecyclerView(recycler_view)
|
||||
swipeManager.attachRecyclerView(recycler_view)
|
||||
|
||||
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
||||
recyclerViewSwipeManager = swipeManager
|
||||
this.swipeableMessageAdapter = adapter
|
||||
|
||||
Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
recyclerViewSwipeManager?.release()
|
||||
recyclerViewSwipeManager = null
|
||||
|
||||
recyclerViewTouchActionGuardManager?.release()
|
||||
recyclerViewTouchActionGuardManager = null
|
||||
|
||||
recycler_view.itemAnimator = null
|
||||
recycler_view.adapter = null
|
||||
|
||||
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
|
||||
wrappedAdapter = null
|
||||
|
||||
swipeableMessageAdapter = null
|
||||
layoutManager = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.value = label
|
||||
}
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick)
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.value = backStack.pop()
|
||||
true
|
||||
}
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.content.FileProvider.getUriForFile
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
|
||||
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
|
||||
import ch.dissem.apps.abit.util.Exports
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.support.v4.indeterminateProgressDialog
|
||||
import org.jetbrains.anko.support.v4.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
|
||||
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
|
||||
val cleanup = findPreference("cleanup")
|
||||
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
|
||||
findPreference("export")?.onPreferenceClickListener = exportClickListener()
|
||||
findPreference("import")?.onPreferenceClickListener = importClickListener()
|
||||
findPreference("status").onPreferenceClickListener = statusClickListener()
|
||||
}
|
||||
|
||||
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
|
||||
(activity as? MainActivity)?.let { activity ->
|
||||
val libsBuilder = LibsBuilder()
|
||||
.withActivityTitle(activity.getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutDescription(getString(R.string.about_app))
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(libsBuilder.supportFragment())
|
||||
} else {
|
||||
libsBuilder.start(activity)
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available")
|
||||
cleanup.isEnabled = false
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
|
||||
|
||||
doAsync {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
bmc.internals.nodeRegistry.clear()
|
||||
bmc.cleanup()
|
||||
Preferences.cleanupExportDirectory(ctx)
|
||||
|
||||
uiThread {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
R.string.cleanup_notification_end,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
cleanup.isEnabled = true
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun exportClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = context ?: throw IllegalStateException("No context available")
|
||||
|
||||
indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
|
||||
doAsync {
|
||||
val exportDirectory = Preferences.getExportDirectory(ctx)
|
||||
exportDirectory.mkdirs()
|
||||
val file = Exports.exportData(exportDirectory, ctx)
|
||||
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
|
||||
val intent = Intent(android.content.Intent.ACTION_SEND)
|
||||
intent.type = "application/zip"
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun importClickListener() = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "application/zip"
|
||||
|
||||
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
private fun statusClickListener() = Preference.OnPreferenceClickListener {
|
||||
val activity = activity as MainActivity
|
||||
if (activity.hasDetailPane) {
|
||||
activity.setDetailView(StatusFragment())
|
||||
} else {
|
||||
startActivity<StatusActivity>()
|
||||
}
|
||||
return@OnPreferenceClickListener true
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val ctx = context ?: throw IllegalStateException("No context available")
|
||||
when (requestCode) {
|
||||
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx)
|
||||
READ_IMPORT_REQUEST_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||
indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
|
||||
doAsync {
|
||||
Exports.importData(data.data, ctx)
|
||||
uiThread {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(ctx: Context?) {
|
||||
super.onAttach(ctx)
|
||||
(ctx as? MainActivity)?.floatingActionButton?.hide()
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
|
||||
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) {
|
||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
||||
val ctx = context ?: throw IllegalStateException("No context available")
|
||||
if (node != null) {
|
||||
SyncAdapter.startSync(ctx)
|
||||
} else {
|
||||
SyncAdapter.stopSync(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
|
||||
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
|
||||
if (node != null) {
|
||||
val ctx = context ?: throw IllegalStateException("No context available")
|
||||
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
|
||||
SyncAdapter.startPowSync(ctx)
|
||||
} else {
|
||||
SyncAdapter.stopPowSync(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WRITE_EXPORT_REQUEST_CODE = 1
|
||||
const val READ_IMPORT_REQUEST_CODE = 2
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import com.mikepenz.materialize.MaterializeBuilder
|
||||
import kotlinx.android.synthetic.main.activity_status.*
|
||||
|
||||
class StatusActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_status)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(false)
|
||||
}
|
||||
|
||||
MaterializeBuilder()
|
||||
.withActivity(this)
|
||||
.withStatusBarColorRes(R.color.colorPrimaryDark)
|
||||
.withTranslucentStatusBarProgrammatically(true)
|
||||
.withStatusBarPadding(true)
|
||||
.build()
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(this)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
content.text = status
|
||||
}
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
|
||||
class StatusFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_status, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val bmc = Singleton.getBitmessageContext(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
val status = StringBuilder()
|
||||
for (address in bmc.addresses.getIdentities()) {
|
||||
status.append(address.address).append('\n')
|
||||
}
|
||||
status.append('\n')
|
||||
status.append(bmc.status())
|
||||
view.findViewById<TextView>(R.id.content).text = status
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
|
||||
|
||||
private val data = identities.map { Selectable(it) }.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.select_identity_row, parent, false)
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val selectable = data[position]
|
||||
holder.data = selectable
|
||||
holder.checkbox.isChecked = selectable.selected
|
||||
holder.checkbox.text = selectable.data.toString()
|
||||
holder.address.text = selectable.data.address
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) {
|
||||
var data: Selectable<BitmessageAddress>? = null
|
||||
val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!!
|
||||
val address = v.findViewById<TextView>(R.id.address)!!
|
||||
|
||||
init {
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
data?.selected = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selected: List<BitmessageAddress>
|
||||
get() {
|
||||
return data
|
||||
.filter { it.selected }
|
||||
.mapTo(LinkedList()) { it.data }
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import ch.dissem.apps.abit.util.PRNGFixes
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidCryptography : SpongyCryptography() {
|
||||
init {
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* An adapter for contacts. Can be filtered by alias or address.
|
||||
*/
|
||||
class ContactAdapter(
|
||||
ctx: Context,
|
||||
private val originalData: List<BitmessageAddress>,
|
||||
private val slim: Boolean = false
|
||||
) :
|
||||
BaseAdapter(), Filterable {
|
||||
private val inflater = LayoutInflater.from(ctx)
|
||||
private var data: List<BitmessageAddress> = originalData
|
||||
|
||||
override fun getCount() = data.size
|
||||
|
||||
override fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val viewHolder = if (convertView == null) {
|
||||
ViewHolder(
|
||||
inflater.inflate(
|
||||
if (slim) {
|
||||
R.layout.contact_row_slim
|
||||
} else {
|
||||
R.layout.contact_row
|
||||
},
|
||||
parent, false
|
||||
)
|
||||
)
|
||||
} else {
|
||||
convertView.tag as ViewHolder
|
||||
}
|
||||
val item = getItem(position)
|
||||
viewHolder.avatar.setImageDrawable(Identicon(item))
|
||||
viewHolder.name.text = item.toString()
|
||||
viewHolder.address?.text = item.address
|
||||
|
||||
return viewHolder.view
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter = ContactFilter()
|
||||
|
||||
private inner class ViewHolder(val view: View) {
|
||||
val avatar: ImageView = view.findViewById(R.id.avatar)
|
||||
val name: TextView = view.findViewById(R.id.name)
|
||||
val address: TextView? = view.findViewById(R.id.address)
|
||||
|
||||
init {
|
||||
view.tag = this
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class ContactFilter : Filter() {
|
||||
override fun performFiltering(prefix: CharSequence?): Filter.FilterResults {
|
||||
val results = Filter.FilterResults()
|
||||
|
||||
if (prefix?.isEmpty() == false) {
|
||||
val prefixString = prefix.toString().toLowerCase()
|
||||
|
||||
val newValues = ArrayList<BitmessageAddress>()
|
||||
|
||||
originalData
|
||||
.forEach { value ->
|
||||
value.alias?.toLowerCase()?.let { alias ->
|
||||
if (alias.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
} else {
|
||||
val words =
|
||||
alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
|
||||
for (word in words) {
|
||||
if (word.startsWith(prefixString)) {
|
||||
newValues.add(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
val address = value.address.toLowerCase()
|
||||
if (address.contains(prefixString)) {
|
||||
newValues.add(value)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
if (newValues.isEmpty()) {
|
||||
try {
|
||||
newValues.add(BitmessageAddress(prefix.toString()))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
results.values = newValues
|
||||
results.count = newValues.size
|
||||
} else {
|
||||
results.values = originalData
|
||||
results.count = originalData.size
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data = results.values as List<BitmessageAddress>
|
||||
if (results.count > 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class Selectable<out T>(val data: T) {
|
||||
var selected = false
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Haruki Hasegawa
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Typeface
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
|
||||
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
|
||||
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
|
||||
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||
*
|
||||
* @author Christian Basler
|
||||
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
*/
|
||||
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
|
||||
|
||||
private val data = LinkedList<Plaintext>()
|
||||
var eventListener: EventListener? = null
|
||||
private val itemViewOnClickListener: View.OnClickListener
|
||||
private val swipeableViewContainerOnClickListener: View.OnClickListener
|
||||
|
||||
private var label: Label? = null
|
||||
private var selectedPosition = -1
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onItemDeleted(item: Plaintext)
|
||||
|
||||
fun onItemArchived(item: Plaintext)
|
||||
|
||||
fun onItemViewClicked(v: View?)
|
||||
}
|
||||
|
||||
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
|
||||
val container = v.findViewById<FrameLayout>(R.id.container)!!
|
||||
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||
|
||||
override fun getSwipeableContainerView() = container
|
||||
}
|
||||
|
||||
init {
|
||||
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
|
||||
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
|
||||
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
fun add(item: Plaintext) {
|
||||
data.add(item)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addFirst(item: Plaintext) {
|
||||
val index = data.size
|
||||
data.addFirst(item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
|
||||
fun addAll(items: Collection<Plaintext>) {
|
||||
val index = data.size
|
||||
data.addAll(items)
|
||||
notifyItemRangeInserted(index, items.size)
|
||||
}
|
||||
|
||||
fun remove(item: Plaintext) {
|
||||
val index = data.indexOf(item)
|
||||
data.removeAll { it.id == item.id }
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
fun update(item: Plaintext) {
|
||||
val index = data.indexOfFirst { it.id == item.id }
|
||||
if (index >= 0) {
|
||||
data[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(newLabel: Label?) {
|
||||
label = newLabel
|
||||
data.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun onItemViewClick(v: View) {
|
||||
eventListener?.onItemViewClicked(v)
|
||||
}
|
||||
|
||||
private fun onSwipeableViewContainerClick(v: View) {
|
||||
eventListener?.onItemViewClicked(
|
||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = data[position]
|
||||
|
||||
override fun getItemId(position: Int) = data[position].id as Long
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val v = inflater.inflate(R.layout.message_row, parent, false)
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = data[position]
|
||||
|
||||
holder.apply {
|
||||
if (activateOnItemClick) {
|
||||
container.setBackgroundResource(
|
||||
if (position == selectedPosition)
|
||||
R.drawable.bg_item_selected_state
|
||||
else
|
||||
R.drawable.bg_item_normal_state
|
||||
)
|
||||
}
|
||||
|
||||
// set listeners
|
||||
// (if the item is *pinned*, click event comes to the itemView)
|
||||
itemView.setOnClickListener(itemViewOnClickListener)
|
||||
// (if the item is *not pinned*, click event comes to the container)
|
||||
container.setOnClickListener(swipeableViewContainerOnClickListener)
|
||||
|
||||
// set data
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
status.setImageResource(Assets.getStatusDrawable(item.status))
|
||||
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
|
||||
sender.text = item.from.toString()
|
||||
subject.text = prepareMessageExtract(item.subject)
|
||||
extract.text = prepareMessageExtract(item.text)
|
||||
if (item.isUnread()) {
|
||||
sender.typeface = Typeface.DEFAULT_BOLD
|
||||
subject.typeface = Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
sender.typeface = Typeface.DEFAULT
|
||||
subject.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
|
||||
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
||||
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
|
||||
} else {
|
||||
REACTION_CAN_SWIPE_BOTH_H
|
||||
}
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
|
||||
holder.itemView.setBackgroundResource(when (type) {
|
||||
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
|
||||
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
|
||||
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
||||
R.drawable.bg_swipe_item_neutral
|
||||
} else {
|
||||
R.drawable.bg_swipe_item_right
|
||||
}
|
||||
else -> R.drawable.bg_swipe_item_neutral
|
||||
})
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
|
||||
when (result) {
|
||||
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
|
||||
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
|
||||
|
||||
fun setSelectedPosition(selectedPosition: Int) {
|
||||
val oldPosition = this.selectedPosition
|
||||
this.selectedPosition = selectedPosition
|
||||
notifyItemChanged(oldPosition)
|
||||
notifyItemChanged(selectedPosition)
|
||||
}
|
||||
|
||||
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
|
||||
private var adapter: SwipeableMessageAdapter? = adapter
|
||||
private val item = adapter.data[position]
|
||||
|
||||
override fun onPerformAction() {
|
||||
adapter?.eventListener?.onItemDeleted(item)
|
||||
}
|
||||
|
||||
override fun onCleanUp() {
|
||||
adapter = null
|
||||
}
|
||||
}
|
||||
|
||||
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
|
||||
private var adapter: SwipeableMessageAdapter? = adapter
|
||||
private val item = adapter.data[position]
|
||||
|
||||
override fun onPerformAction() {
|
||||
adapter?.eventListener?.onItemArchived(item)
|
||||
}
|
||||
|
||||
override fun onCleanUp() {
|
||||
adapter = null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
|
||||
/**
|
||||
* Switches between two [ProofOfWorkEngine]s depending on the configuration.
|
||||
*
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class SwitchingProofOfWorkEngine(
|
||||
private val ctx: Context,
|
||||
private val preference: String,
|
||||
private val option: ProofOfWorkEngine,
|
||||
private val fallback: ProofOfWorkEngine
|
||||
) : ProofOfWorkEngine, InternalContext.ContextHolder {
|
||||
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
if (preferences.getBoolean(preference, false)) {
|
||||
option.calculateNonce(initialHash, target, callback)
|
||||
} else {
|
||||
fallback.calculateNonce(initialHash, target, callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContext(context: InternalContext) = listOf(option, fallback)
|
||||
.filterIsInstance<InternalContext.ContextHolder>()
|
||||
.forEach { it.setContext(context) }
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.ImportIdentityActivity
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.support.v4.startActivity
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class AddIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
private var parent: ViewGroup? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_identity)
|
||||
parent = container
|
||||
return inflater.inflate(R.layout.dialog_add_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
val ctx = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
|
||||
when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.create_identity -> {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.import_identity -> startActivity<ImportIdentityActivity>()
|
||||
R.id.add_chan -> addChanDialog()
|
||||
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
private fun addChanDialog() {
|
||||
val activity = activity ?: throw IllegalStateException("No activity available")
|
||||
val ctx = activity.baseContext ?: throw IllegalStateException("No context available")
|
||||
val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent)
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.add_chan)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val passphrase = dialogView.findViewById<TextView>(R.id.passphrase)
|
||||
Toast.makeText(ctx, R.string.toast_long_running_operation,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val pass = passphrase.text.toString()
|
||||
doAsync {
|
||||
val chan = bmc.createChan(pass)
|
||||
chan.alias = pass
|
||||
bmc.addresses.save(chan)
|
||||
uiThread {
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_chan_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply { addIdentityEntry(chan) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class DeterministicIdentityDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
bmc = Singleton.getBitmessageContext(context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
dialog.setTitle(R.string.add_deterministic_address)
|
||||
return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(dialogView, savedInstanceState)
|
||||
ok.setOnClickListener {
|
||||
dismiss()
|
||||
val context = activity?.baseContext ?: throw IllegalStateException("No context available")
|
||||
val passphraseText = passphrase.text.toString()
|
||||
|
||||
Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show()
|
||||
doAsync {
|
||||
val identities = bmc.createDeterministicAddresses(
|
||||
passphraseText,
|
||||
number_of_identities.text.toString().toInt(),
|
||||
Pubkey.LATEST_VERSION,
|
||||
1L,
|
||||
shorter.isChecked
|
||||
)
|
||||
for ((i, identity) in identities.withIndex()) {
|
||||
if (identities.size == 1) {
|
||||
identity.alias = label.text.toString()
|
||||
} else {
|
||||
identity.alias = "${label.text} (${i + 1})"
|
||||
}
|
||||
bmc.addresses.save(identity)
|
||||
}
|
||||
uiThread {
|
||||
val messageRes = if (identities.size == 1) {
|
||||
R.string.toast_identity_created
|
||||
} else {
|
||||
R.string.toast_identities_created
|
||||
}
|
||||
Toast.makeText(context,
|
||||
messageRes,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply {
|
||||
identities.forEach { identity ->
|
||||
addIdentityEntry(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
override fun getTheme() = R.style.FixedDialog
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import kotlinx.android.synthetic.main.dialog_full_node.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class FullNodeDialogActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.dialog_full_node)
|
||||
ok.setOnClickListener {
|
||||
Preferences.setWifiOnly(this@FullNodeDialogActivity, false)
|
||||
NetworkUtils.enableNode(applicationContext)
|
||||
finish()
|
||||
}
|
||||
dismiss.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
NetworkUtils.scheduleNodeStart(applicationContext)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.dialog
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
|
||||
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
|
||||
import kotlinx.android.synthetic.main.dialog_select_message_encoding.*
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class SelectEncodingDialogFragment : AppCompatDialogFragment() {
|
||||
private lateinit var encoding: Plaintext.Encoding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE
|
||||
dialog.setTitle(R.string.select_encoding_title)
|
||||
return inflater.inflate(R.layout.dialog_select_message_encoding, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
when (encoding) {
|
||||
SIMPLE -> radioGroup.check(R.id.simple)
|
||||
EXTENDED -> radioGroup.check(R.id.extended)
|
||||
else -> LOG.warn("Unexpected encoding: " + encoding)
|
||||
}
|
||||
ok.setOnClickListener(View.OnClickListener {
|
||||
encoding = when (radioGroup.checkedRadioButtonId) {
|
||||
R.id.extended -> EXTENDED
|
||||
R.id.simple -> SIMPLE
|
||||
else -> {
|
||||
dismiss()
|
||||
return@OnClickListener
|
||||
}
|
||||
}
|
||||
val result = Intent()
|
||||
result.putExtra(EXTRA_ENCODING, encoding)
|
||||
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result)
|
||||
dismiss()
|
||||
})
|
||||
dismiss.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener {
|
||||
|
||||
override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
if (current) {
|
||||
// Show QR code in modal dialog
|
||||
val dialog = Dialog(ctx)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
val imageView = ImageView(ctx)
|
||||
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx)))
|
||||
imageView.setOnClickListener { dialog.dismiss() }
|
||||
dialog.addContentView(
|
||||
imageView,
|
||||
RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
val display = window.windowManager.defaultDisplay
|
||||
val size = Point()
|
||||
display.getSize(size)
|
||||
val dim = if (size.x < size.y) size.x else size.y
|
||||
|
||||
val lp = WindowManager.LayoutParams()
|
||||
lp.copyFrom(window.attributes)
|
||||
lp.width = dim
|
||||
lp.height = dim
|
||||
|
||||
window.attributes = lp
|
||||
}
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package ch.dissem.apps.abit.drawer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
import ch.dissem.apps.abit.AddressDetailActivity
|
||||
import ch.dissem.apps.abit.AddressDetailFragment
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
|
||||
class ProfileSelectionListener(
|
||||
private val ctx: Context,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : AccountHeader.OnAccountHeaderListener {
|
||||
|
||||
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
|
||||
when (profile.identifier.toInt()) {
|
||||
MainActivity.ADD_IDENTITY -> addIdentityDialog()
|
||||
MainActivity.MANAGE_IDENTITY -> {
|
||||
val identity = Singleton.getIdentity(ctx)
|
||||
if (identity == null) {
|
||||
Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show()
|
||||
} else {
|
||||
val show = Intent(ctx, AddressDetailActivity::class.java)
|
||||
show.putExtra(AddressDetailFragment.ARG_ITEM, identity)
|
||||
ctx.startActivity(show)
|
||||
}
|
||||
}
|
||||
else -> if (profile is ProfileDrawerItem) {
|
||||
val tag = profile.tag
|
||||
if (tag is BitmessageAddress) {
|
||||
Singleton.setIdentity(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
// false if it should close the drawer
|
||||
return false
|
||||
}
|
||||
|
||||
private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog")
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
/**
|
||||
* A callback interface that all activities containing this fragment must
|
||||
* implement. This mechanism allows activities to be notified of item
|
||||
* selections.
|
||||
*/
|
||||
interface ListSelectionListener<in T> {
|
||||
/**
|
||||
* Callback for when an item has been selected.
|
||||
*/
|
||||
fun onItemSelected(item: T)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
import android.content.Context
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.notification.NewMessageNotification
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||
*
|
||||
*
|
||||
* Should show a notification when the app isn't running, but update the message list when it is.
|
||||
* Also,
|
||||
* notifications should be combined.
|
||||
*
|
||||
*/
|
||||
class MessageListener(ctx: Context) : BitmessageContext.Listener {
|
||||
private val unacknowledged = LinkedList<Plaintext>()
|
||||
private var numberOfUnacknowledgedMessages = 0
|
||||
private val notification = NewMessageNotification(ctx)
|
||||
private val pool = Executors.newSingleThreadExecutor()
|
||||
|
||||
override fun receive(plaintext: Plaintext) {
|
||||
pool.submit {
|
||||
unacknowledged.addFirst(plaintext)
|
||||
numberOfUnacknowledgedMessages++
|
||||
if (unacknowledged.size > 5) {
|
||||
unacknowledged.removeLast()
|
||||
}
|
||||
if (numberOfUnacknowledgedMessages == 1) {
|
||||
notification.singleNotification(plaintext)
|
||||
} else {
|
||||
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages)
|
||||
}
|
||||
notification.show()
|
||||
|
||||
// If MainActivity is shown, update the sidebar badges
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
}
|
||||
|
||||
fun resetNotification() {
|
||||
pool.submit {
|
||||
notification.hide()
|
||||
unacknowledged.clear()
|
||||
numberOfUnacknowledgedMessages = 0
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.listener
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import org.jetbrains.anko.connectivityManager
|
||||
|
||||
class WifiReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
|
||||
NetworkUtils.doStartBitmessageService(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.support.annotation.ColorRes
|
||||
import android.support.v4.content.ContextCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import org.jetbrains.anko.notificationManager
|
||||
|
||||
/**
|
||||
* Some base class to create and handle notifications.
|
||||
*/
|
||||
abstract class AbstractNotification(ctx: Context) {
|
||||
protected val ctx = ctx.applicationContext!!
|
||||
private val manager = ctx.notificationManager
|
||||
var notification: Notification? = null
|
||||
protected set
|
||||
protected var showing = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return an id unique to this notification class
|
||||
*/
|
||||
protected abstract val notificationId: Int
|
||||
|
||||
open fun show() {
|
||||
manager.notify(notificationId, notification)
|
||||
showing = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
showing = false
|
||||
manager.cancel(notificationId)
|
||||
}
|
||||
|
||||
protected fun initChannel(channelId: String, @ColorRes color: Int = R.color.colorPrimary) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
ctx.getText(R.string.app_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
lightColor = ContextCompat.getColor(ctx, color)
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
internal const val ONGOING_CHANNEL_ID = "abit.ongoing"
|
||||
internal const val MESSAGE_CHANNEL_ID = "abit.message"
|
||||
internal const val ERROR_CHANNEL_ID = "abit.error"
|
||||
|
||||
init {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.StringRes
|
||||
import android.support.v4.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
|
||||
/**
|
||||
* Easily create notifications with error messages. Use carefully, users probably won't like them.
|
||||
* (But they are useful during development/testing)
|
||||
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ERROR_CHANNEL_ID)
|
||||
.setContentTitle(ctx.getString(R.string.app_name))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
|
||||
init {
|
||||
initChannel(ERROR_CHANNEL_ID, R.color.colorPrimaryDark)
|
||||
}
|
||||
|
||||
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_warning)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
|
||||
builder.setSmallIcon(R.drawable.ic_notification_error)
|
||||
.setContentText(ctx.getString(resId, *args))
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = ERROR_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
const val ERROR_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.BitmessageService
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Shows the network status (as long as the client is connected as a full node)
|
||||
*/
|
||||
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
val showAppIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
|
||||
builder
|
||||
.setSmallIcon(R.drawable.ic_notification_full_node)
|
||||
.setContentTitle(ctx.getString(R.string.bitmessage_full_node))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
.setContentIntent(pendingIntent)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun update(): Boolean {
|
||||
val running = BitmessageService.isRunning
|
||||
builder.setOngoing(running)
|
||||
val connections = BitmessageService.status.getProperty("network", "connections")
|
||||
if (!running) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
|
||||
} else if (connections == null || connections.properties.isEmpty()) {
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
} else {
|
||||
val info = StringBuilder()
|
||||
for (stream in connections.properties) {
|
||||
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
|
||||
val nodeCount = stream.getProperty("nodes")!!.value as Int?
|
||||
if (nodeCount == 1) {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_1,
|
||||
streamNumber
|
||||
)
|
||||
)
|
||||
} else {
|
||||
info.append(
|
||||
ctx.getString(
|
||||
R.string.connection_info_n,
|
||||
streamNumber, nodeCount
|
||||
)
|
||||
)
|
||||
}
|
||||
info.append('\n')
|
||||
}
|
||||
builder.setContentText(info)
|
||||
}
|
||||
builder.mActions.clear()
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
if (running) {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
} else {
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true)
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_start,
|
||||
ctx.getString(R.string.full_node_restart),
|
||||
PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
}
|
||||
notification = builder.build()
|
||||
return running
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
|
||||
if (!update()) {
|
||||
cancel()
|
||||
ctx.stopService(Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
super@NetworkNotification.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun showShutdown() {
|
||||
timer?.cancel()
|
||||
update()
|
||||
super.show()
|
||||
}
|
||||
|
||||
override val notificationId = NETWORK_NOTIFICATION_ID
|
||||
|
||||
fun connecting() {
|
||||
builder.setOngoing(true)
|
||||
builder.setContentText(ctx.getString(R.string.connection_info_pending))
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true)
|
||||
builder.mActions.clear()
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification_node_stop,
|
||||
ctx.getString(R.string.full_node_stop),
|
||||
PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NETWORK_NOTIFICATION_ID = 2
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
||||
import android.support.v4.app.NotificationCompat.InboxStyle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
|
||||
import ch.dissem.apps.abit.util.Drawables.toBitmap
|
||||
|
||||
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
init {
|
||||
initChannel(MESSAGE_CHANNEL_ID, R.color.colorPrimary)
|
||||
}
|
||||
|
||||
fun singleNotification(plaintext: Plaintext): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text)
|
||||
plaintext.subject?.let { subject ->
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(toBitmap(Identicon(plaintext.from), 192))
|
||||
.setContentTitle(plaintext.from.toString())
|
||||
.setContentText(plaintext.subject)
|
||||
.setStyle(BigTextStyle().bigText(bigText))
|
||||
.setContentInfo("Info")
|
||||
|
||||
builder.setContentIntent(
|
||||
createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_reply, ctx.getString(R.string.reply),
|
||||
createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)
|
||||
)
|
||||
builder.addAction(
|
||||
R.drawable.ic_action_delete, ctx.getString(R.string.delete),
|
||||
createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)
|
||||
)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun createActivityIntent(action: String, message: Plaintext): PendingIntent {
|
||||
val intent = Intent(ctx, MainActivity::class.java).putExtra(action, message)
|
||||
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
private fun createServiceIntent(
|
||||
ctx: Context,
|
||||
action: String,
|
||||
message: Plaintext
|
||||
): PendingIntent {
|
||||
val intent = Intent(ctx, BitmessageIntentService::class.java)
|
||||
intent.putExtra(action, message)
|
||||
return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unacknowledged will be accessed from different threads, so make sure wherever it's
|
||||
* * accessed it will be in a `synchronized(unacknowledged)
|
||||
* * {}` block
|
||||
*/
|
||||
fun multiNotification(
|
||||
unacknowledged: Collection<Plaintext>,
|
||||
numberOfUnacknowledgedMessages: Int
|
||||
): NewMessageNotification {
|
||||
val builder = NotificationCompat.Builder(ctx, MESSAGE_CHANNEL_ID)
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
|
||||
.setContentText(ctx.getString(R.string.app_name))
|
||||
|
||||
val inboxStyle = InboxStyle()
|
||||
|
||||
synchronized(unacknowledged) {
|
||||
for (msg in unacknowledged) {
|
||||
val sb = SpannableString(msg.from.toString() + " " + msg.subject)
|
||||
sb.setSpan(
|
||||
SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
|
||||
.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
inboxStyle.addLine(sb)
|
||||
}
|
||||
}
|
||||
builder.setStyle(inboxStyle)
|
||||
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.action = MainActivity.ACTION_SHOW_INBOX
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
|
||||
builder.setContentIntent(pendingIntent)
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
|
||||
|
||||
companion object {
|
||||
private const val NEW_MESSAGE_NOTIFICATION_ID = 1
|
||||
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService
|
||||
import ch.dissem.apps.abit.util.PowStats
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_notification_proof_of_work)
|
||||
.setContentTitle(ctx.getString(R.string.proof_of_work_title))
|
||||
private var startTime = 0L
|
||||
private var progress = 0
|
||||
private var progressMax = 0
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
update(0)
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(numberOfItems: Int): ProofOfWorkNotification {
|
||||
|
||||
val showMessageIntent = Intent(ctx, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentText(if (numberOfItems == 0)
|
||||
ctx.getString(R.string.proof_of_work_text_0)
|
||||
else
|
||||
ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
notification = builder.build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun start(item: ProofOfWorkService.PowItem) {
|
||||
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
|
||||
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
|
||||
startTime = System.currentTimeMillis()
|
||||
progress = 0
|
||||
progressMax = delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
|
||||
timer = fixedRateTimer(initialDelay = 2000, period = 2000){
|
||||
val elapsedTime = System.currentTimeMillis() - startTime
|
||||
progress = elapsedTime.toInt()
|
||||
progressMax = progress + delta
|
||||
builder.setProgress(progressMax, progress, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished() {
|
||||
timer?.cancel()
|
||||
progress = 0
|
||||
progressMax = 0
|
||||
if (showing) {
|
||||
builder.setProgress(0, 0, false)
|
||||
notification = builder.build()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.pow
|
||||
|
||||
import android.content.Context
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.synchronization.SyncAdapter
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder {
|
||||
private lateinit var context: InternalContext
|
||||
|
||||
private val pool: ExecutorService
|
||||
|
||||
init {
|
||||
pool = Executors.newCachedThreadPool { r ->
|
||||
val thread = Executors.defaultThreadFactory().newThread(r)
|
||||
thread.priority = Thread.MIN_PRIORITY
|
||||
thread
|
||||
}
|
||||
}
|
||||
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) =
|
||||
pool.execute {
|
||||
val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW")
|
||||
|
||||
val request = ProofOfWorkRequest(identity, initialHash,
|
||||
CALCULATE, target)
|
||||
SyncAdapter.startPowSync(ctx)
|
||||
try {
|
||||
val cryptoMsg = CryptoCustomMessage(request)
|
||||
cryptoMsg.signAndEncrypt(
|
||||
identity,
|
||||
cryptography().createPublicKey(identity.publicDecryptionKey)
|
||||
)
|
||||
val node = Preferences.getTrustedNode(ctx)
|
||||
if (node == null) {
|
||||
LOG.error("trusted node is not defined")
|
||||
} else {
|
||||
context.networkHandler.send(
|
||||
node,
|
||||
Preferences.getTrustedNodePort(ctx),
|
||||
cryptoMsg)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java)
|
||||
}
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.AddressRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [AddressRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
|
||||
|
||||
override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag)
|
||||
|
||||
override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag)
|
||||
|
||||
private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? {
|
||||
for (address in find(where)) {
|
||||
if (address.version > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.tag)) return address
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.ripe)) return address
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIdentities() = find("private_key IS NOT NULL")
|
||||
|
||||
override fun getChans() = find("chan = '1'")
|
||||
|
||||
override fun getSubscriptions() = find("subscribed = '1'")
|
||||
|
||||
override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) {
|
||||
find("subscribed = '1' AND version > 3")
|
||||
} else {
|
||||
find("subscribed = '1' AND version <= 3")
|
||||
}
|
||||
|
||||
override fun getContacts() = find("private_key IS NULL OR chan = '1'")
|
||||
|
||||
/**
|
||||
* Returns the contacts in the following order:
|
||||
*
|
||||
* * Subscribed addresses come first
|
||||
* * Addresses with aliases (alphabetically)
|
||||
* * Addresses without aliases are omitted
|
||||
*
|
||||
*
|
||||
* @return the ordered list of ids (address strings)
|
||||
*/
|
||||
fun getContactIds(): List<String> = findIds(
|
||||
"($COLUMN_PRIVATE_KEY IS NULL OR $COLUMN_CHAN = '1') AND $COLUMN_ALIAS IS NOT NULL",
|
||||
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS, $COLUMN_ADDRESS"
|
||||
)
|
||||
|
||||
private fun findIds(where: String, orderBy: String): List<String> {
|
||||
val result = LinkedList<String>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
orderBy
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS)))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun find(where: String): List<BitmessageAddress> {
|
||||
val result = LinkedList<BitmessageAddress>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getAddress(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getAddress(c: Cursor): BitmessageAddress {
|
||||
|
||||
fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let {
|
||||
BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it)))
|
||||
}
|
||||
|
||||
fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address ->
|
||||
c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes ->
|
||||
Factory.readPubkey(
|
||||
version = address.version, stream = address.stream,
|
||||
input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size,
|
||||
encrypted = false
|
||||
).let {
|
||||
address.pubkey = if (address.version == 4L && it is V3Pubkey) {
|
||||
V4Pubkey(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (getIdentity(c) ?: getContact(c)).apply {
|
||||
alias = c.getString(c.getColumnIndex(COLUMN_ALIAS))
|
||||
isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1
|
||||
isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(address: BitmessageAddress) = if (exists(address)) {
|
||||
update(address)
|
||||
} else {
|
||||
insert(address)
|
||||
}
|
||||
|
||||
private fun exists(address: BitmessageAddress): Boolean {
|
||||
sql.readableDatabase.rawQuery(
|
||||
"SELECT COUNT(*) FROM Address WHERE address=?",
|
||||
arrayOf(address.address)
|
||||
).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address)
|
||||
|
||||
val update = sql.writableDatabase.update(TABLE_NAME, values, "address=?", arrayOf(address.address))
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(address: BitmessageAddress) {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = getContentValues(address).apply {
|
||||
put(COLUMN_ADDRESS, address.address)
|
||||
put(COLUMN_VERSION, address.version)
|
||||
put(COLUMN_CHAN, address.isChan)
|
||||
}
|
||||
|
||||
val insert = sql.writableDatabase.insert(TABLE_NAME, null, values)
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address {}", address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentValues(address: BitmessageAddress) = ContentValues().apply {
|
||||
address.alias?.let { put(COLUMN_ALIAS, it) }
|
||||
address.pubkey?.let { pubkey ->
|
||||
val out = ByteArrayOutputStream()
|
||||
pubkey.writer().writeUnencrypted(out)
|
||||
put(COLUMN_PUBLIC_KEY, out.toByteArray())
|
||||
}
|
||||
address.privateKey?.let { put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
|
||||
if (address.isChan) {
|
||||
put(COLUMN_CHAN, true)
|
||||
}
|
||||
put(COLUMN_SUBSCRIBED, address.isSubscribed)
|
||||
}
|
||||
|
||||
override fun remove(address: BitmessageAddress) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
|
||||
}
|
||||
|
||||
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Address"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_ALIAS = "alias"
|
||||
private const val COLUMN_PUBLIC_KEY = "public_key"
|
||||
private const val COLUMN_PRIVATE_KEY = "private_key"
|
||||
private const val COLUMN_SUBSCRIBED = "subscribed"
|
||||
private const val COLUMN_CHAN = "chan"
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.Inventory
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* [Inventory] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidInventory(private val sql: SqlHelper) : Inventory {
|
||||
|
||||
private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>()
|
||||
|
||||
override fun getInventory(vararg streams: Long): List<InventoryVector> {
|
||||
val result = LinkedList<InventoryVector>()
|
||||
val now = now
|
||||
for (stream in streams) {
|
||||
for ((key, value) in getCache(stream)) {
|
||||
if (value > now) {
|
||||
result.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
|
||||
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
|
||||
cache[stream] = result
|
||||
|
||||
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"stream = $stream", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH))
|
||||
val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES))
|
||||
InventoryVector.fromHash(blob)?.let { result.put(it, expires) }
|
||||
}
|
||||
}
|
||||
LOG.info("Stream #$stream inventory size: ${result.size}")
|
||||
return result
|
||||
}
|
||||
return cache[stream] ?: synchronized(cache) {
|
||||
return@synchronized cache[stream] ?: addToCache(stream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys }
|
||||
|
||||
override fun getObject(vector: InventoryVector): ObjectMessage? {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'$vector'", null, null, null, null
|
||||
).use { c ->
|
||||
if (!c.moveToFirst()) {
|
||||
LOG.info("Object requested that we don't have. IV: {}", vector)
|
||||
return null
|
||||
}
|
||||
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
|
||||
val where = StringBuilder("1=1")
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream)
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version)
|
||||
}
|
||||
if (types.isNotEmpty()) {
|
||||
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
|
||||
}
|
||||
|
||||
val result = LinkedList<ObjectMessage>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(), null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun storeObject(objectMessage: ObjectMessage) {
|
||||
val iv = objectMessage.inventoryVector
|
||||
|
||||
if (getCache(objectMessage.stream).containsKey(iv))
|
||||
return
|
||||
|
||||
LOG.trace("Storing object {}", iv)
|
||||
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_HASH, objectMessage.inventoryVector.hash)
|
||||
put(COLUMN_STREAM, objectMessage.stream)
|
||||
put(COLUMN_EXPIRES, objectMessage.expiresTime)
|
||||
put(COLUMN_DATA, Encode.bytes(objectMessage))
|
||||
put(COLUMN_TYPE, objectMessage.type)
|
||||
put(COLUMN_VERSION, objectMessage.version)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
|
||||
getCache(objectMessage.stream)[iv] = objectMessage.expiresTime
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector)
|
||||
|
||||
override fun cleanup() {
|
||||
val fiveMinutesAgo = now - 5 * MINUTE
|
||||
sql.writableDatabase.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
|
||||
|
||||
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Inventory"
|
||||
private const val COLUMN_HASH = "hash"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_EXPIRES = "expires"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import ch.dissem.apps.abit.util.Labels
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractLabelRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) : AbstractLabelRepository() {
|
||||
|
||||
override fun find(where: String): List<Label> {
|
||||
val result = LinkedList<Label>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
COLUMN_ORDER
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getLabel(c, context))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun save(label: Label) {
|
||||
val db = sql.writableDatabase
|
||||
if (label.id != null) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
|
||||
} else {
|
||||
db.transaction {
|
||||
val exists = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "label=?", arrayOf(label.toString())) > 0
|
||||
|
||||
if (exists) {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString()))
|
||||
} else {
|
||||
val values = ContentValues()
|
||||
values.put(COLUMN_LABEL, label.toString())
|
||||
values.put(COLUMN_TYPE, label.type?.name)
|
||||
values.put(COLUMN_COLOR, label.color)
|
||||
values.put(COLUMN_ORDER, label.ord)
|
||||
db.insertOrThrow(TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
|
||||
companion object {
|
||||
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
|
||||
|
||||
private const val TABLE_NAME = "Label"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_LABEL = "label"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_COLOR = "color"
|
||||
private const val COLUMN_ORDER = "ord"
|
||||
|
||||
internal fun getLabel(c: Cursor, context: Context): Label {
|
||||
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
|
||||
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
|
||||
val text: String? = Labels.getText(type, null, context)
|
||||
val label = Label(
|
||||
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(COLUMN_COLOR)))
|
||||
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
@ -1,260 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import ch.dissem.apps.abit.util.UuidUtils.asUuid
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractMessageRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
|
||||
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
|
||||
super.findMessages(null as Label?, offset, limit)
|
||||
} else {
|
||||
super.findMessages(label, offset, limit)
|
||||
}
|
||||
|
||||
override fun countUnread(label: Label?) = when {
|
||||
label === LABEL_ARCHIVE -> 0
|
||||
label == null -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
|
||||
arrayOf(Label.Type.UNREAD.name)
|
||||
).toInt()
|
||||
else -> DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
|
||||
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
|
||||
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
|
||||
).toInt()
|
||||
}
|
||||
|
||||
override fun findConversations(label: Label?): List<UUID> {
|
||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
||||
|
||||
val where = when {
|
||||
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
|
||||
label == null -> null
|
||||
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
|
||||
}
|
||||
val result = LinkedList<UUID>()
|
||||
sql.readableDatabase.query(
|
||||
true,
|
||||
TABLE_NAME, projection, where,
|
||||
null, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
||||
result.add(asUuid(uuidBytes))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
|
||||
val inventoryVector = message.inventoryVector
|
||||
if (inventoryVector == null || message.parents.isEmpty()) {
|
||||
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
|
||||
return
|
||||
}
|
||||
val childIV = inventoryVector.hash
|
||||
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
|
||||
|
||||
// save new parents
|
||||
var order = 0
|
||||
val values = ContentValues()
|
||||
for (parentIV in message.parents) {
|
||||
getMessage(parentIV)?.let { parent ->
|
||||
mergeConversations(db, parent.conversationId, message.conversationId)
|
||||
order++
|
||||
values.put("parent", parentIV.hash)
|
||||
values.put("child", childIV)
|
||||
values.put("pos", order)
|
||||
values.put("conversation", UuidUtils.asBytes(message.conversationId))
|
||||
db.insertOrThrow(PARENTS_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every occurrence of the source conversation ID with the target ID
|
||||
|
||||
* @param db is used to keep everything within one transaction
|
||||
* *
|
||||
* @param source ID of the conversation to be merged
|
||||
* *
|
||||
* @param target ID of the merge target
|
||||
*/
|
||||
private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
|
||||
val values = ContentValues()
|
||||
values.put("conversation", UuidUtils.asBytes(target))
|
||||
val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
|
||||
db.update(TABLE_NAME, values, where, null)
|
||||
db.update(PARENTS_TABLE_NAME, values, where, null)
|
||||
}
|
||||
|
||||
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> {
|
||||
val result = LinkedList<Plaintext>()
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ID, COLUMN_IV, COLUMN_TYPE, COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, COLUMN_NEXT_TRY, COLUMN_CONVERSATION)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
where, null, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature(
|
||||
Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))),
|
||||
ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA)))
|
||||
).build {
|
||||
id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV)))
|
||||
c.getString(c.getColumnIndex(COLUMN_SENDER))?.let {
|
||||
from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let {
|
||||
to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
|
||||
}
|
||||
ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))
|
||||
sent = c.getLong(c.getColumnIndex(COLUMN_SENT))
|
||||
received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED))
|
||||
status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))
|
||||
ttl = c.getLong(c.getColumnIndex(COLUMN_TTL))
|
||||
retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES))
|
||||
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
|
||||
if (!c.isNull(nextTryColumn)) {
|
||||
nextTry = c.getLong(nextTryColumn)
|
||||
}
|
||||
conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))
|
||||
labels = findLabels(id!!)
|
||||
}
|
||||
|
||||
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||
|
||||
override fun save(message: Plaintext) {
|
||||
saveContactIfNecessary(message.from)
|
||||
saveContactIfNecessary(message.to)
|
||||
val db = sql.writableDatabase
|
||||
db.transaction {
|
||||
// save message
|
||||
if (message.id == null) {
|
||||
insert(db, message)
|
||||
} else {
|
||||
update(db, message)
|
||||
}
|
||||
|
||||
updateParents(db, message)
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
|
||||
|
||||
// save labels
|
||||
val values = ContentValues()
|
||||
for (label in message.labels) {
|
||||
values.put(JT_COLUMN_LABEL, label.id as Long?)
|
||||
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValues(message: Plaintext) = ContentValues(14).apply {
|
||||
put(COLUMN_IV, message.inventoryVector?.hash)
|
||||
put(COLUMN_TYPE, message.type.name)
|
||||
put(COLUMN_SENDER, message.from.address)
|
||||
put(COLUMN_RECIPIENT, message.to?.address)
|
||||
put(COLUMN_DATA, Encode.bytes(message))
|
||||
put(COLUMN_ACK_DATA, message.ackData)
|
||||
put(COLUMN_SENT, message.sent)
|
||||
put(COLUMN_RECEIVED, message.received)
|
||||
put(COLUMN_STATUS, message.status.name)
|
||||
put(COLUMN_INITIAL_HASH, message.initialHash)
|
||||
put(COLUMN_TTL, message.ttl)
|
||||
put(COLUMN_RETRIES, message.retries)
|
||||
put(COLUMN_NEXT_TRY, message.nextTry)
|
||||
put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
|
||||
}
|
||||
|
||||
private fun insert(db: SQLiteDatabase, message: Plaintext) {
|
||||
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
|
||||
message.id = id
|
||||
}
|
||||
|
||||
private fun update(db: SQLiteDatabase, message: Plaintext) {
|
||||
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
override fun remove(message: Plaintext) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "Message"
|
||||
private const val COLUMN_ID = "id"
|
||||
private const val COLUMN_IV = "iv"
|
||||
private const val COLUMN_TYPE = "type"
|
||||
private const val COLUMN_SENDER = "sender"
|
||||
private const val COLUMN_RECIPIENT = "recipient"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_ACK_DATA = "ack_data"
|
||||
private const val COLUMN_SENT = "sent"
|
||||
private const val COLUMN_RECEIVED = "received"
|
||||
private const val COLUMN_STATUS = "status"
|
||||
private const val COLUMN_TTL = "ttl"
|
||||
private const val COLUMN_RETRIES = "retries"
|
||||
private const val COLUMN_NEXT_TRY = "next_try"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_CONVERSATION = "conversation"
|
||||
|
||||
private const val PARENTS_TABLE_NAME = "Message_Parent"
|
||||
|
||||
private const val JOIN_TABLE_NAME = "Message_Label"
|
||||
private const val JT_COLUMN_MESSAGE = "message_id"
|
||||
private const val JT_COLUMN_LABEL = "label_id"
|
||||
}
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.database.sqlite.SQLiteDoneException
|
||||
import android.database.sqlite.SQLiteStatement
|
||||
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
|
||||
import ch.dissem.bitmessage.ports.NodeRegistry
|
||||
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
|
||||
import ch.dissem.bitmessage.utils.Collections
|
||||
import ch.dissem.bitmessage.utils.SqlStrings
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import ch.dissem.bitmessage.utils.UnixTime.DAY
|
||||
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
||||
import ch.dissem.bitmessage.utils.UnixTime.now
|
||||
import ch.dissem.bitmessage.utils.max
|
||||
import org.jetbrains.anko.db.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import kotlin.concurrent.getOrSet
|
||||
|
||||
const val MAX_ENTRY_AGE = 7 * DAY
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
|
||||
|
||||
private val loadExistingStatement = ThreadLocal<SQLiteStatement>()
|
||||
private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap()
|
||||
get() {
|
||||
if (field.isEmpty())
|
||||
field = loadStableNodes()
|
||||
return field
|
||||
}
|
||||
|
||||
init {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
private fun cleanUp() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString()))
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
sql.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
private fun loadExistingTime(node: NetworkAddress): Long? {
|
||||
val statement: SQLiteStatement = loadExistingStatement.getOrSet {
|
||||
sql.writableDatabase.compileStatement(
|
||||
"SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?"
|
||||
)
|
||||
}
|
||||
statement.bindLong(1, node.stream)
|
||||
statement.bindBlob(2, node.IPv6)
|
||||
statement.bindLong(3, node.port.toLong())
|
||||
return try {
|
||||
statement.simpleQueryForLong()
|
||||
} catch (e: SQLiteDoneException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> {
|
||||
val result = LinkedList<NetworkAddress>()
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME),
|
||||
"stream IN (?)",
|
||||
arrayOf(SqlStrings.join(*streams)), null, null,
|
||||
"time DESC",
|
||||
limit.toString()
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(NetworkAddress(
|
||||
time = c.getLong(c.getColumnIndex(COLUMN_TIME)),
|
||||
stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)),
|
||||
services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)),
|
||||
IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)),
|
||||
port = c.getInt(c.getColumnIndex(COLUMN_PORT))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
streams
|
||||
.asSequence()
|
||||
.mapNotNull { stableNodes[it] }
|
||||
.filterNot { it.isEmpty() }
|
||||
.mapTo(result) { Collections.selectRandom(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun offerAddresses(nodes: List<NetworkAddress>) {
|
||||
sql.writableDatabase.transaction {
|
||||
cleanUp()
|
||||
nodes
|
||||
.filter {
|
||||
// Don't accept nodes from the future, it might be a trap
|
||||
it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE
|
||||
}
|
||||
.forEach { node ->
|
||||
synchronized(this) {
|
||||
val existing = loadExistingTime(node)
|
||||
if (existing == null) {
|
||||
insert(node)
|
||||
} else if (node.time > existing) {
|
||||
update(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(node: NetworkAddress) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_STREAM, node.stream)
|
||||
put(COLUMN_ADDRESS, node.IPv6)
|
||||
put(COLUMN_PORT, node.port)
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME,
|
||||
if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(node: NetworkAddress) {
|
||||
try {
|
||||
val time = if (node.time > UnixTime.now) {
|
||||
// This might be an attack, let's not use those nodes with priority
|
||||
UnixTime.now - 7 * UnixTime.DAY
|
||||
} else {
|
||||
node.time
|
||||
}
|
||||
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_SERVICES, node.services)
|
||||
put(COLUMN_TIME, max(node.time, time))
|
||||
}
|
||||
|
||||
sql.writableDatabase.update(
|
||||
TABLE_NAME,
|
||||
values,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(node: NetworkAddress) {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
sql.writableDatabase.delete(
|
||||
TABLE_NAME,
|
||||
"time<${UnixTime.now - 8 * DAY}",
|
||||
null
|
||||
)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
|
||||
|
||||
private const val TABLE_NAME = "Node"
|
||||
private const val COLUMN_STREAM = "stream"
|
||||
private const val COLUMN_ADDRESS = "address"
|
||||
private const val COLUMN_PORT = "port"
|
||||
private const val COLUMN_SERVICES = "services"
|
||||
private const val COLUMN_TIME = "time"
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.LinkedList
|
||||
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.factory.Factory
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository
|
||||
import ch.dissem.bitmessage.utils.Encode
|
||||
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import ch.dissem.bitmessage.utils.Strings.hex
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder {
|
||||
private lateinit var bmc: InternalContext
|
||||
|
||||
override fun setContext(context: InternalContext) {
|
||||
this.bmc = context
|
||||
}
|
||||
|
||||
override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"initial_hash=X'${hex(initialHash)}'",
|
||||
null, null, null, null
|
||||
).use { c ->
|
||||
if (c.moveToFirst()) {
|
||||
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
|
||||
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
|
||||
return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
|
||||
)
|
||||
} else {
|
||||
ProofOfWorkRepository.Item(
|
||||
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
|
||||
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
|
||||
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
|
||||
bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}")
|
||||
}
|
||||
|
||||
override fun getItems(): List<ByteArray> {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_INITIAL_HASH)
|
||||
|
||||
val result = LinkedList<ByteArray>()
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection, null, null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH))
|
||||
result.add(initialHash)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun putObject(item: ProofOfWorkRepository.Item) {
|
||||
try {
|
||||
// Create a new map of values, where column names are the keys
|
||||
val values = ContentValues().apply {
|
||||
put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
|
||||
put(COLUMN_DATA, Encode.bytes(item.objectMessage))
|
||||
put(COLUMN_VERSION, item.objectMessage.version)
|
||||
put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
|
||||
put(COLUMN_EXTRA_BYTES, item.extraBytes)
|
||||
item.message?.let { message ->
|
||||
put(COLUMN_EXPIRATION_TIME, item.expirationTime)
|
||||
put(COLUMN_MESSAGE_ID, message.id as Long?)
|
||||
}
|
||||
}
|
||||
|
||||
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
LOG.trace(e.message, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) =
|
||||
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
|
||||
|
||||
override fun removeObject(initialHash: ByteArray) {
|
||||
sql.writableDatabase.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
|
||||
|
||||
private const val TABLE_NAME = "POW"
|
||||
private const val COLUMN_INITIAL_HASH = "initial_hash"
|
||||
private const val COLUMN_DATA = "data"
|
||||
private const val COLUMN_VERSION = "version"
|
||||
private const val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
|
||||
private const val COLUMN_EXTRA_BYTES = "extra_bytes"
|
||||
private const val COLUMN_EXPIRATION_TIME = "expiration_time"
|
||||
private const val COLUMN_MESSAGE_ID = "message_id"
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repository
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.UuidUtils
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION)
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf(
|
||||
0 to {
|
||||
executeMigration(db, "V1.0__Create_table_inventory")
|
||||
executeMigration(db, "V1.1__Create_table_address")
|
||||
executeMigration(db, "V1.2__Create_table_message")
|
||||
},
|
||||
1 to {
|
||||
// executeMigration(db, "V2.0__Update_table_message");
|
||||
executeMigration(db, "V2.1__Create_table_POW")
|
||||
},
|
||||
2 to {
|
||||
executeMigration(db, "V3.0__Update_table_address")
|
||||
},
|
||||
3 to {
|
||||
executeMigration(db, "V3.1__Update_table_POW")
|
||||
executeMigration(db, "V3.2__Update_table_message")
|
||||
},
|
||||
4 to {
|
||||
executeMigration(db, "V3.3__Create_table_node")
|
||||
},
|
||||
5 to {
|
||||
executeMigration(db, "V3.4__Add_label_outbox")
|
||||
},
|
||||
6 to {
|
||||
executeMigration(db, "V4.0__Create_table_message_parent")
|
||||
},
|
||||
7 to {
|
||||
setMissingConversationIds(db)
|
||||
}
|
||||
).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() }
|
||||
|
||||
/**
|
||||
* Set UUIDs for all messages that have no conversation ID
|
||||
*/
|
||||
private fun setMissingConversationIds(db: SQLiteDatabase) = db.query(
|
||||
"Message", arrayOf("id"),
|
||||
"conversation IS NULL", null, null, null, null
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(0)
|
||||
setMissingConversationId(id, db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
|
||||
val values = ContentValues(1).apply {
|
||||
put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
|
||||
}
|
||||
db.update("Message", values, "id=?", arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
private fun executeMigration(db: SQLiteDatabase, name: String) {
|
||||
for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) {
|
||||
db.execSQL(statement)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
private const val DATABASE_VERSION = 7
|
||||
const val DATABASE_NAME = "jabit.db"
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
|
||||
class BitmessageIntentService : IntentService("BitmessageIntentService") {
|
||||
|
||||
private lateinit var bmc: BitmessageContext
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
bmc = Singleton.getBitmessageContext(this)
|
||||
}
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
intent?.let {
|
||||
if (it.hasExtra(EXTRA_DELETE_MESSAGE)) {
|
||||
val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext
|
||||
bmc.labeler.delete(item)
|
||||
bmc.messages.save(item)
|
||||
Singleton.getMessageListener(this).resetNotification()
|
||||
}
|
||||
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
|
||||
NetworkUtils.enableNode(this)
|
||||
}
|
||||
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
|
||||
NetworkUtils.disableNode(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"
|
||||
const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"
|
||||
const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Handler
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification
|
||||
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.utils.Property
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
* sync adapter class, allowing the sync adapter framework to call
|
||||
* onPerformSync().
|
||||
*/
|
||||
class BitmessageService : Service() {
|
||||
|
||||
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
|
||||
private lateinit var notification: NetworkNotification
|
||||
|
||||
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (bmc.isRunning() && !Preferences.isConnectionAllowed(this@BitmessageService)) {
|
||||
bmc.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cleanupHandler = Handler()
|
||||
private val cleanupTask: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
bmc.cleanup()
|
||||
if (isRunning) {
|
||||
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
registerReceiver(
|
||||
connectivityReceiver,
|
||||
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
)
|
||||
notification = NetworkNotification(this)
|
||||
running = false
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (!isRunning) {
|
||||
running = true
|
||||
notification.connecting()
|
||||
startForeground(NETWORK_NOTIFICATION_ID, notification.notification)
|
||||
if (!bmc.isRunning()) {
|
||||
bmc.startup()
|
||||
}
|
||||
notification.show()
|
||||
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L)
|
||||
}
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (bmc.isRunning()) {
|
||||
bmc.shutdown()
|
||||
}
|
||||
running = false
|
||||
notification.showShutdown()
|
||||
cleanupHandler.removeCallbacks(cleanupTask)
|
||||
bmc.cleanup()
|
||||
unregisterReceiver(connectivityReceiver)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = null
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = running && Singleton.bitmessageContext?.isRunning() == true
|
||||
|
||||
val status: Property
|
||||
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.support.v4.content.ContextCompat
|
||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
|
||||
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
|
||||
import ch.dissem.apps.abit.util.PowStats
|
||||
import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be
|
||||
* killed by the system before the nonce is found.
|
||||
*/
|
||||
class ProofOfWorkService : Service() {
|
||||
private lateinit var notification: ProofOfWorkNotification
|
||||
|
||||
override fun onCreate() {
|
||||
notification = ProofOfWorkNotification(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = PowBinder(this)
|
||||
|
||||
class PowBinder internal constructor(private val service: ProofOfWorkService) : Binder() {
|
||||
private val notification = service.notification
|
||||
|
||||
fun process(item: PowItem) = synchronized(queue) {
|
||||
ContextCompat.startForegroundService(
|
||||
service,
|
||||
Intent(service, ProofOfWorkService::class.java)
|
||||
)
|
||||
service.startForeground(
|
||||
ONGOING_NOTIFICATION_ID,
|
||||
notification.notification
|
||||
)
|
||||
if (!calculating) {
|
||||
calculating = true
|
||||
service.calculateNonce(item)
|
||||
} else {
|
||||
queue.add(item)
|
||||
notification.update(queue.size).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class PowItem(
|
||||
val initialHash: ByteArray,
|
||||
val targetValue: ByteArray,
|
||||
val callback: ProofOfWorkEngine.Callback
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PowItem
|
||||
|
||||
if (!Arrays.equals(initialHash, other.initialHash)) return false
|
||||
if (!Arrays.equals(targetValue, other.targetValue)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = Arrays.hashCode(initialHash)
|
||||
result = 31 * result + Arrays.hashCode(targetValue)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateNonce(item: PowItem) {
|
||||
notification.start(item)
|
||||
val startTime = System.currentTimeMillis()
|
||||
engine.calculateNonce(
|
||||
item.initialHash,
|
||||
item.targetValue,
|
||||
object : ProofOfWorkEngine.Callback {
|
||||
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
|
||||
notification.finished()
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
|
||||
try {
|
||||
item.callback.onNonceCalculated(initialHash, nonce)
|
||||
} finally {
|
||||
var next: PowItem? = null
|
||||
synchronized(queue) {
|
||||
next = queue.poll()
|
||||
if (next == null) {
|
||||
calculating = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
} else {
|
||||
notification.update(queue.size).show()
|
||||
}
|
||||
}
|
||||
next?.let { calculateNonce(it) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Object to use as a thread-safe lock
|
||||
private val engine = MultiThreadedPOWEngine()
|
||||
private val queue = LinkedList<PowItem>()
|
||||
private var calculating: Boolean = false
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder
|
||||
import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem
|
||||
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
|
||||
|
||||
import android.content.Context.BIND_AUTO_CREATE
|
||||
|
||||
/**
|
||||
* Proof of Work engine that uses the Proof of Work service.
|
||||
*/
|
||||
class ServicePowEngine(private val ctx: Context) : ProofOfWorkEngine {
|
||||
private val queue = LinkedList<PowItem>()
|
||||
private var service: PowBinder? = null
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) = synchronized(lock) {
|
||||
this@ServicePowEngine.service = service as PowBinder
|
||||
while (!queue.isEmpty()) {
|
||||
service.process(queue.poll())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
service = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
|
||||
val item = PowItem(initialHash, target, callback)
|
||||
synchronized(lock) {
|
||||
service?.process(item) ?: {
|
||||
queue.add(item)
|
||||
ctx.bindService(Intent(ctx, ProofOfWorkService::class.java), connection, BIND_AUTO_CREATE)
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val lock = Any()
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.adapter.AndroidCryptography
|
||||
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
|
||||
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
|
||||
import ch.dissem.apps.abit.listener.MessageListener
|
||||
import ch.dissem.apps.abit.pow.ServerPowEngine
|
||||
import ch.dissem.apps.abit.repository.*
|
||||
import ch.dissem.apps.abit.util.Constants
|
||||
import ch.dissem.apps.abit.util.Observable
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
|
||||
import ch.dissem.bitmessage.ports.DefaultLabeler
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import ch.dissem.bitmessage.utils.TTL
|
||||
import ch.dissem.bitmessage.utils.UnixTime.DAY
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Provides singleton objects across the application.
|
||||
*/
|
||||
object Singleton {
|
||||
var currentLabel = Observable<Label?>(null)
|
||||
|
||||
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
|
||||
val labeler = DefaultLabeler().apply {
|
||||
listener = { message, added, removed ->
|
||||
MainActivity.apply {
|
||||
runOnUiThread {
|
||||
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
|
||||
currentLabel.value?.let { label ->
|
||||
when {
|
||||
label.type == Label.Type.TRASH
|
||||
&& added.all { it.type == Label.Type.TRASH }
|
||||
&& removed.any { it.type == Label.Type.TRASH } -> {
|
||||
// work-around for messages that are deleted from trash
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
label.type == Label.Type.UNREAD
|
||||
&& added.all { it.type == Label.Type.TRASH } -> {
|
||||
// work-around for messages that are deleted from unread, which already have the unread label removed
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
added.contains(label) -> {
|
||||
// in most cases, top should be the correct position, but time will show if
|
||||
// the message should be properly sorted in
|
||||
swipeableMessageAdapter.addFirst(message)
|
||||
}
|
||||
removed.contains(label) -> {
|
||||
swipeableMessageAdapter.remove(message)
|
||||
}
|
||||
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
|
||||
swipeableMessageAdapter.update(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
|
||||
updateUnread()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var bitmessageContext: BitmessageContext? = null
|
||||
private set
|
||||
private var conversationService: ConversationService? = null
|
||||
private var messageListener: MessageListener? = null
|
||||
private var identity: BitmessageAddress? = null
|
||||
private var powRepo: AndroidProofOfWorkRepository? = null
|
||||
private var creatingIdentity: Boolean = false
|
||||
|
||||
fun getBitmessageContext(context: Context): BitmessageContext =
|
||||
init({ bitmessageContext }, { bitmessageContext = it }) {
|
||||
BitmessageContext.build {
|
||||
TTL.pubkey = 2 * DAY
|
||||
val ctx = context.applicationContext
|
||||
val sqlHelper = SqlHelper(ctx)
|
||||
proofOfWorkEngine = SwitchingProofOfWorkEngine(
|
||||
ctx, Constants.PREFERENCE_SERVER_POW,
|
||||
ServerPowEngine(ctx),
|
||||
ServicePowEngine(ctx)
|
||||
)
|
||||
cryptography = AndroidCryptography()
|
||||
nodeRegistry = AndroidNodeRegistry(sqlHelper)
|
||||
inventory = AndroidInventory(sqlHelper)
|
||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
||||
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
|
||||
messageRepo = AndroidMessageRepository(sqlHelper)
|
||||
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
|
||||
networkHandler = NioNetworkHandler()
|
||||
listener = getMessageListener(ctx)
|
||||
labeler = Singleton.labeler
|
||||
preferences.sendPubkeyOnIdentityCreation = false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMessageListAdapterInListener(adapter: SwipeableMessageAdapter) {
|
||||
swipeableMessageAdapter = WeakReference(adapter)
|
||||
}
|
||||
|
||||
fun getMessageListener(ctx: Context) = init({ messageListener }, { messageListener = it }) { MessageListener(ctx) }
|
||||
|
||||
fun getLabelRepository(ctx: Context) = getBitmessageContext(ctx).labels as AndroidLabelRepository
|
||||
|
||||
fun getMessageRepository(ctx: Context) = getBitmessageContext(ctx).messages as AndroidMessageRepository
|
||||
|
||||
fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository
|
||||
|
||||
fun getProofOfWorkRepository(ctx: Context) = powRepo ?: getBitmessageContext(ctx).internals.proofOfWorkRepository
|
||||
|
||||
fun getIdentity(ctx: Context): BitmessageAddress? =
|
||||
init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc ->
|
||||
val identities = bmc.addresses.getIdentities()
|
||||
if (identities.isNotEmpty()) {
|
||||
identities[0]
|
||||
} else {
|
||||
if (!creatingIdentity) {
|
||||
creatingIdentity = true
|
||||
doAsync {
|
||||
val identity = bmc.createIdentity(false,
|
||||
Pubkey.Feature.DOES_ACK)
|
||||
identity.alias = ctx.getString(R.string.alias_default_identity)
|
||||
bmc.addresses.save(identity)
|
||||
|
||||
uiThread {
|
||||
Singleton.identity = identity
|
||||
Toast.makeText(ctx,
|
||||
R.string.toast_identity_created,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
MainActivity.apply { addIdentityEntry(identity) }
|
||||
}
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setIdentity(identity: BitmessageAddress) {
|
||||
if (identity.privateKey == null)
|
||||
throw IllegalArgumentException("Identity expected, but no private key available")
|
||||
Singleton.identity = identity
|
||||
}
|
||||
|
||||
fun getConversationService(ctx: Context) = init(ctx, { conversationService }, { conversationService = it }) { ConversationService(it.messages) }
|
||||
|
||||
private inline fun <T> init(crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: () -> T): T =
|
||||
getter() ?: {
|
||||
synchronized(Singleton) {
|
||||
getter() ?: {
|
||||
val v = creator()
|
||||
setter(v)
|
||||
v
|
||||
}.invoke()
|
||||
}
|
||||
}.invoke()
|
||||
|
||||
private inline fun <T> init(ctx: Context, crossinline getter: () -> T?, crossinline setter: (T) -> Unit, crossinline creator: (BitmessageContext) -> T): T =
|
||||
getter() ?: {
|
||||
val bmc = getBitmessageContext(ctx)
|
||||
synchronized(Singleton) {
|
||||
getter() ?: {
|
||||
val v = creator(bmc)
|
||||
setter(v)
|
||||
v
|
||||
}.invoke()
|
||||
}
|
||||
}.invoke()
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
|
||||
/**
|
||||
* Starts the Bitmessage "full node" service if conditions allow it
|
||||
*/
|
||||
class StartServiceReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.action == "android.intent.action.BOOT_COMPLETED") {
|
||||
if (Preferences.isFullNodeActive(context)) {
|
||||
NetworkUtils.enableNode(context, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package ch.dissem.apps.abit.service
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import ch.dissem.apps.abit.util.NetworkUtils
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
|
||||
/**
|
||||
* Starts the full node if
|
||||
* * it is active
|
||||
* * it is not already running
|
||||
*
|
||||
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class StartupNodeOnWifiService : JobService() {
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
val bmc = Singleton.getBitmessageContext(this)
|
||||
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
|
||||
NetworkUtils.doStartBitmessageService(applicationContext)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters?) = if (Preferences.isWifiOnly(this)) {
|
||||
// Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
|
||||
// depending on Android version.
|
||||
Preferences.isFullNodeActive(this)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.synchronization
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.NetworkErrorException
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
|
||||
/**
|
||||
* Implement AbstractAccountAuthenticator and stub out all
|
||||
* of its methods
|
||||
*/
|
||||
class Authenticator(context: Context) : AbstractAccountAuthenticator(context) {
|
||||
|
||||
override fun editProperties(r: AccountAuthenticatorResponse, s: String) =
|
||||
throw UnsupportedOperationException("Editing properties is not supported")
|
||||
|
||||
// Don't add additional accounts
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun addAccount(r: AccountAuthenticatorResponse, s: String, s2: String, strings: Array<String>, bundle: Bundle) = null
|
||||
|
||||
// Ignore attempts to confirm credentials
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun confirmCredentials(r: AccountAuthenticatorResponse, account: Account, bundle: Bundle) = null
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun getAuthToken(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
|
||||
throw UnsupportedOperationException("Getting an authentication token is not supported")
|
||||
|
||||
override fun getAuthTokenLabel(s: String) =
|
||||
throw UnsupportedOperationException("Getting a label for the auth token is not supported")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun updateCredentials(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
|
||||
throw UnsupportedOperationException("Updating user credentials is not supported")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun hasFeatures(r: AccountAuthenticatorResponse, account: Account, strings: Array<String>) =
|
||||
throw UnsupportedOperationException("Checking features for the account is not supported")
|
||||
|
||||
companion object {
|
||||
val ACCOUNT_SYNC = Account("Bitmessage", "ch.dissem.bitmessage")
|
||||
val ACCOUNT_POW = Account("Proof of Work ", "ch.dissem.bitmessage")
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.synchronization
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* A bound Service that instantiates the authenticator
|
||||
* when started.
|
||||
*/
|
||||
class AuthenticatorService : Service() {
|
||||
/**
|
||||
* Instance field that stores the authenticator object
|
||||
*/
|
||||
private var authenticator: Authenticator? = null
|
||||
|
||||
override fun onCreate() {
|
||||
// Create a new authenticator object
|
||||
authenticator = Authenticator(this)
|
||||
}
|
||||
|
||||
/*
|
||||
* When the system binds to this Service to make the RPC call
|
||||
* return the authenticator's IBinder.
|
||||
*/
|
||||
override fun onBind(intent: Intent) = authenticator?.iBinder
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.synchronization
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
|
||||
/*
|
||||
* Define an implementation of ContentProvider that stubs out
|
||||
* all methods
|
||||
*/
|
||||
class StubProvider : ContentProvider() {
|
||||
|
||||
/**
|
||||
* Always return true, indicating that the
|
||||
* provider loaded correctly.
|
||||
*/
|
||||
override fun onCreate() = true
|
||||
|
||||
/**
|
||||
* Return no type for MIME type
|
||||
*/
|
||||
override fun getType(uri: Uri) = null
|
||||
|
||||
/**
|
||||
* query() always returns no results
|
||||
*/
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?,
|
||||
sortOrder: String?) = null
|
||||
|
||||
/**
|
||||
* insert() always returns null (no URI)
|
||||
*/
|
||||
override fun insert(uri: Uri, values: ContentValues?) = null
|
||||
|
||||
/**
|
||||
* delete() always returns "no rows affected" (0)
|
||||
*/
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
|
||||
|
||||
/**
|
||||
* update() always returns "no rows affected" (0)
|
||||
*/
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?) = 0
|
||||
|
||||
companion object {
|
||||
const val AUTHORITY = "ch.dissem.apps.abit.provider"
|
||||
}
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.synchronization
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
|
||||
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
|
||||
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
|
||||
import ch.dissem.apps.abit.util.Preferences
|
||||
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
|
||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE
|
||||
import ch.dissem.bitmessage.utils.Singleton.cryptography
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Sync Adapter to synchronize with the Bitmessage network - fetches
|
||||
* new objects and then disconnects.
|
||||
*/
|
||||
class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) {
|
||||
|
||||
private val bmc = Singleton.getBitmessageContext(context)
|
||||
|
||||
override fun onPerformSync(
|
||||
account: Account,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
try {
|
||||
if (account == ACCOUNT_SYNC) {
|
||||
if (Preferences.isConnectionAllowed(context)) {
|
||||
syncData()
|
||||
}
|
||||
} else if (account == ACCOUNT_POW) {
|
||||
syncPOW()
|
||||
} else {
|
||||
syncResult.stats.numAuthExceptions++
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
} catch (e: DecryptionFailedException) {
|
||||
syncResult.stats.numAuthExceptions++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun syncData() {
|
||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||
if (bmc.isRunning()) {
|
||||
LOG.info("Synchronization skipped, Abit is acting as a full node")
|
||||
return
|
||||
}
|
||||
val trustedNode = Preferences.getTrustedNode(context)
|
||||
if (trustedNode == null) {
|
||||
LOG.info("Trusted node not available, disabling synchronization")
|
||||
stopSync(context)
|
||||
return
|
||||
}
|
||||
LOG.info("Synchronization started")
|
||||
bmc.synchronize(
|
||||
trustedNode,
|
||||
Preferences.getTrustedNodePort(context),
|
||||
Preferences.getTimeoutInSeconds(context),
|
||||
true
|
||||
)
|
||||
LOG.info("Synchronization finished")
|
||||
}
|
||||
|
||||
private fun syncPOW() {
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
LOG.info("No identity available - skipping POW synchronization")
|
||||
return
|
||||
}
|
||||
val trustedNode = Preferences.getTrustedNode(context)
|
||||
if (trustedNode == null) {
|
||||
LOG.info("Trusted node not available, disabling POW synchronization")
|
||||
stopPowSync(context)
|
||||
return
|
||||
}
|
||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||
LOG.info("Looking for completed POW")
|
||||
|
||||
val privateKey = identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
|
||||
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
|
||||
val reader = ProofOfWorkRequest.Reader(identity)
|
||||
val powRepo = Singleton.getProofOfWorkRepository(context)
|
||||
val items = powRepo.getItems()
|
||||
for (initialHash in items) {
|
||||
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
|
||||
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
|
||||
val cryptoMsg = CryptoCustomMessage(
|
||||
ProofOfWorkRequest(identity, initialHash, CALCULATE, target))
|
||||
cryptoMsg.signAndEncrypt(identity, signingKey)
|
||||
val response = bmc.send(
|
||||
trustedNode,
|
||||
Preferences.getTrustedNodePort(context),
|
||||
cryptoMsg
|
||||
)
|
||||
if (response.isError) {
|
||||
LOG.error("Server responded with error: ${String(response.getData())}")
|
||||
} else {
|
||||
val (_, _, request, data) = CryptoCustomMessage.read(response, reader).decrypt(privateKey)
|
||||
if (request == COMPLETE) {
|
||||
bmc.internals.proofOfWorkService.onNonceCalculated(initialHash, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
stopPowSync(context)
|
||||
}
|
||||
LOG.info("Synchronization finished")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(SyncAdapter::class.java)
|
||||
|
||||
private const val SYNC_FREQUENCY = 15 * 60L // seconds
|
||||
|
||||
fun startSync(ctx: Context) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
val account = addAccount(ctx, ACCOUNT_SYNC)
|
||||
|
||||
// Recommend a schedule for automatic synchronization. The system may modify this based
|
||||
// on other scheduled syncs and network utilization.
|
||||
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
|
||||
}
|
||||
|
||||
fun stopSync(ctx: Context) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
val account = addAccount(ctx, ACCOUNT_SYNC)
|
||||
|
||||
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
|
||||
}
|
||||
|
||||
fun startPowSync(ctx: Context) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
val account = addAccount(ctx, ACCOUNT_POW)
|
||||
|
||||
// Recommend a schedule for automatic synchronization. The system may modify this based
|
||||
// on other scheduled syncs and network utilization.
|
||||
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
|
||||
}
|
||||
|
||||
fun stopPowSync(ctx: Context) {
|
||||
// Create account, if it's missing. (Either first run, or user has deleted account.)
|
||||
val account = addAccount(ctx, ACCOUNT_POW)
|
||||
|
||||
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
|
||||
}
|
||||
|
||||
private fun addAccount(ctx: Context, account: Account): Account {
|
||||
if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) {
|
||||
// Inform the system that this account supports sync
|
||||
ContentResolver.setIsSyncable(account, AUTHORITY, 1)
|
||||
// Inform the system that this account is eligible for auto sync when the network is up
|
||||
ContentResolver.setSyncAutomatically(account, AUTHORITY, true)
|
||||
}
|
||||
return account
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.synchronization
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Define a Service that returns an IBinder for the
|
||||
* sync adapter class, allowing the sync adapter framework to call
|
||||
* onPerformSync().
|
||||
*/
|
||||
class SyncService : Service() {
|
||||
|
||||
/**
|
||||
* Instantiate the sync adapter object.
|
||||
*/
|
||||
override fun onCreate() = synchronized(syncAdapterLock) {
|
||||
if (syncAdapter == null) {
|
||||
syncAdapter = SyncAdapter(this, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that allows the system to invoke
|
||||
* the sync adapter.
|
||||
*/
|
||||
override fun onBind(intent: Intent) = syncAdapter?.syncAdapterBinder
|
||||
|
||||
companion object {
|
||||
// Storage for an instance of the sync adapter
|
||||
private var syncAdapter: SyncAdapter? = null
|
||||
// Object to use as a thread-safe lock
|
||||
private val syncAdapterLock = Any()
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.DrawableRes
|
||||
import android.support.annotation.StringRes
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class to work with Assets.
|
||||
*/
|
||||
object Assets {
|
||||
fun readSqlStatements(ctx: Context, name: String): List<String> {
|
||||
try {
|
||||
val `in` = ctx.assets.open(name)
|
||||
val scanner = Scanner(`in`, "UTF-8").useDelimiter(";")
|
||||
val result = LinkedList<String>()
|
||||
while (scanner.hasNext()) {
|
||||
val statement = scanner.next().trim { it <= ' ' }
|
||||
if ("" != statement) {
|
||||
result.add(statement)
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
|
||||
Plaintext.Status.RECEIVED -> 0
|
||||
Plaintext.Status.DRAFT -> R.drawable.draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
|
||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.drawable.ic_notification_proof_of_work
|
||||
Plaintext.Status.SENT -> R.drawable.sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.drawable.sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@StringRes
|
||||
fun getStatusString(status: Plaintext.Status) = when (status) {
|
||||
Plaintext.Status.RECEIVED -> R.string.status_received
|
||||
Plaintext.Status.DRAFT -> R.string.status_draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
|
||||
Plaintext.Status.DOING_PROOF_OF_WORK -> R.string.proof_of_work_title
|
||||
Plaintext.Status.SENT -> R.string.status_sent
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
object Constants {
|
||||
const val PREFERENCE_WIFI_ONLY = "wifi_only"
|
||||
const val PREFERENCE_TRUSTED_NODE = "trusted_node"
|
||||
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout"
|
||||
const val PREFERENCE_SERVER_POW = "server_pow"
|
||||
const val PREFERENCE_FULL_NODE = "full_node"
|
||||
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
|
||||
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
|
||||
const val PREFERENCE_POW_COUNT = "pow_count"
|
||||
|
||||
const val BITMESSAGE_URL_SCHEMA = "bitmessage:"
|
||||
|
||||
val BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b")!!
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color.BLACK
|
||||
import android.graphics.Color.WHITE
|
||||
import android.util.Base64
|
||||
import android.util.Base64.NO_WRAP
|
||||
import android.util.Base64.URL_SAFE
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* Some helper methods to work with drawables.
|
||||
*/
|
||||
object Drawables {
|
||||
private val LOG = LoggerFactory.getLogger(Drawables::class.java)
|
||||
|
||||
private const val QR_CODE_SIZE = 350
|
||||
|
||||
fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
|
||||
val item = menu.findItem(menuItem)
|
||||
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
|
||||
return item
|
||||
}
|
||||
|
||||
fun toBitmap(identicon: Identicon, width: Int, height: Int = width): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
identicon.setBounds(0, 0, canvas.width, canvas.height)
|
||||
identicon.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun qrCode(address: BitmessageAddress?): Bitmap? {
|
||||
if (address == null) {
|
||||
return null
|
||||
}
|
||||
val link = StringBuilder()
|
||||
link.append(Constants.BITMESSAGE_URL_SCHEMA)
|
||||
link.append(address.address)
|
||||
if (address.alias != null) {
|
||||
link.append("?label=").append(address.alias)
|
||||
}
|
||||
address.pubkey?.apply {
|
||||
link.append(if (address.alias == null) '?' else '&')
|
||||
val pubkey = ByteArrayOutputStream()
|
||||
writer().writeUnencrypted(pubkey)
|
||||
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
|
||||
|
||||
}
|
||||
val result: BitMatrix
|
||||
try {
|
||||
result = MultiFormatWriter().encode(link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null)
|
||||
} catch (e: WriterException) {
|
||||
LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
|
||||
val w = result.width
|
||||
val h = result.height
|
||||
val pixels = IntArray(w * h)
|
||||
for (y in 0 until h) {
|
||||
val offset = y * w
|
||||
for (x in 0 until w) {
|
||||
pixels[offset + x] = if (result.get(x, y)) BLACK else WHITE
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.exports.ContactExport
|
||||
import ch.dissem.bitmessage.exports.MessageExport
|
||||
import ch.dissem.bitmessage.utils.UnixTime
|
||||
import com.beust.klaxon.JsonArray
|
||||
import com.beust.klaxon.Parser
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* Helper object for data export and import.
|
||||
*/
|
||||
object Exports {
|
||||
|
||||
fun exportData(target: File, ctx: Context): File {
|
||||
val temp = if (target.isDirectory) {
|
||||
File(target, "export-${UnixTime.now}.zip")
|
||||
} else {
|
||||
target
|
||||
}
|
||||
ZipOutputStream(FileOutputStream(temp)).use { zip ->
|
||||
zip.putNextEntry(ZipEntry("contacts.json"))
|
||||
val addressRepo = Singleton.getAddressRepository(ctx)
|
||||
val exportContacts = ContactExport.exportContacts(addressRepo.getContacts())
|
||||
zip.write(
|
||||
exportContacts.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
|
||||
val labelRepo = Singleton.getLabelRepository(ctx)
|
||||
zip.putNextEntry(ZipEntry("labels.json"))
|
||||
val exportLabels = MessageExport.exportLabels(labelRepo.getLabels())
|
||||
zip.write(
|
||||
exportLabels.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
zip.putNextEntry(ZipEntry("messages.json"))
|
||||
val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
val exportMessages = MessageExport.exportMessages(messageRepo.getAllMessages())
|
||||
zip.write(
|
||||
exportMessages.toJsonString(true).toByteArray()
|
||||
)
|
||||
zip.closeEntry()
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
fun importData(zipFile: Uri, ctx: Context) {
|
||||
val bmc = Singleton.getBitmessageContext(ctx)
|
||||
val labels = mutableMapOf<String, Label>()
|
||||
|
||||
processEntry(ctx, zipFile, "contacts.json") { json ->
|
||||
ContactExport.importContacts(json).forEach { contact ->
|
||||
bmc.addresses.save(contact)
|
||||
}
|
||||
}
|
||||
bmc.labels.getLabels().forEach { label ->
|
||||
labels[label.toString()] = label
|
||||
}
|
||||
processEntry(ctx, zipFile, "labels.json") { json ->
|
||||
MessageExport.importLabels(json).forEach { label ->
|
||||
if (!labels.contains(label.toString())) {
|
||||
bmc.labels.save(label)
|
||||
labels[label.toString()] = label
|
||||
}
|
||||
}
|
||||
}
|
||||
processEntry(ctx, zipFile, "messages.json") { json ->
|
||||
MessageExport.importMessages(json, labels).forEach { message ->
|
||||
bmc.messages.save(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEntry(ctx: Context, zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) =
|
||||
ZipInputStream(ctx.contentResolver.openInputStream(zipFile)).use { zip ->
|
||||
var nextEntry = zip.nextEntry
|
||||
while (nextEntry != null) {
|
||||
if (nextEntry.name == entry) {
|
||||
processor(Parser().parse(zip) as JsonArray<*>)
|
||||
}
|
||||
nextEntry = zip.nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.support.annotation.DrawableRes
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
|
||||
/**
|
||||
* Utilities to work with the common floating action button in the main activity
|
||||
*/
|
||||
object FabUtils {
|
||||
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
return fab
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.ColorInt
|
||||
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
|
||||
/**
|
||||
* Helper class to help with translating the default labels, getting label colors and so on.
|
||||
*/
|
||||
object Labels {
|
||||
fun getText(label: Label, ctx: Context): String = getText(label.type, label.toString(), ctx)!!
|
||||
|
||||
fun getText(type: Label.Type?, alternative: String?, ctx: Context) = when (type) {
|
||||
Label.Type.INBOX -> ctx.getString(R.string.inbox)
|
||||
Label.Type.DRAFT -> ctx.getString(R.string.draft)
|
||||
Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
|
||||
Label.Type.SENT -> ctx.getString(R.string.sent)
|
||||
Label.Type.UNREAD -> ctx.getString(R.string.unread)
|
||||
Label.Type.TRASH -> ctx.getString(R.string.trash)
|
||||
Label.Type.BROADCAST -> ctx.getString(R.string.broadcasts)
|
||||
else -> alternative
|
||||
}
|
||||
|
||||
fun getIcon(label: Label): IIcon = when (label.type) {
|
||||
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
|
||||
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
|
||||
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
|
||||
Label.Type.SENT -> CommunityMaterial.Icon.cmd_send
|
||||
Label.Type.BROADCAST -> CommunityMaterial.Icon.cmd_rss
|
||||
Label.Type.UNREAD -> GoogleMaterial.Icon.gmd_markunread_mailbox
|
||||
Label.Type.TRASH -> GoogleMaterial.Icon.gmd_delete
|
||||
else -> CommunityMaterial.Icon.cmd_label
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getColor(label: Label) = if (label.type == null) {
|
||||
label.color
|
||||
} else 0xFF000000.toInt()
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.content.ContextCompat
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
|
||||
import ch.dissem.apps.abit.service.BitmessageService
|
||||
import ch.dissem.apps.abit.service.StartupNodeOnWifiService
|
||||
|
||||
|
||||
object NetworkUtils {
|
||||
|
||||
fun enableNode(ctx: Context, ask: Boolean = true) {
|
||||
Preferences.setFullNodeActive(ctx, true)
|
||||
if (Preferences.isWifiOnly(ctx)) {
|
||||
if (Preferences.isConnectionAllowed(ctx)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scheduleNodeStart(ctx)
|
||||
} else {
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
} else if (ask) {
|
||||
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
|
||||
if (ctx !is Activity) {
|
||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
}
|
||||
ctx.startActivity(dialogIntent)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scheduleNodeStart(ctx)
|
||||
}
|
||||
} else {
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
fun doStartBitmessageService(ctx: Context) {
|
||||
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
|
||||
fun disableNode(ctx: Context) {
|
||||
Preferences.setFullNodeActive(ctx, false)
|
||||
ctx.stopService(Intent(ctx, BitmessageService::class.java))
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun scheduleNodeStart(ctx: Context) {
|
||||
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
|
||||
val builder = JobInfo.Builder(0, serviceComponent)
|
||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
|
||||
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
|
||||
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
jobScheduler.schedule(builder.build())
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user