Compare commits

...

858 Commits

Author SHA1 Message Date
b028ff9d8e Add github sync for i2p.i2p-bote
Some checks failed
Sync Primary Repository to GitHub Mirror / sync (push) Has been cancelled
2025-05-10 18:50:16 -04:00
0775c4dd49 Merge pull request #119 from i2p/release-0.4.8
I2P-Bote 0.4.8
2019-05-16 13:29:06 +01:00
9fd4f6cb03 Update versions and history for 0.4.8 2019-05-16 12:21:08 +01:00
edb086cc1d Update translations after string change 2019-05-16 12:19:57 +01:00
7d5b9a62d4 Update translation strings after changes 2019-05-16 12:18:08 +01:00
86479e543f Update I2P-Bote connection error page to match Bote Android 2019-05-16 12:14:55 +01:00
32ae4d0cf4 Remove old redundant entries from plugin.config
The values are canonically set in webapp/build.gradle, and are
unnecessary here.
2019-05-16 11:35:54 +01:00
c835216b98 Update bug reports URL 2019-05-16 11:29:14 +01:00
2b7e6985ff Translation updates 2019-05-16 11:07:25 +01:00
e989db7470 Upgrade to I2P 0.9.40 2019-05-16 11:06:40 +01:00
1c4401590b Upgrade Gradle wrapper to 5.4.1 2019-05-16 09:10:55 +01:00
d5383c056a Merge pull request #118 from mikalv/master
Release commit for 0.6.1
2019-05-15 21:22:48 +01:00
eb9e88d51d Release commit for 0.6.1 2019-05-14 00:50:10 +02:00
4f142e6737 Merge pull request #112 from eyedeekay/master
Add eyedeekay to bootstrap peers, remove kytv
2019-05-10 00:38:45 +02:00
1df6f61513 Merge pull request #115 from i2p/bug-fixes
Bug fixes
2019-05-09 16:54:07 +01:00
idk
348010573a update node list to include my node(idk) and mark kytv's node as inactive 2019-04-25 14:23:48 -04:00
6dc3c49a01 Set log level for i2p.bote.* in Android to DEBUG in BoteService 2019-04-24 12:50:54 +01:00
2e65d25b02 Test against OpenJDK 12 2019-04-24 12:14:02 +01:00
da2e65a4e8 Fix NPE when reading the local destination key from the key file
Base64.decode() returns null on error. Additionally, since I2P 0.9.14,
leading and trailing whitespace will cause an error.
2019-04-24 12:10:26 +01:00
9019f40252 Merge pull request #111 from i2p/i2p-0.9.39
Upgrade to I2P 0.9.39
2019-04-22 20:09:38 +01:00
4b1eb5f841 Upgrade to I2P 0.9.39 2019-04-22 09:21:29 +01:00
18c05f57a5 Copy ElGamalAESEngine from the router library
The engine was moved out of the I2P core library in 0.9.38, but we still
depend on it for ElGamal2048-DSA1024 addresses and RelayRequest
encryption.
2019-04-22 08:57:33 +01:00
fafab53af2 Merge pull request #110 from i2p/build-system-updates-2
Build system updates 2
2019-04-21 23:34:50 +01:00
6f610b40fb Update Java 7 build support with changes from i2p.i2p 2019-04-21 16:49:04 +01:00
2451f1503a Upgrade Tomcat to 8.5.40 2019-04-21 16:49:04 +01:00
697f31fb41 Upgrade to supported Gretty plugin 2019-04-20 19:50:05 +01:00
2351462b84 Upgrade Android test runner support libraries 2019-04-20 19:20:46 +01:00
a4ac435a78 Upgrade Android multidex support library 2019-04-20 19:15:38 +01:00
ecf4437412 Upgrade general dependencies 2019-04-20 17:01:36 +01:00
c9178f6595 Upgrade Android Gradle build tools 2019-04-20 16:21:18 +01:00
20dee0c93b Fix Android dependencies after build system updates 2019-04-20 16:20:53 +01:00
06c16f7184 Merge pull request #109 from mikalv/master
Translation updates and permissions fix
2019-04-19 22:43:00 +01:00
19d9bdf146 Use same uid as package name 2019-03-23 03:40:33 +01:00
945dd02bc4 A fix to make bote connect to the router on newer android api's. 2019-03-23 03:36:46 +01:00
3cee668040 Translations update 2019-03-23 03:35:06 +01:00
86b6d9d16b Translations update 2019-03-23 03:33:26 +01:00
9f83d07d73 Merge pull request #107 from i2p/build-system-updates
Build system updates
2019-02-16 18:05:31 +00:00
40a3ec9b4e Install newer gettext in Travis CI
gettext 0.19.8-6 fixes a bug where newer JDKs weren't detected. The
trusty build environment doesn't support the latest gettext Debian
package, as it requires a newer dpkg. However, upgrading to a 0.19 build
is sufficient, as then we can generate source files with msgfmt instead
of class files.
2019-02-16 17:18:14 +00:00
86bec4d2e1 Mark message classes from older msgfmt versions as generated output 2019-02-16 16:04:00 +00:00
e71b3c416c Update Travis CI config to test against more JDKs 2019-02-16 15:19:58 +00:00
d5ef16da5b Fix building and testing with JDK 11 2019-02-16 15:17:46 +00:00
2a64794a05 Set up cross-compilation for JDK 9+ 2019-02-16 15:17:39 +00:00
ebe99afe57 Upgrade Gradle wrapper to 5.2.1 2019-02-16 13:43:26 +00:00
9ed3867ccb Merge pull request #101 from i2p/release-0.4.7
I2P-Bote 0.4.7
2018-04-09 07:41:57 -06:00
ec526ccbe1 Update versions and history for 0.4.7 2018-04-09 07:15:39 -06:00
cb2b9801c8 Upgrade Tomcat to 8.5.23 2018-04-09 06:50:36 -06:00
dcf6d8d982 Upgrade I2P dependencies 2018-04-08 14:30:32 +01:00
c0926d0002 Set default store hops to 2 2018-04-08 14:30:00 +01:00
292517003a Merge pull request #100 from i2p/imap4-fixes
IMAP4 fixes
2018-04-08 13:33:59 +01:00
0454319313 Merge pull request #99 from i2p/build-script-fixes
Build script fixes
2018-04-08 13:25:17 +01:00
f1facc975b Don't bitshift uidValidity and modSeq out of range 2018-04-07 22:50:33 +01:00
e9dd6d0877 Use default IMAP namespace for users 2018-04-07 22:38:02 +01:00
4589b7ad38 Remove stray line in build script 2018-04-07 20:53:17 +01:00
e1c721dc78 Migrate from bootClasspath to bootstrapClasspath 2018-04-07 20:53:17 +01:00
5b095b257d Merge pull request #97 from i2p/james-server-3.0.1
Migrate to James Server 3.0.1
2018-04-07 20:51:23 +01:00
c4bf87717c Upgrade Gradle wrapper to 4.6 2018-04-07 20:21:02 +01:00
1906bcbc1f Add support for older msgfmt versions to webapps build script 2018-04-07 18:59:30 +01:00
32d634e350 Migrate to James Server 3.0.1 2018-04-07 17:24:28 +01:00
28bed3c325 Initialise APIs during construction instead of after tunnel building
IMAP and SMTP should be useable while an I2P connection is being set up.
2018-04-07 02:38:46 +01:00
93efffae1b Revert more webapp dependencies to use the "compile" configuration
The plugin build script needs to be able to enumerate all dependencies that need
to be packed, which isn't possible with the "implementation" configuration.
2017-12-07 14:17:07 +13:00
5a9220c47c Revert several webapp dependencies to use the "compile" configuration
They are needed by the JSP compilation step.
2017-12-04 12:28:32 +13:00
3b0ad4ee13 Define API dependencies of core library 2017-12-04 12:14:57 +13:00
44b0fdf75a Add Travis CI build script
Copied from b8eeb72044/.travis.yml
2017-12-04 11:44:48 +13:00
b9bafe2f1a Upgrade I2P dependencies 2017-12-04 11:37:36 +13:00
11fd617fee Upgrade BouncyCastle to 1.58
Includes workaround for https://github.com/bcgit/bc-java/issues/262
2017-12-04 11:29:46 +13:00
5172b704fd Upgrade Android dependencies 2017-12-04 09:17:18 +13:00
52a5c70671 Upgrade to Gradle 4.1 and Android Gradle Tools 3.0.1
Required dropping gradle-witness because it does not support modern Gradle.
2017-12-04 08:50:41 +13:00
c082a50cad Use a common I2P dependency version for core and android 2017-12-04 08:36:27 +13:00
766d5ef00f Fix compilation without bootstrap classpath set 2017-12-04 08:12:37 +13:00
96784b88bb Use new plugins DSL to apply Gretty plugin
Fixes a classpath interaction issue between Gretty and the Android subproject.
2017-08-20 06:13:01 +12:00
f2c152baf8 Tidy up README 2017-05-07 16:06:16 +12:00
d7157423ae Add build tasks and instructions for standalone WAR
Closes #92.
2017-05-07 16:02:51 +12:00
95d7180400 Add missing runtime dependency to fat WAR 2017-05-07 15:51:20 +12:00
7363b658f8 Build separate thin WAR for use in I2P plugin 2017-05-07 15:19:42 +12:00
287ff45915 Merge Android README into main README, add plugin build instructions 2017-05-07 13:03:26 +12:00
9b275e59ce Add .gitignore 2017-05-07 13:03:08 +12:00
b8b9171c08 Fetch James Server archive during execution phase instead of configuration phase 2017-05-07 13:02:17 +12:00
9430125217 Fetch James Server archive over HTTPS 2017-05-07 13:01:52 +12:00
fbf5b43b9e Enable building the plugin without having the Android SDK 2017-05-07 13:00:45 +12:00
6566ad1510 I2P-Bote 0.4.6 2017-05-03 21:25:46 +00:00
169d2061d0 Leave Tomcat JARs out of plugin 2017-05-03 21:16:19 +00:00
3a1298369c Only set i2cp.domainSocket if using I2P Android 2017-04-29 12:09:10 +00:00
024930873f Add Gradle wrapper 2017-04-08 01:17:24 +00:00
2d92022994 Changes to support Jetty 9
- Upgrade servlet and JSP dependencies
- Require Java 1.7, I2P 0.9.30, Jetty 9
2017-04-02 06:40:55 +00:00
8686741693 Fix project version configuration 2017-04-02 05:34:06 +00:00
f9a40374da Upgrade I2P dependencies 2017-04-02 05:20:16 +00:00
f741a7f966 Move old build.xml out of the way
Leaving it in the repository until the Win32 packaging is migrated.
2017-04-02 04:56:18 +00:00
c0baa7278e Update history 2017-04-02 04:53:37 +00:00
720a42ed4d Drop unused libs 2017-04-02 04:46:07 +00:00
155e0bfd0d Remove XPI2P support from the I2P plugin Gradle plugin
It works, but the format is long-deprecated and should not be used for new
plugins.
2017-04-02 04:39:22 +00:00
dbeaf665a2 Generate plugin update files identical to installers (for now) 2017-03-27 11:16:57 +00:00
215120fed0 Upgrade test dependencies 2017-03-27 04:44:33 +00:00
678cf1a5be Move I2PPlugin application to start of webapp build file 2017-03-27 04:44:25 +00:00
fb1a7ddebe Static functions 2017-03-27 04:03:33 +00:00
e056828e3f More sensible name for plugin key directory 2017-03-27 04:00:23 +00:00
ed37609e80 Update translations 2017-03-26 07:41:28 +00:00
45ed12fe7d Update webapp messages after changes 2017-03-26 07:19:35 +00:00
7cdb3cac17 Update translations 2017-03-26 07:17:15 +00:00
f3866de65d Update messages path in Transifex config 2017-03-26 07:16:20 +00:00
0234afec0e Migrate message compilation and bundling to Gradle 2017-03-26 07:07:34 +00:00
13798b3bf2 Migrate poupdate command to Gradle 2017-03-13 11:14:01 +00:00
beadf2917e Move translation files into webapp 2017-03-13 08:16:17 +00:00
50c2316537 Move translation helpers from core to webapp 2017-03-13 01:41:47 +00:00
2987ec6901 Un-translate theme names
This is a minor usability regression, and may be fixed later, but is necessary
for now to enable correct translation handling between webapp and android.
2017-03-13 01:32:31 +00:00
2de7962a37 Extract peer stats translations from core, expose reachability 2017-03-13 01:24:59 +00:00
37ebdf5ced Connect plugin signing inputs to plugin.zip output 2017-03-12 09:58:27 +00:00
ea9fbb5b27 Remove remaining usages of Util._t() from GeneralHelper 2017-03-12 09:57:51 +00:00
bb99f1196f Bugfix 2017-03-12 06:14:17 +00:00
a70bdee405 Move invalid address error translation from core to webapp 2017-03-12 05:10:06 +00:00
9fb28d6724 Upgrade support libraries to 25.2.0 2017-03-12 04:47:38 +00:00
6f0cff9ca5 Suppress Support version lint
Lint is incorrectly requiring multidex to have the same version as other support
libraries.
2017-03-12 04:36:33 +00:00
259106a369 Upgrade to newer maintained version of ViewPagerIndicator
Fixes an incompatibility with API 23+
2017-03-12 03:06:00 +00:00
76dfa45fce Move getHumanReadableSize to JSPHelper 2017-03-12 02:43:37 +00:00
4f506027f4 Escape apostrophes in Android strings 2017-03-12 00:28:53 +00:00
32ca9e1988 Move "bad vanity chars" error message out of core 2017-02-19 21:21:24 +00:00
97b7c6a4d9 Work around SCrypt pack200 issue 2017-02-19 21:19:09 +00:00
26208ddeb4 Move BanList strings from core to webapp 2017-02-19 15:49:07 +00:00
fb9aae2928 Move send error translations from core to webapp 2017-02-19 15:34:18 +00:00
e0edf870b8 Bugfix 2017-02-19 15:14:44 +00:00
a49360a2cb Subclass PasswordException for specific causes 2017-02-19 15:10:21 +00:00
2b47c1f6b3 Move StatusListener strings from core to android 2017-02-19 12:22:17 +00:00
9d1dcabdf2 Don't translate crypto names 2017-02-18 16:04:08 +00:00
0b7c98820a Update translations 2017-02-13 00:30:12 +00:00
597205bd09 Use static string 2017-02-12 23:59:47 +00:00
6b0f66376e Bugfix: use system default locale 2017-02-12 23:54:20 +00:00
3ab3d2faac Update list of Android UI languages 2017-02-12 23:34:14 +00:00
af6abb5b21 New Android translations 2017-02-12 23:23:33 +00:00
1c2e7d886a Update Android translations 2017-02-12 23:20:36 +00:00
ebde691774 Merge Transifex config files 2017-02-12 23:19:16 +00:00
1184bcaba4 Enable encrypted files to be selected for importing 2017-02-12 23:15:21 +00:00
de9c8f9036 Android address book import/export 2017-02-12 23:06:50 +00:00
ee783dc31b Extract Android data import/export logic into parent classes 2017-02-12 22:30:41 +00:00
1536ffc68c Add address book export/import test 2017-02-12 21:15:53 +00:00
23760ee341 Make the address book exportable 2017-02-12 20:52:41 +00:00
7770214150 Extract import/export file logic from identities into a parent class 2017-02-12 20:41:44 +00:00
9865927b30 Extract AddressBook <-> Properties conversion into separate methods 2017-02-12 20:04:47 +00:00
2f2c67d7f3 app:theme -> android:theme (and layout reformatting) 2017-02-12 18:41:43 +00:00
102c62cffa Remove checks for API >= 10, since minSdkVersion = 10 2017-02-12 18:17:13 +00:00
45242e9079 Fix NPE when adding attachment (ticket #1730) 2017-02-12 17:43:46 +00:00
b9f175f4b9 Replace anonymous DialogFragment subclasses with full subclasses 2017-02-12 01:17:37 +00:00
1f2aa72f1a Upgrade Android dependencies 2017-02-12 00:39:59 +00:00
d3b2d33860 Compile against SDK 25
- Bump compileSdkVersion to 25
- Upgrade build tools to 25.0.2
- Upgrade support libraries to 25.1.1
2017-02-12 00:05:10 +00:00
fd492ec00c Upgrade Android test harness 2017-02-11 23:31:29 +00:00
7a0d7b3acc Migrate to support library PreferenceFragmentCompat 2017-02-11 23:27:55 +00:00
b9da2b27a8 Enable multidex 2017-02-11 15:43:14 +00:00
1b00976ff9 Compile against SDK 23
- Bump compileSdkVersion to 23
- Upgrade build tools to 23.0.3
- Upgrade support libraries to 23.4.0
- Replace html-textview code with upstream library that supports SDK 23
2017-02-11 00:14:27 +00:00
a8c5ae8586 Upgrade Android Gradle build tools 2017-02-10 23:16:00 +00:00
4a5cb919de Exclude jstl.jar from I2P plugin 2017-01-21 15:56:38 +00:00
0e7e7ed03a Update Gradle build scripts after prop 2017-01-21 15:56:23 +00:00
d1a2b92bca propagate from branch 'i2p.i2p-bote' (head c1349d682349dd701b9f0b18624293e7acca9ee3)
to branch 'i2p.i2p-bote.gradle' (head ff5f091c01e0dee5a95632846781536d11ff6324)
2017-01-20 18:13:21 +00:00
31cc63bf84 0.4.5 2017-01-20 18:06:17 +00:00
10f951a03b New translations 2017-01-20 15:44:21 +00:00
a1c5af227c Updated translations 2017-01-20 15:43:48 +00:00
2e53c3d5bc Update history 2017-01-20 15:40:22 +00:00
05c915743c Add a Content Security Policy 2017-01-20 15:38:44 +00:00
f1d790c5ff Escape title in header 2017-01-20 15:38:17 +00:00
75ed190892 Escape attachment filenames when showing an email
Thanks BearDog for raising the issue!
2017-01-20 15:37:57 +00:00
fce00ccc7a Add CSRF checking to GET requests on setPassword.jsp 2017-01-20 15:17:23 +00:00
cd1f3e4b98 Catch IAEs when sorting a Folder (ticket #1931) 2017-01-15 19:10:55 +00:00
23a9a0c0db Add CSRF checking to GET requests on submitIdentity.jsp 2017-01-08 14:42:34 +00:00
8cfff96e15 Pull in bundle-messages.sh improvements 2017-01-08 10:49:23 +00:00
61fe334d79 0.4.4 2016-11-28 09:59:45 +00:00
4985e07307 Enforce same-origin policy for POST 2016-11-28 09:14:17 +00:00
c1b40076df Update ignores 2016-11-28 03:39:25 +00:00
a2c98d83f0 Update translations 2016-11-28 03:33:22 +00:00
3234f05c6b Update translation strings 2016-11-27 10:00:04 +00:00
dc7eb02636 Updated translations 2016-11-27 09:52:04 +00:00
1e0f561126 Add missing POST check 2016-11-27 09:47:55 +00:00
8bd02eba47 Show a more helpful screen for CSRF errors 2016-11-27 07:54:13 +00:00
200756a13b Hook CSRF logging into I2P logs 2016-11-27 07:33:46 +00:00
57d12ca32f Add version to CSS URLs so browsers refetch after upgrades 2016-11-27 06:35:05 +00:00
90818cf1f4 Distinguish error messages from info messages in Material theme 2016-11-27 06:13:12 +00:00
3052824d1b Tweak error message to avoid XSS filter (ticket #1815) 2016-11-27 04:04:06 +00:00
b4b52dffa0 Move HungryHobo to "past developers" :( 2016-11-27 02:45:24 +00:00
de720d01c4 Added CSRF guards to all forms
Thanks Beardog for raising the issue!
2016-11-27 02:10:36 +00:00
694de2e4f6 Updated translation strings and translations 2016-11-24 02:54:11 +00:00
435ca2d12b Add links to source code in FAQ 2016-11-24 02:48:43 +00:00
849fbc26d4 Added new translations 2016-11-24 02:42:33 +00:00
37ce138788 Update history and about 2016-11-24 01:56:17 +00:00
c716b93509 Upgrade JavaMail 2016-11-21 07:52:39 +00:00
e2a9f0b323 Tweak I2P plugin DSL to reflect plugin layout 2016-11-21 02:34:19 +00:00
1662da85b7 Add javax.servlet and javax.servlet.jsp to plugin exclusions 2016-11-20 11:04:08 +00:00
4a2b3a8047 Get the Android app building
- Upgrade to I2P 0.9.27
- Rewrite ECUtils to work with both BouncyCastle and SpongyCastle
- Upgrade Mockito to 2.2.+
- Fix ProGuard rules
- Fix viewpagerindicator hash (author accidentally overwrote it on MC)
2016-11-20 09:28:10 +00:00
ac458948e3 Missing file from previous commit 2016-11-20 03:16:53 +00:00
cba49fb0a5 Move common local libs into separate subproject
Required so that Android can access the dependencies without complaining
about them being included twice during dexing:
http://tools.android.com/tech-docs/new-build-system/tips#TOC-Handling-transitive-dependencies-for-local-artifacts-jars-and-aar-
2016-11-20 03:15:35 +00:00
690f22d60d Set up android subproject 2016-10-25 00:02:49 +00:00
dbd99ab539 Remove duplicate files 2016-10-24 23:52:21 +00:00
61126ef640 propagate from branch 'i2p.i2p-bote.android' (head 17ebdec164206fcce5445bd62feb5524a4f4c53a)
to branch 'i2p.i2p-bote.gradle' (head d3b12e514a4f1f218ec8b5dda1f39835a748fc64)
2016-10-24 23:25:38 +00:00
cb5165bd11 Implement I2P plugin build system as a custom Gradle Task 2016-10-24 04:52:59 +00:00
fd56283aea Stop filter clobbering image files 2016-10-24 04:22:31 +00:00
625c6df67e Precompile JSPs 2016-08-22 14:20:44 +00:00
0db69bbe5c Exclude test suite runner from Gradle tests to remove duplicates 2016-08-22 13:33:32 +00:00
f384358723 Add Gradle build scripts
Gradle Witness rev: 10f1269c0aafdc1d478efc005ed48f3a47d44278
2016-08-22 10:10:54 +00:00
9a51d7c071 Split source into core (used for Android) and webapp (used for plugin) 2016-08-22 07:00:48 +00:00
89c7648a27 Dynamically load router classes to remove dependency on router.jar 2016-08-20 14:14:23 +00:00
5936667c0b Refactor SeedlessParameters 2016-08-20 13:32:17 +00:00
2c2a7b703b Use new I2CP domain socket API
Requires an I2P client library based on I2P 0.9.26 (once that is released)
2016-05-29 05:15:43 +00:00
09beea3022 Upgrade Android Gradle plugin 2016-05-29 05:14:47 +00:00
7dd85757f7 More upstream changes 2016-05-29 05:14:25 +00:00
caf03e2d44 Fixes due to i2p.i2p-bote changes 2016-05-29 05:13:55 +00:00
b73a52f48f Use new I2CP domain socket API
Requires I2P Android 0.9.26
2016-05-29 05:11:48 +00:00
ab099ff3a3 Click on truncated identity key to see details.
Allow user to click on truncated identity key to see full identity.

Explanation:
As a noob, I spent 15 minutes clicking around, reading FAQ, documentation, etc because I couldn't figure out how to get my full address.
This should not require any thought at all.
2015-12-11 10:42:56 +00:00
7ee9db3153 Updated CHANGELOG 2015-06-21 06:41:02 +00:00
9b01bef92c 0.6
i2p.i2p-bote: 2bf81924e31a6c73f97f37a67ee877147d908cc2
i2p.i2p tag: i2p-0.9.20
2015-06-21 06:40:46 +00:00
b79696402a Fixed default warning visibility 2015-06-21 03:27:20 +00:00
65575d3125 Updated translations 2015-06-21 02:52:24 +00:00
e949864c47 Test SetupActivity UI 2015-06-21 02:33:05 +00:00
0703112831 Test IntroActivity UI
Doesn't verify result codes yet (is it possible to do so?)
2015-06-20 12:03:20 +00:00
6a1f96dd4f Static imports 2015-06-20 12:01:06 +00:00
ebcebb9f98 Use Espresso-Intents to check started Activities 2015-06-20 11:30:03 +00:00
4605158227 Start testing EmailListActivity UI 2015-06-20 10:32:18 +00:00
0b1c992739 Clean up inbox after tests 2015-06-20 09:32:08 +00:00
9f6e37f2f1 Fix Espresso dependencies 2015-06-20 09:31:46 +00:00
088ff3c883 Upgraded Espresso 2015-06-20 00:35:19 +00:00
626a25b965 Upgraded libraries 2015-06-19 10:53:25 +00:00
3269866b79 Update identities loaders on changes
Requires i2p.i2p-bote 2bf81924e31a6c73f97f37a67ee877147d908cc2
2015-06-05 10:42:10 +00:00
74cd4d7142 Make first identity default by default 2015-06-05 07:27:26 +00:00
d3d92c0df7 Use selected identity as default for new emails 2015-06-05 06:29:04 +00:00
449f2d171d Require password to access privacy and app protection settings 2015-06-05 05:56:25 +00:00
80e2bb2559 Updated CHANGELOG 2015-06-05 04:31:41 +00:00
51901525da Fix lock size in drawer 2015-06-05 04:30:35 +00:00
6ff1e4ae3e Make identicon size in drawer a multiple of nine 2015-06-05 04:30:12 +00:00
6f1d6e99eb propagate from branch 'i2p.i2p-bote.android.dev' (head 2a826ee95560ec9a0e9fc61f1bc8534229a9cda9)
to branch 'i2p.i2p-bote.android' (head be56b90da9cf777c5291d91ff6fb18294e6582d0)
2015-06-05 02:44:43 +00:00
47a24f8544 Better app protection icon 2015-06-05 02:42:13 +00:00
4cb93b8c8a Upgrade to Iconics 1.0.2 and MaterialDrawer 3.0.6 to fix bugs 2015-06-05 02:41:58 +00:00
4e25e59d30 Upgraded support libraries 2015-06-04 01:23:18 +00:00
f858f6a90e New help translations for in 2015-06-04 01:17:31 +00:00
4efdfcc612 Updated translations 2015-06-04 01:17:23 +00:00
904993ea77 Upgrade to client library v0.7 2015-06-04 01:09:06 +00:00
c6c6a26ac2 Drop now-unused drawables 2015-06-04 00:46:00 +00:00
6dc2161306 Move FABs to Iconics 2015-06-04 00:45:29 +00:00
79ebb2013d Fix defStyle handling 2015-06-04 00:44:36 +00:00
87908149c4 Move menus to Iconics 2015-06-04 00:22:20 +00:00
bef97cb259 Use Iconics for intro swipe icon 2015-06-03 22:50:38 +00:00
f8a6640beb Add padding to settings icons, tweak size 2015-06-03 12:58:28 +00:00
330931a03f Move Preferences to Iconics 2015-06-03 12:44:12 +00:00
c104dcd320 Added padding to IconicsDrawables measured from original drawables 2015-06-03 11:54:27 +00:00
cfc751aae2 Add explicit dependency on Android-Iconics 2015-06-02 23:28:32 +00:00
dc3f152079 Use Android-Iconics for most icons
Some icon sizes increase slightly. Fix waiting on
https://github.com/mikepenz/Android-Iconics/issues/36
https://github.com/mikepenz/MaterialDrawer/issues/384
2015-06-02 23:28:01 +00:00
d0495cfc14 Add fake "locked" profile to unlock app from drawer
Currently broken with an NPE, waiting on
https://github.com/mikepenz/MaterialDrawer/issues/381
2015-06-02 11:21:18 +00:00
f007a044db Display correct new email count for per-identity views 2015-06-02 04:52:30 +00:00
ef2edee93e Tweak how identities are displayed in the drawer 2015-06-01 14:08:37 +00:00
bebf8404bd Add "All mail" view 2015-06-01 13:59:13 +00:00
3923d70d07 Use resource IDs directly for drawer folder icons 2015-06-01 13:34:39 +00:00
fc9c4a04ac Remove unnecessary cast 2015-06-01 13:31:06 +00:00
bb9dc8a90f Enable users to filter folders by identity 2015-06-01 13:29:09 +00:00
e66f3f736d Move DrawerFolderLoader callbacks into an inner class 2015-06-01 11:27:25 +00:00
0945caf8eb Migrate to com.mikepenz:materialdrawer
Net status updates in the drawer are broken, waiting on
https://github.com/mikepenz/MaterialDrawer/issues/378
2015-06-01 10:59:28 +00:00
13e6b530d3 Moved to minSdk 10 and support libs 22.2.0 to use com.mikepenz:materialdrawer 2015-06-01 10:55:48 +00:00
e4e6fc5ec2 Updated translations 2015-05-29 11:58:37 +00:00
ef223e09b5 Use FAB for new identity action 2015-05-29 11:57:10 +00:00
4016b2d019 Updated translations after string push 2015-05-29 09:39:43 +00:00
6fbf145864 Updated CHANGELOG 2015-05-29 09:33:56 +00:00
10abb14758 Icons for preference categories 2015-05-29 09:28:04 +00:00
7915177805 Rename general settings category to network 2015-05-29 09:15:43 +00:00
0811ad125c Implement screen security, combine with password in app protection category 2015-05-29 08:20:06 +00:00
72693a3d51 Split out privacy settings into separate category 2015-05-29 06:40:26 +00:00
ad40a22b00 Extract I2P router config to advanced settings 2015-05-29 06:07:44 +00:00
8547ae4028 Lint 2015-05-29 03:47:19 +00:00
00110bfafe Configurable language, part 3 2015-05-29 03:25:33 +00:00
4af2b881e6 Configurable language, part 2 2015-05-29 03:14:35 +00:00
335b3f4e14 Configurable language, part 1 2015-05-29 02:54:40 +00:00
28c4b20b78 Nav drawer style fixes 2015-05-29 02:30:46 +00:00
6d6d2cdd00 AppCompat v22.1.*: drawable tinting 2015-05-29 02:26:03 +00:00
43bcda77b1 Correct nav drawer styling 2015-05-29 01:52:06 +00:00
3fd0a2b915 Lint 2015-05-29 01:51:39 +00:00
9235be5df2 Don't add settings twice 2015-05-29 00:09:46 +00:00
7864d62892 Pull reseed certs into Bote so the internal router can reseed 2015-05-29 00:06:33 +00:00
8d097f0712 Updated CHANGELOG 2015-05-28 12:28:03 +00:00
0da3adda55 AppCompat v22.1.*: Material design dialogs 2015-05-28 12:22:28 +00:00
a1de624d0a Deprecations 2015-05-28 12:06:42 +00:00
4199c3de8f ActionBarActivity -> AppCompatActivity 2015-05-28 12:01:53 +00:00
229e495a0f Dropped old layout 2015-05-28 11:59:41 +00:00
eb519d6255 Rebuilt libscrypt.so, included armeabi-v7a
Inclusion of armeabi-v7a is necessary because the I2P Android client library
contains a libjbigi.so for armeabi-v7a, and that causes Android to not load the
armeabi libscrypt.so, slowing down Bote.

Source: https://github.com/spaggetti/scrypt
NDK: android-ndk-r10d
2015-05-28 11:43:34 +00:00
ba745bae31 Layout tweak 2015-05-28 11:40:20 +00:00
51416659fb Bugfix 2015-05-28 11:25:55 +00:00
e54027e52b Updated translations 2015-05-28 11:21:50 +00:00
972d64db35 Missing file from previous commit 2015-05-28 11:21:15 +00:00
2f2e44c413 Added Transifex config, renamed id -> in 2015-05-28 11:21:01 +00:00
91c3590155 Migrate settings to support-v4-preferencefragment, split out identities 2015-05-28 11:01:57 +00:00
474a47d0d4 Re-enable drawer toggle (accidentally removed during Toolbar migration) 2015-05-28 01:53:22 +00:00
59f26a3522 Upgraded build tools, bumped target SDK 2015-05-27 11:39:42 +00:00
ff838bbc33 Upgraded support libraries 2015-05-27 11:38:27 +00:00
60bc70219f Prevent NPE 2015-05-27 11:26:38 +00:00
8b98f38d5e Updated TODO with Silent Store checklist (useful reference) 2015-05-27 11:24:24 +00:00
11bd90a1d5 Fully clean I2P source 2015-05-27 11:23:10 +00:00
26333ee610 Upgrade to client library 0.5.1 which includes LogWriter 2015-03-02 11:15:28 +00:00
286ce0977f Necessary LogWriter changes after update to 0.9.18 2015-03-02 04:08:40 +00:00
5437999960 Upgrade Material-ish Progress library to 1.4 2015-03-02 02:21:03 +00:00
3315d6e6c2 Updated I2P client library to 0.5 2015-03-02 00:54:18 +00:00
2514e276c0 Update SKD build tools and dependencies, enable Java 1.7 features 2015-03-02 00:40:39 +00:00
6b246fb0d7 Updated README 2015-02-24 21:23:19 +00:00
c8d6703c86 Update TODO 2015-02-24 20:47:41 +00:00
b9d778f8fa Spelling 2015-01-13 03:29:01 +00:00
2ccbe875a8 Updated CHANGELOG 2015-01-13 03:26:17 +00:00
50b36e49b4 0.5
i2p.i2p-bote: 19d3dc33a474b094b84043cc3c36e186cf3d2338
i2p.i2p tag: i2p-0.9.17
2015-01-13 03:24:19 +00:00
4c8a33ec46 Fixed occasional crash 2015-01-13 02:52:42 +00:00
25533c4236 Formatting 2015-01-13 02:48:35 +00:00
1b8340b292 Fix CAB inserting above Toolbar
This causes the CAB to overlay the nav drawer too. Waiting on a fix:
https://stackoverflow.com/q/27663853/3317191
2015-01-13 00:43:38 +00:00
19329dad6e Loading and empty views for address book 2015-01-13 00:40:40 +00:00
227e6e9700 Checksum and license updates 2015-01-12 22:50:30 +00:00
0e580b8ebb Moved widgets to i2p.bote.android.widget 2015-01-12 22:42:46 +00:00
1a7302bb73 Loading and empty views for emails list 2015-01-12 22:34:09 +00:00
004987db71 Handle attachments with incorrect MIME types 2015-01-12 10:08:22 +00:00
2c6c43c3ac Updated translations 2015-01-11 09:51:50 +00:00
d8d1caaf2a Updated translations 2015-01-11 02:42:32 +00:00
6b8e41ba37 NPE fix 2015-01-09 23:20:44 +00:00
ef51264d4a Missing file 2015-01-09 02:21:52 +00:00
af21384fc1 Updated translations 2015-01-09 02:21:14 +00:00
c8ed705f1f Warn about large attachment sizes 2015-01-08 23:23:05 +00:00
9bde6402f4 Ensure ContactsCompletionView text is visible on all devices 2015-01-08 22:52:41 +00:00
7873cb40d3 New translations for help pages 2015-01-08 22:10:15 +00:00
b6e1242cc3 Updated translations 2015-01-08 22:09:33 +00:00
b947f9ba88 Use ripple effect on API 21+ 2015-01-08 12:52:39 +00:00
ae8f87c7da Protect ECUtils class methods 2015-01-08 11:54:53 +00:00
dab92a2227 Help page for identities 2015-01-08 11:45:28 +00:00
72fed007f7 Reworked about page for translations 2015-01-08 10:44:14 +00:00
fb58e63f04 Updated changelog 2015-01-08 09:37:10 +00:00
460e8d9280 Updated CHANGELOG 2015-01-08 02:54:48 +00:00
7130eae4d9 Help and About pages 2015-01-07 23:15:32 +00:00
fc8932646f Re-add list dividers 2015-01-07 05:53:16 +00:00
57cf36bed5 Missing file 2015-01-07 05:30:44 +00:00
4e4c57c321 Cache whether we have sent an email or not 2015-01-07 05:09:06 +00:00
45e4cba708 Unused import 2015-01-07 04:31:41 +00:00
2619d88c85 Migrate AuthenticatedListFragments to RecyclerView 2015-01-07 04:22:43 +00:00
7acbb22887 Missing file 2015-01-07 02:56:36 +00:00
ed600b13ee Fixed bug 2015-01-06 19:48:18 +00:00
03926923e2 Color comment 2015-01-06 11:33:18 +00:00
1dabe02684 Fixed selection state bug 2015-01-06 10:48:48 +00:00
0d15417432 Nav drawer folder selection styling 2015-01-06 10:39:27 +00:00
0763d2ea0a Migrated folder list to RecyclerView 2015-01-05 12:15:15 +00:00
5502561454 Pull layout into XML 2015-01-05 06:25:50 +00:00
38fc0ade86 View holder pattern 2015-01-05 06:18:22 +00:00
e9d55fb8ad Cleanups 2015-01-05 05:42:05 +00:00
ff1984e349 Updated translations 2015-01-05 04:34:20 +00:00
16f56f598d Updated CHANGELOG 2015-01-05 04:33:09 +00:00
e4350f1da2 Bugfix: on legacy devices, Views that are GONE are still clickable 2015-01-05 04:29:43 +00:00
8bb9ceacf0 Put identities inside category on legacy devices
A PreferenceCategory in settings_headers_legacy.xml was not being rendered, so
it is added manually instead.
2015-01-05 04:14:59 +00:00
1071116b04 Dynamic Preferences for legacy devices 2015-01-05 04:04:23 +00:00
dc844ab12a Implement legacy identity headers in Setting 2015-01-05 01:36:17 +00:00
7d3cd077d1 Use SpongyCastle for crypto
Requires i2p.i2p-bote rev 474ac06823cf555f824fdf914fe40247e38eab55
2015-01-04 22:21:06 +00:00
ced8dee1dc No need to stub out UpdateChecker any more 2015-01-04 22:18:10 +00:00
08748f0c69 Updated translations 2015-01-02 13:28:46 +00:00
15d8bb214f Updated TODO 2015-01-02 13:20:34 +00:00
b6cdf3aa33 FloatingActionBar 1.5.1 2015-01-02 13:20:22 +00:00
ee9e33c938 Separate multiple attachments 2015-01-02 04:41:07 +00:00
4bb1cc949e Removed mistaken string 2015-01-02 04:34:27 +00:00
bc9d067670 Updated translations 2015-01-02 04:31:44 +00:00
2fb6fdab4f Replace copy image with button, extract strings 2015-01-02 04:30:02 +00:00
4b0e78a383 New delivered icon, tweaked email listitem layout 2015-01-02 02:57:15 +00:00
93dc161e97 Updated CHANGELOG 2015-01-02 00:09:45 +00:00
a012a0ca41 Wording 2015-01-02 00:07:06 +00:00
b003143ad7 Layout tweak 2015-01-02 00:05:08 +00:00
fbcbdd8f95 Updated translations 2015-01-01 23:15:56 +00:00
d041bee10a Save attachments to Downloads folder 2015-01-01 23:13:00 +00:00
d40dce7dda Moved copyStream() to BoteHelper 2015-01-01 23:11:48 +00:00
01a50f72ca Missing file from previous commit 2015-01-01 11:49:07 +00:00
cfd62accee Stubbed out attachment saving 2015-01-01 11:48:45 +00:00
1d2f728bb5 Fixed AttachmentProvider.getType() 2015-01-01 11:02:16 +00:00
c1f2c8f6f6 AttachmentProvider.getType() tests 2015-01-01 10:57:17 +00:00
c3c2314ecd Fixed attachment creation in tests 2015-01-01 10:36:33 +00:00
36f104bab4 Start making tests pass 2014-12-31 03:31:13 +00:00
3c81bd0e8f AttachmentProviderTests 2014-12-31 02:11:36 +00:00
a303dcc36f FloatingActionBar 1.4.0 2014-12-30 12:07:49 +00:00
fdb2a903c8 Updated verification hashes 2014-12-30 11:54:45 +00:00
321aba11cc Testing dependencies etc. 2014-12-30 11:50:28 +00:00
3fc665e5ad Layout changes 2014-12-30 01:22:46 +00:00
e021bd3fa1 Implemented AttachmentProvider, removed ContentAttachment.getUri() 2014-12-29 10:50:04 +00:00
b05dcd8ebe Prep work for viewing attachments
Todo: implement a ContentProvider for attachments
2014-12-29 00:05:00 +00:00
5ffdf2018d Added missing files 2014-12-29 00:01:41 +00:00
5520a36e60 Padding fix 2014-12-28 23:59:13 +00:00
612419cd32 Add/remove attachments in new email, list attachments in view email 2014-12-28 20:16:06 +00:00
07ad5bdd08 Updated attachment icon on email list items 2014-12-28 11:48:12 +00:00
cbfd318cf3 Move log tag to Constants 2014-12-27 23:14:34 +00:00
d64613c0fc Updated translations 2014-12-27 13:47:58 +00:00
b97fe8f5b5 Updated CHANGELOG 2014-12-27 13:47:10 +00:00
e310a3b54e Add support for Cc: and Bcc: 2014-12-27 13:46:19 +00:00
0deaf2aecd Updated CHANGELOG 2014-12-25 10:57:37 +00:00
ab767a03d2 Selectable email content (API 11+) 2014-12-25 10:56:38 +00:00
5b0d631bd3 NPE fix 2014-12-23 21:57:58 +00:00
b4e154e0f4 Fixed string 2014-12-23 11:08:56 +00:00
d0377970d8 Updated translations 2014-12-23 11:08:50 +00:00
a1a89dd2bc Updated TODO 2014-12-22 00:23:06 +00:00
51ab2e7aaa Updated CHANGELOG 2014-12-19 22:00:18 +00:00
124fdeb623 0.4
i2p.i2p-bote: 455772b2a5e9f7ed13bdd6bb883d2a471092627f
i2p.i2p tag: i2p-0.9.17
2014-12-19 21:59:40 +00:00
cdf91568ad Moved release signing into separate buildscript 2014-12-19 20:48:10 +00:00
780171dd86 Upgraded build tools to 21.1.1 2014-12-19 20:40:57 +00:00
36b7ca0714 Upgraded support libraries to 21.0.3 2014-12-19 20:38:58 +00:00
2c8d7209b3 Permission to vibrate 2014-12-19 20:10:52 +00:00
45172e410e Updated translations 2014-12-19 20:08:35 +00:00
d91401ef0b Updated translations 2014-12-19 04:27:06 +00:00
00c6af0c2b Updated TODO 2014-12-19 04:06:32 +00:00
d8785d5c28 New email notification fixes 2014-12-19 04:06:14 +00:00
07c9fdfebb Fix settings menu Intents in debug build 2014-12-17 02:39:50 +00:00
92a22c447a Consistent titles 2014-12-17 02:33:56 +00:00
88106ab5f2 Visually break up emails in ViewPager 2014-12-17 02:08:42 +00:00
014932d06f Notify users that Bote needs to be connected to check emails 2014-12-17 02:05:25 +00:00
4d7f5bfe41 Proper error page 2014-12-17 01:53:05 +00:00
4124e2e336 Fixed new email notifications 2014-12-16 11:44:11 +00:00
e8a927e30a Don't modify UI from email-sending thread 2014-12-16 08:13:51 +00:00
bcfd6193c3 New translations for ro 2014-12-16 06:11:11 +00:00
05064a34bf Updated translations 2014-12-16 06:09:23 +00:00
2216f64864 More robust email checking 2014-12-16 06:07:55 +00:00
4ca77a366d Updated TODO 2014-12-13 04:53:02 +00:00
80f08c0435 Updated CHANGELOG 2014-12-13 04:31:53 +00:00
f45eae9c8f Updated translations 2014-12-13 04:29:29 +00:00
e52de5d9ef Copy EmailDestinations to clipboard 2014-12-13 04:27:32 +00:00
f151f55a2a Only call connectNow() if status==DELAY; always call when I2P status==ACTIVE 2014-12-12 23:08:58 +00:00
848ecbef9a Updated CHANGELOG 2014-12-10 12:54:41 +00:00
02e00d7fb3 Prevent NPE 2014-12-10 12:16:48 +00:00
c38feed198 Updated translations 2014-12-10 11:17:26 +00:00
dc588fe2b7 Updated CHANGELOG 2014-12-10 11:13:03 +00:00
b3350ce395 Missing file from previous commit 2014-12-10 11:08:41 +00:00
65f08049cf Labels in address book FAM (using FAB library 1.3.0) 2014-12-10 11:08:06 +00:00
9411805eec Bumped gradle plugin to 1.0.0 2014-12-09 12:21:18 +00:00
6a38ff1d68 Enable debug versions to be installed alongside release versions 2014-12-04 23:48:12 +00:00
2230f9b0ed Fixed NPE 2014-12-04 22:57:58 +00:00
56811ad9b1 Updated CHANGELOG 2014-12-04 22:56:17 +00:00
3b42807509 0.3
i2p.i2p-bote: bd9c18f038fda8cb52bf9a1f27c65db19b826106
i2p.i2p tag: i2p-0.9.17
2014-12-02 00:39:28 +00:00
d917fb554b Upgrade to client library 0.4, use new helper bind method with new Intents 2014-12-01 10:04:41 +00:00
a3b595b4fa Updated translations 2014-12-01 04:09:20 +00:00
10f0593f0c Updated translations 2014-11-30 11:46:32 +00:00
9dee45cba3 Users don't need to see the local Destination 2014-11-30 11:43:38 +00:00
aee45ce2c5 Upgraded support libraries to 21.0.2 2014-11-30 11:37:33 +00:00
39f04b7f52 Tag strings for translation 2014-11-30 11:36:59 +00:00
67563a2adb Updated TODO 2014-11-26 22:37:01 +00:00
7463470609 Pie charts on network info page 2014-11-26 22:30:53 +00:00
be67702e61 Extract common code for viewing identities and contacts
Side-effect: view contact page now shows QR code
2014-11-25 04:45:13 +00:00
d9b1aa30b3 Use same layout for viewing identities and contacts 2014-11-25 04:12:06 +00:00
07e2067ee6 Updated Gradle Witness
Source: https://github.com/WhisperSystems/gradle-witness
Git commit: 10f1269c0aafdc1d478efc005ed48f3a47d44278
2014-11-25 03:51:13 +00:00
1df677daa5 Zoom QR code on click 2014-11-25 03:46:37 +00:00
6c10e0934c QR code scanning 2014-11-25 03:09:05 +00:00
287358277c Icon for scanning QR codes 2014-11-25 02:21:24 +00:00
c8f9671b62 Move add contact action to FAM 2014-11-25 01:31:52 +00:00
872fa2e99c Don't allow EmailDestination of an existing contact to change 2014-11-25 01:11:35 +00:00
0ba87f00cc NFC: check received message, receive on API 9 2014-11-25 00:49:33 +00:00
743055a121 Updated translations 2014-11-19 20:06:31 +00:00
35030b1d86 Updated CHANGELOG 2014-11-19 07:17:02 +00:00
00aad9fd57 View identity: Generate and show QR code 2014-11-18 11:14:33 +00:00
93f4caeb4c Divider styles 2014-11-18 09:56:05 +00:00
5c1a96237f 0.3-rc5
i2p.i2p-bote: bd9c18f038fda8cb52bf9a1f27c65db19b826106
i2p.i2p:      cb66382d9716f7d9cd9441830face910705253e0
2014-11-18 09:50:42 +00:00
a6df621472 Specify version in build.gradle 2014-11-18 07:00:07 +00:00
f530515e2e New email: If the user has typed anything, confirm when they navigate away 2014-11-18 06:55:46 +00:00
03870e17d8 Missing file 2014-11-18 06:46:40 +00:00
d3df8b4929 Better name handling for email replies 2014-11-18 04:00:21 +00:00
11acd45092 Don't include Anonymous in Reply All 2014-11-18 03:28:19 +00:00
05c65c6a40 Increase identicon density 2014-11-18 03:10:31 +00:00
98f7cad19c Better handling of identicon backgrounds 2014-11-18 03:07:01 +00:00
4fa16cfdef View contact: layout changes, removed ShareActionProvider (for now) 2014-11-18 02:11:25 +00:00
be88c7b074 Layout cleanup 2014-11-18 02:11:10 +00:00
60daaa3323 View identity: layout changes, removed ShareActionProvider (for now) 2014-11-18 01:58:22 +00:00
5ffdc62e7f Action bar title fixes 2014-11-17 04:09:52 +00:00
ef96c63e51 View email text styles and sizes 2014-11-17 04:01:46 +00:00
7a06c59584 Divider between email header and content 2014-11-17 03:40:09 +00:00
d2d1e95684 Max navigation drawer width 2014-11-17 01:03:55 +00:00
620daab952 View email layout tweaks 2014-11-17 01:03:36 +00:00
0c20f936d5 Show identicon for anonymous sent emails 2014-11-16 21:48:15 +00:00
d82bb57504 More uniform identicons 2014-11-16 21:39:31 +00:00
48db57f1c0 Make identicon visible in contact token 2014-11-16 20:16:31 +00:00
adb14287a8 Email list item layout fix 2014-11-16 20:16:10 +00:00
8bb939c3a5 Material design: text appearance 2014-11-15 01:47:53 +00:00
80a5e0dfd6 Toolbar for settings 2014-11-11 04:34:43 +00:00
5208cb4d88 SHA256 hash for net.i2p.android.ext:floatingactionbutton:1.1.0 2014-11-08 10:39:16 +00:00
ce4eb711b4 Use client library 0.3 2014-11-02 01:49:01 +00:00
a9ac5b6a0a Use mavenLocal() repository instead of a local file repo 2014-11-02 01:42:21 +00:00
a1401e8f37 Upgraded build tools to 21.0.2 (fixes a compile bug) 2014-10-30 23:14:51 +00:00
ff0d4f1d67 Upgraded build tools to 21.0.0 (needed to fix a resource merging bug) 2014-10-30 07:40:06 +00:00
395187ba3f New Gradle Witness build
Includes a fix for Gradle 2.+ that is not yet pulled in:
https://github.com/WhisperSystems/gradle-witness/pull/3
2014-10-30 07:23:05 +00:00
4a432bf51c Use our own, backwards-compatible version of the FloatingActionBar library 2014-10-30 07:22:03 +00:00
90f0ecab0e Upgraded gradle wrapper and build tools 2014-10-30 07:21:13 +00:00
e93e200327 Removed old translations 2014-10-30 07:20:22 +00:00
329e8e9201 Material design: contact chips 2014-10-27 06:35:42 +00:00
10d46edd06 Fixed deprecation 2014-10-26 20:45:36 +00:00
622c9b8d6f FloatingActionBar 1.1.0 2014-10-26 01:16:42 +00:00
60e6b1cd8d Materail design: Floating action button for new email action
This commit breaks automatic builds that pull dependencies from Maven Central,
because com.getbase:floatingactionbutton:1.0.0 has a minSdkVersion of 14. Local
testing indicates the library operates fine on API 10.

Builders will need to pull the source, change the minSdkVersion to 9, and then
run `gradle installArchives` to build and install the library locally. Then add
mavenLocal() to allprojects{repositories{}} in build.gradle.

An issue has been raised requesting official support for API 9+:
https://github.com/futuresimple/android-floating-action-button/issues/11
2014-10-23 03:54:01 +00:00
6a266bc2d1 Material design: Left side nav should open over toolbar 2014-10-22 21:32:29 +00:00
d9e3c179f3 Updated TODO 2014-10-22 11:31:03 +00:00
e1550147cd Identicons for EmailDestinations with no associated picture
Identicon generation code adapted from Android-Identicons
Source: https://github.com/saiimons/Android-Identicons
License: Apache License, Version 2.0
2014-10-22 11:28:05 +00:00
9ab2118110 Material theme: side nav width, toolbar height 2014-10-21 20:00:13 +00:00
11fbae85c4 Change email checker indicator colors 2014-10-21 11:18:11 +00:00
7b4d7de2a5 New network status icons 2014-10-20 10:54:28 +00:00
1f074bce74 Changed outbox icon 2014-10-20 10:00:49 +00:00
956469e7ce Material theme: nav drawer icons 2014-10-20 04:53:41 +00:00
5e762d8063 0.3-rc4 2014-10-20 03:29:20 +00:00
0add04fab0 Material theme: text appearance 2014-10-20 00:35:38 +00:00
2230ab2574 Changed accent color 2014-10-19 23:50:15 +00:00
77ebb06e7d Close drawer when opening address book or network status 2014-10-19 23:44:46 +00:00
c70ad18979 Line accidentally removed 2014-10-19 23:35:03 +00:00
0380b5c287 Material theme: main toolbar 2014-10-19 23:22:39 +00:00
486b06eec0 Material theme: contact list 2014-10-19 11:01:59 +00:00
a2d5d0e8c9 Material theme: email list 2014-10-19 10:52:00 +00:00
5818ab1551 Missing line 2014-10-19 10:11:30 +00:00
44bb19b6a8 Material theme: folder list 2014-10-19 10:06:20 +00:00
2f55af22f9 Material theme: new styles 2014-10-19 09:44:31 +00:00
d7ac0f7a42 Material theme: new icons 2014-10-19 09:41:08 +00:00
e5f9630d99 Upgraded to API 21 2014-10-19 09:39:28 +00:00
2ebf8b7155 Picture styling for identity and contact view pages 2014-10-18 05:06:28 +00:00
fb2e275f06 Updated translations 2014-10-16 04:09:37 +00:00
7b9716834a Use released client library 0.2 2014-09-30 01:51:46 +00:00
8cb3435d22 Updated translations 2014-09-29 04:07:35 +00:00
72ca8e737f Use new helper method in client library 2014-09-26 12:40:17 +00:00
8556aceae5 Plurals fix 2014-09-26 03:23:05 +00:00
1408601ea2 Use client library 0.2 2014-09-26 01:54:36 +00:00
d307e60dad Use the new i2p.i2p-bote directory structure 2014-09-26 01:54:12 +00:00
e48e76483b Different sent date formats for different ages of email 2014-09-25 04:17:59 +00:00
40efa738b2 Updated translations 2014-09-25 02:04:51 +00:00
d8741fa691 Only show incomplete email count in inbox 2014-09-25 01:57:57 +00:00
8a2da003e1 Don't create a new fragment when selecting the current folder in nav drawer 2014-09-25 01:56:17 +00:00
b9c4783a94 Placeholder colors for SwipeRefreshLayout 2014-09-25 01:32:10 +00:00
1ab29db2bc Nav drawer design guidelines 2014-09-25 01:31:45 +00:00
cb8502bd25 Plural string fixes 2014-09-25 01:07:42 +00:00
de64f09c50 Plural strings 2014-09-25 00:45:10 +00:00
db85557308 Updated translations 2014-09-24 14:23:25 +00:00
fa7b63e9d9 Settings improvements 2014-09-24 14:16:49 +00:00
7a92548fcf Updated TODO 2014-09-24 12:58:36 +00:00
27fcd7c31c Initial CHANGELOG 2014-09-24 12:57:38 +00:00
2a18db4859 Check email action; only allow checking email from inbox and when authenticated 2014-09-24 12:56:59 +00:00
f20b3f6743 Make empty inbox swipeable 2014-09-24 12:41:38 +00:00
402ab84a31 Switch to android.support.v4.widget.SwipeRefreshLayout 2014-09-23 11:52:07 +00:00
c22ae24786 Updated translations 2014-09-19 14:02:12 +00:00
2219dbc8d7 Remove unused string 2014-09-19 14:00:25 +00:00
0c3d2bab8d Password check in edit contact page (because it could be opened by an Intent) 2014-09-19 13:58:55 +00:00
e061f17864 Use AuthenticatedListFragment for AddressBookFragment 2014-09-19 13:49:46 +00:00
768162aeb6 Methods that should be private 2014-09-19 13:40:07 +00:00
7d51dc59a3 Split out authentication into abstract class 2014-09-19 13:38:49 +00:00
b808607050 Show keyboard with password dialog 2014-09-18 22:39:01 +00:00
59a6d586d1 "Log out" is a better user hint than "Clear password" 2014-09-18 22:27:42 +00:00
e303214a50 Clarify how to log in 2014-09-18 22:23:38 +00:00
b8d33ddd43 New translations for pl 2014-09-18 21:06:44 +00:00
a917655852 Updated translations 2014-09-18 21:06:26 +00:00
4dd9156eac Missing lock and unlock icons 2014-09-18 06:22:25 +00:00
b31f503fa4 Update folder list on set/clear password (for unread email count) 2014-09-18 06:20:39 +00:00
f9640c6fb2 Better action management 2014-09-18 05:56:07 +00:00
ba2b9bad8b Remove pop-up login request from email list 2014-09-18 04:36:13 +00:00
e4f2b9b960 "Log in" and "Clear password" actions on email list 2014-09-18 04:27:13 +00:00
0420aa2943 Improved errors 2014-08-30 09:37:30 +00:00
d86ab3466f Fixed NPE 2014-08-30 07:37:14 +00:00
be4e346ff0 Updated translation strings 2014-08-28 14:50:31 +00:00
5fc1d706f9 Updated translations 2014-08-28 13:04:16 +00:00
eba5b4f415 Bugfix: initialize list after authentication 2014-08-28 12:59:44 +00:00
e6348ace18 Forward and Reply all 2014-08-28 12:46:05 +00:00
bf439f213f Hide fingerprints from UI until they are reliable 2014-08-28 10:59:24 +00:00
18de25d75d Fingerprint field for contacts, not filled yet
The regular Fingerprint doesn't work here because it requires the salt, which is
not part of the public Destination. The salt is published with the Destination
in the public address book, and could be conveyed between Bote apps via NFC or
QR code, but that doesn't work for Destinations published in other spaces.
2014-08-28 06:37:58 +00:00
8013783b30 Added view contact activity 2014-08-28 05:42:10 +00:00
a9734ae393 Missing disableForegroundNdefPush() 2014-08-28 03:57:02 +00:00
0e5ae34f82 Show identity fingerprint in current locale if possible 2014-08-28 03:52:25 +00:00
ce74614ba5 Don't allow password dialogs to be canceled by the back button 2014-08-28 03:16:07 +00:00
ecd2667281 Authenticate for identity creation 2014-08-28 03:15:50 +00:00
b6e74b7458 Comment fixes 2014-08-28 03:03:15 +00:00
502e9ddfba Italics for Anonymous senders 2014-08-28 01:53:29 +00:00
d6bc72d9d5 Layout tools fix 2014-08-27 22:17:57 +00:00
3240d04e44 Show recipient for emails listed in Outbox and Sent 2014-08-27 05:27:03 +00:00
29025191f6 Updated TODO 2014-08-27 05:26:39 +00:00
6c8fbeb3be Updated TODO 2014-08-27 04:26:48 +00:00
d1db7c14fc View identity:
- Show fingerprint (en)
- Generate QR code from identity key
2014-08-26 05:57:56 +00:00
a352c138e8 0.3-rc3 2014-08-23 01:53:37 +00:00
7d2cc86920 Docstring fix 2014-08-23 01:43:52 +00:00
99d2dc2c62 Disable hidden mode for internal router (participation still disabled) 2014-08-23 01:41:42 +00:00
b22b8eea50 0.3-rc2 2014-08-23 01:25:46 +00:00
24ab394cb4 New translations for zh 2014-08-22 11:24:04 +00:00
c9269ca4bf Updated translations 2014-08-22 11:23:36 +00:00
11909ce6df Use client library 0.1.1 2014-08-21 12:13:10 +00:00
53a540d0b7 Compiled libscrypt.so for x86 and mips using NDK r10 2014-08-20 23:13:26 +00:00
f9f4c17cd2 Recompiled libscrypt.so with NDK r10 to fix text relocations 2014-08-20 22:49:49 +00:00
d53f5c0ac7 0.3-rc1 2014-08-20 11:27:37 +00:00
48134404a2 Added comments to translation strings 2014-08-20 10:59:47 +00:00
d0247e7ca2 Feature graphic for Google Play 2014-08-20 06:40:54 +00:00
1b5691734e Fixed bug caused by API changes 2014-08-20 05:28:53 +00:00
398e4f6bad New translations: id, nb, sq 2014-08-20 05:27:43 +00:00
4148bad064 Missing change 2014-08-20 05:26:26 +00:00
6e632a4748 Don't let a user accidentally exit the setup wizard if they chose to use it 2014-08-20 05:26:10 +00:00
f316933fde Don't show Share menu for new contacts 2014-08-20 05:07:29 +00:00
208c69fb15 Hide old password field in SetPasswordFragment if none is set 2014-08-18 11:56:11 +00:00
ee926ff23b More TODO items 2014-08-18 11:55:26 +00:00
6b990f6feb Request password in onActivityCreated(), not onResume()
This prevents the dialog being re-displayed when returning to EmailListFragment
from another Activity.
2014-08-18 08:58:49 +00:00
46d42210ad Password handling:
- Init lists once in onResume (handles passwords entered higher in stack)
- Destroy any existing data in UI if cached password is cleared
2014-08-17 12:40:46 +00:00
dcec72a5db Password restrictions 2014-08-17 12:02:30 +00:00
bafe5293ac Modularize password entry 2014-08-17 11:36:17 +00:00
b8cb30aff2 Don't leak incomplete email count outside password 2014-08-17 10:47:20 +00:00
cf4dfad946 Make it easier to use a local copy/build of the client library 2014-08-15 01:49:45 +00:00
ca8399104d Updated translations 2014-08-07 07:00:23 +00:00
a44af32ebf New translations for ru 2014-07-26 02:46:20 +00:00
16c23e9506 Updated translations 2014-07-26 02:45:35 +00:00
ea4f5f50eb I2P Android now using Enum for State 2014-07-17 01:03:07 +00:00
8128d0a075 jstl.jar is a more reliable test, from source only jarBote will prepare it 2014-07-16 05:16:25 +00:00
a5d9e6a3ed Switch to I2CP over domain sockets for I2P Android 2014-07-16 04:13:19 +00:00
4172d483cc Use the I2P Android client library
The build script looks for the client library in Maven Central. It is possible              
to use a local .aar by placing it in a folder "aars" in the base folder, and
uncommenting the flatDir{} section in the base build.gradle.
2014-07-16 01:45:14 +00:00
9283840793 Better control of what artifacts are built by what task 2014-07-15 06:31:05 +00:00
d896833aa8 Updated TODO 2014-07-15 06:28:18 +00:00
4430b1c8e0 Cleaner build script 2014-07-11 05:20:35 +00:00
1d351132b2 Updated translations 2014-07-10 22:43:26 +00:00
173bc97301 New translations for fr 2014-07-10 14:45:13 +00:00
b2d3fd969e String improvement (thx Towatowa441) 2014-07-10 14:43:14 +00:00
f6bebfa51d Don't abort on lint errors (this is the 0.2 release commit) 2014-07-10 02:32:20 +00:00
b6010798ad Fixed overflowing text on small screens 2014-07-10 02:29:58 +00:00
854b2c67e6 0.2 2014-07-10 02:17:02 +00:00
a661f48dd0 Made intro_4.svg more Bote-like 2014-07-10 02:00:55 +00:00
73e4413508 Missing checkin 2014-07-10 01:33:37 +00:00
28186b0d43 Updated translations 2014-07-10 01:23:16 +00:00
31628c133b Reverted mistaken commit 2014-07-10 00:58:39 +00:00
494f36ed8d Updated translations for changed strings 2014-07-10 00:49:37 +00:00
29f4c1bd5e Improved intro text 2014-07-10 00:45:35 +00:00
244a91fb1a Fixes 2014-07-10 00:45:00 +00:00
ef98b16e15 Adding in another intro page 2014-07-09 09:13:48 +00:00
fc991d6a98 Adding in another intro page 2014-07-09 09:08:49 +00:00
b235f36cd0 Swap order of intro pages 2014-07-09 08:21:16 +00:00
c38569b4ac Swap order of intro pages 2014-07-09 08:19:16 +00:00
7f2e332248 Updated TODO 2014-07-09 04:56:15 +00:00
eb774b0194 Exclude x86_64 native binaries from scrypt.jar 2014-07-09 04:55:39 +00:00
6327d1f524 Fix email selection by clicking picture 2014-07-09 04:45:19 +00:00
0470939c14 ScrollView around intro pages that might overflow text 2014-07-09 01:25:36 +00:00
21dfd34ff6 Adapted images from the FSF Email Self-Defense guide for use in intro 2014-07-09 01:22:30 +00:00
bd56715ed4 Updated translations 2014-07-08 22:04:42 +00:00
5a84ceccc3 Error message has no info (yet), revert to e.toString() 2014-07-08 13:02:32 +00:00
68ad7757c5 Extracted more strings 2014-07-08 12:39:10 +00:00
22e27fdfe5 Updated translations 2014-07-08 12:32:19 +00:00
d66e277528 Updated ignores 2014-07-08 12:25:33 +00:00
8158bcec27 Clearer launcher icon, new notification icon 2014-07-08 12:23:36 +00:00
63b347292d Fixed locale hiding in replies 2014-07-08 00:22:04 +00:00
17fbe74fb1 New translations for es 2014-07-08 00:07:40 +00:00
eeeb214041 Extracted remaining strings 2014-07-08 00:05:40 +00:00
bc9f4d759a Make Inbox incomplete emails header unselectable 2014-07-08 00:05:00 +00:00
b508c3822b New translations for de 2014-07-07 23:29:15 +00:00
5b480d83c8 Updated license info 2014-07-07 23:14:54 +00:00
08c9005070 Use MultiSelectionUtil from Android Samples for CHOICE_MODE_MULTIPLE_MODAL
Fixed:
- Background highlighting

Broken:
- Selecting emails by clicking pictures
- Tick appearing over picture of selected emails
2014-07-07 13:35:11 +00:00
c05228c206 Ellipse character 2014-07-07 07:42:47 +00:00
33d35010f2 Don't connectNow() on RUNNING, too soon to know that I2CP is ready 2014-07-07 01:38:19 +00:00
4d795e264e Revert to only showing intro/setup on first run 2014-07-07 00:28:42 +00:00
399aa163ea Unified layout margins 2014-07-06 22:57:07 +00:00
9a0f5a5ee1 Stub out UpdateChecker 2014-07-06 22:18:32 +00:00
cdf01bc9e7 Bugfixes, run I2PBote.shutDown() in separate thread 2014-07-06 14:39:15 +00:00
c8212b7df9 Use NetworkStatusListener for nav drawer status info 2014-07-06 14:33:01 +00:00
ce4136a224 Update service Notification text via NetworkStatusListener 2014-07-06 13:06:14 +00:00
64be0e5240 Persistent notification while Bote is running 2014-07-06 10:37:25 +00:00
47a2c31196 Support different I2P Android package names 2014-07-06 09:27:10 +00:00
19e58d223c Improved support for landscape and small screens in intro and setup 2014-07-06 06:17:59 +00:00
21d87a953b Support different I2P Android package names 2014-07-06 03:14:49 +00:00
869042fb93 Updated TODO 2014-07-06 02:58:03 +00:00
9b45832a5d Pull out hard-coded strings, inform user if importer found no identities 2014-07-06 02:56:42 +00:00
7eaf913a36 Updated TODO 2014-07-06 02:07:46 +00:00
89fb236ba1 Easy Share the EmailDestination of identities and contacts 2014-07-06 02:05:56 +00:00
0b6c96f48b Implement append strategy for duplicates 2014-07-05 23:50:35 +00:00
5dc2572989 Environment.DIRECTORY_DOCUMENTS is only available in API 19+ 2014-07-05 04:14:07 +00:00
9b1f29be64 Import identities UI 2014-07-05 04:12:56 +00:00
9aecce7cc4 Fixed reading contact Destination from text file 2014-07-05 01:42:48 +00:00
a889246066 Export identities UI 2014-07-04 23:53:21 +00:00
24e66a08fd Prepare for export dialogs 2014-07-04 10:55:36 +00:00
044d190c09 Export identities to Documents folder 2014-07-04 06:21:56 +00:00
dcba9109c8 Updated checksum for com.mcxiaoke.viewpagerindicator:library:2.4.1 2014-07-03 13:18:25 +00:00
9c552e0a8a Fixed README 2014-07-03 12:44:15 +00:00
af6ceddbd5 Send identity destinations to other devices via NFC 2014-07-03 12:32:03 +00:00
35d103ee58 Send address book contacts between devices via NFC 2014-07-03 12:23:35 +00:00
b61a15b4dd Updated TODO 2014-07-03 12:22:45 +00:00
697b29d189 Show number of incomplete emails at top of Inbox 2014-07-02 06:55:59 +00:00
7607e2d1f3 Ensure that I2P system vars are set in any Activity that uses I2PBote
Sometimes the Android runtime kills off the Bote process to save memory, and
recreates it when the user next opens it. If the user was on an Activity that
was not EmailListActivity, then when recreated the I2P system vars would not be
set, and the first call to I2PBote.getInstance() would create an instance with
invalid paths. This was non-fatal - killing Bote and restarting it would fix
the problem - but was bad UX, because from the user's PoV all their emails and
data had disappeared.
2014-07-02 03:01:28 +00:00
4d736205b3 Separate message to explain why Bote waits for 3 minutes 2014-07-02 02:36:43 +00:00
8f8fd90851 Move app initialization out of EmailListActivity so it can be used elsewhere 2014-07-02 01:39:50 +00:00
6b11bae3a0 Show contact pictures in recipients dropdown 2014-07-02 00:38:47 +00:00
d3ddca0d4d Static verification of remote dependencies using Gradle Witness
https://github.com/WhisperSystems/gradle-witness
2014-07-01 05:13:50 +00:00
7397c04538 Updated android gradle plugin version 2014-07-01 05:12:55 +00:00
1ce9c6072e Updated README 2014-06-29 02:58:46 +00:00
d647f310e3 Enable building Bote against I2P source
Requires I2P source revision 116c7da67b5bb23b91f9fb55980ceb9865ae7d0a or later
(ie. cannot be built against 0.9.13 source, must use an install for that).
2014-06-28 05:59:52 +00:00
27c086a2dc Signing instructions 2014-06-28 04:21:35 +00:00
ce8e26c109 Improved handling of signing keys 2014-06-28 03:58:22 +00:00
2736ba7627 Temporary fix for broken developer preview of Support Library 2014-06-28 03:37:23 +00:00
076f595abf Fixed jar dependency problems, updated ignores 2014-06-28 02:39:53 +00:00
df7a35934a README fixes 2014-06-28 02:16:11 +00:00
4bb52983be Describe how to pull dependencies via Tor 2014-06-24 05:52:00 +00:00
15f3b54000 License info 2014-06-23 02:11:52 +00:00
a6bb88567f Parent activity meta-data for 4.0 and below 2014-06-20 08:56:30 +00:00
ec12db7cec 0.1.1 2014-06-20 08:44:43 +00:00
731bde55f3 Prevent NPE with FragmentManager not being initialized 2014-06-20 08:20:10 +00:00
b7295e983a Link to EditIdentityActivity on legacy devices 2014-06-20 02:10:56 +00:00
3fe669a738 Incremented version after dev release 2014-06-19 04:07:33 +00:00
f457ec3de5 Release signing 2014-06-19 04:07:08 +00:00
b5540e1711 Fixed some warnings 2014-06-19 03:32:32 +00:00
b684aaa535 Setup finished page, moved setup buttons to bottom of screen 2014-06-19 03:08:36 +00:00
ac61de07ff Fixed string 2014-06-19 02:48:31 +00:00
73acb4b689 Setup wizard for new users 2014-06-19 02:46:35 +00:00
b30e31fc81 Introduction to Bote for new users 2014-06-19 01:00:22 +00:00
79c3a622ea Committed missing override files for newer APIs 2014-06-19 00:35:02 +00:00
34107e6a62 Fixed ignores 2014-06-19 00:32:55 +00:00
d5864f6258 Added README with build instructions 2014-06-19 00:15:38 +00:00
4f8979d279 Automatic format changed from Android Studio 2014-06-18 20:10:24 +00:00
ad397605a6 Fixed B64 encoding of Bitmaps 2014-06-16 23:15:10 +00:00
5b347e878a Adjust email content margins 2014-06-16 06:30:28 +00:00
522be91528 Save pictures for identities 2014-06-16 06:30:10 +00:00
1c0cbff55a Fixed getPictureForAddress() 2014-06-16 02:27:39 +00:00
aa954b03ec Fixed wrong ImageView 2014-06-16 02:27:14 +00:00
6dcd309418 Picture layout fixes 2014-06-16 02:10:30 +00:00
947056d344 Updated to Android Studio 0.6.1 2014-06-16 00:51:21 +00:00
fafabe37bc Added default router.config based on I2P Android 2014-06-14 11:07:32 +00:00
237124df1e Removed Eclipse files 2014-06-14 07:36:03 +00:00
50faef08be Pull to check email 2014-06-14 07:11:35 +00:00
f97c96916f Fixed build process 2014-06-13 10:28:25 +00:00
0b689a5a54 Gradle build and settings files, updated ignores 2014-06-13 01:28:36 +00:00
2340a5f265 Reorganized directories 2014-06-13 01:12:43 +00:00
9691f3d169 Added gradle wrapper 2014-06-13 01:07:38 +00:00
c84870ccb0 Fixed duplicate string name 2014-06-13 01:03:03 +00:00
d8637a3b27 Move "New Identity" action to list item 2014-06-12 08:15:34 +00:00
50e909e806 Mark email subject input field to prevent newlines 2014-06-12 04:30:30 +00:00
879c5cc5a5 Quote subject and content when replying to emails 2014-06-12 04:23:09 +00:00
b2439f73d1 Pass full email through when replying 2014-06-12 02:54:07 +00:00
c480103c17 Outbox emails stay marked as unread until sent 2014-06-12 02:21:53 +00:00
f8a69849a8 Handle new vanity prefix parameter in I2P-Bote 2014-06-11 23:04:58 +00:00
5832416b91 Only allow moving emails from trash (for now), never allow move to outbox 2014-06-11 08:18:53 +00:00
1f8b87c4ef Moved from and sent lines to bottom 2014-06-11 08:11:08 +00:00
2e98d95213 Show contact picture in notification for single new email 2014-06-11 06:52:32 +00:00
54990b2a45 Moved sent date to second line (to not truncate subject line) 2014-06-11 06:14:30 +00:00
c84f1db03e Updated TODO 2014-06-11 05:53:40 +00:00
3b64f589f4 Connect to network immediately for remote router, or if I2P Android is running 2014-06-11 05:29:15 +00:00
5ca77f2d50 Change in i2p.i2p-bote 2014-06-10 12:18:13 +00:00
6fd09bccb9 Use NewEmailListener instead of FolderListener 2014-06-09 04:52:58 +00:00
0f15534e73 Fixed use of \Recent flag 2014-06-08 05:01:29 +00:00
850675dce5 Use \Recent flag to only notify user about newly received emails 2014-06-08 04:33:56 +00:00
0763c8cff2 Updated for new FolderListener API 2014-06-08 04:33:05 +00:00
5e94144578 String clarification 2014-06-06 08:53:23 +00:00
9885860e91 Notify on new emails
FolderListener is not ideal, it fires on any change. We only want to listen for
new emails arriving (not user marking emails unread), and each email should
only appear in a notification once.
2014-06-06 08:39:47 +00:00
ae731edfd9 Missing file 2014-06-05 04:31:09 +00:00
f1fb48860b Bugfix: redeliver Intent to BoteService if killed and then restarted 2014-06-05 01:26:18 +00:00
185ec7ff9e Updated TODO 2014-06-04 23:48:57 +00:00
4f6cbbaf3a Reply action 2014-06-04 23:48:26 +00:00
13583b4bef Fall back to internal router if I2P Android is pre-0.9.13 2014-06-04 21:20:33 +00:00
f77f273e30 Fixed variable name to match convention 2014-06-04 21:06:06 +00:00
7843f06142 Truncate subject and from lines to not overlap RHS fields 2014-06-04 20:15:16 +00:00
5a088ae969 Call correct startActivityForResult() 2014-06-04 19:57:26 +00:00
c2a9b5202a Updated TODO 2014-06-04 12:08:45 +00:00
eced9e6239 I2P Android integration 2014-06-04 10:28:29 +00:00
b8e4c719ab Method changes in i2p.i2p-bote 2014-06-03 07:56:23 +00:00
69b55a8e2d Refresh options menu when starting or stopping Bote 2014-05-29 10:03:40 +00:00
7d70d7c0df Bundle internal router, settings to choose internal or remote 2014-05-29 09:56:02 +00:00
f5732d699c Fixed parameter substitution in strings 2014-05-25 13:55:24 +00:00
eb54ff8dcf Show delivery percentage if zero 2014-05-23 03:18:12 +00:00
f228f64bd7 Show sending status in ViewEmailFragment 2014-05-23 01:58:39 +00:00
b3131d2462 Show sending status only in outbox; don't allow outbox messages to be moved 2014-05-23 01:44:48 +00:00
b2bd4158f4 Use the new flexible email status system 2014-05-23 01:19:01 +00:00
9a3f7ba541 Updated TODO 2014-05-22 13:32:20 +00:00
5a38238ec8 Fixed NPE in settings when rotating device 2014-05-22 13:32:00 +00:00
37c754d570 Option to delete identities 2014-05-22 12:54:03 +00:00
c08b15fc8f Option to delete contacts 2014-05-22 12:10:58 +00:00
95d3bfe01b Missing resources, removed placeholder text 2014-05-22 10:01:47 +00:00
e540c5733c Show entire email if external 2014-05-21 06:07:18 +00:00
26f96ed7f5 TODO bugfix 2014-05-21 01:56:30 +00:00
ccf1f92be7 Highlight address book and network info links in nav drawer when touched 2014-05-20 13:06:30 +00:00
374e1d71cf Require recipients 2014-05-20 12:58:39 +00:00
f4d845bb95 Fixed NPE (that should never occur, but there is an input validation bug) 2014-05-20 11:58:02 +00:00
36b46f6587 Show any connection error on network info page 2014-05-17 15:00:01 +00:00
15d0d0f950 Updated TODO 2014-05-17 13:12:24 +00:00
341db41207 Show email status image/text using a single TextView, stop content overlapping 2014-05-17 13:10:42 +00:00
2e556727c0 Improved sent mail detection, show email_status_text when set 2014-05-17 11:56:47 +00:00
d21d7d3fcb Show attachments and delivery status in email list 2014-05-17 06:54:58 +00:00
717c697fcc Don't open network info page if Bote has not started 2014-05-17 05:32:04 +00:00
9d6b4ce48f Update network status whenever drawer state changes, not only on swipes 2014-05-17 04:50:08 +00:00
df9c841606 Updated TODO 2014-05-17 04:33:07 +00:00
f7bba81669 Updated TODO 2014-05-17 03:46:35 +00:00
766b93fa85 Simple network info page 2014-05-17 03:46:27 +00:00
9b4ecf30df Service for starting and stopping Bote 2014-05-17 02:56:29 +00:00
7b39399128 Network status in nav drawer 2014-05-17 01:25:23 +00:00
791a16f36c More TODO items 2014-05-16 23:10:58 +00:00
3510264c56 Set relevant titles on pages 2014-05-16 23:09:09 +00:00
d8cc7278c0 More TODO items 2014-05-16 13:57:57 +00:00
22795c4224 Updated TODO 2014-05-16 13:52:11 +00:00
16ba7f0acd Show when an email is selected 2014-05-16 13:51:31 +00:00
cfd4107bd5 Click on email contact pictures to select them 2014-05-16 13:39:28 +00:00
d3b611d9a6 Mark viewed emails as read ("not new") 2014-05-16 13:03:33 +00:00
8278e51f4d Fixed NPE 2014-05-16 13:01:58 +00:00
14658aa9ea Picture fixes 2014-05-16 10:43:14 +00:00
18542be5f9 Backend for setting identity and contact pictures
Pictures are not saved correctly, something is corrupting them.
2014-05-16 09:33:28 +00:00
3628c50b24 Moved address book link into nav drawer 2014-05-15 10:56:30 +00:00
139557df55 Default image for contacts and identities with no picture 2014-05-15 10:50:12 +00:00
e58b3bd43a Correctly handle empty contact pictures 2014-05-15 05:52:21 +00:00
7b2c78cb09 Fixed edit contact button size 2014-05-15 05:45:52 +00:00
1ce3c6d6eb Simplified name 2014-05-15 05:37:01 +00:00
342f11ac9f Fixed string 2014-05-15 04:33:36 +00:00
770b1a85fa Integrated address book into new email recipients field 2014-05-15 04:29:53 +00:00
3d4d8e598b Added TokenAutoComplete library
Source:
https://github.com/splitwise/TokenAutoComplete

License:
Apache License, Version 2.0
2014-05-15 03:30:09 +00:00
34981a9980 Contact edit page 2014-05-15 02:54:32 +00:00
ac3032e707 Fixed drawable name 2014-05-15 02:53:18 +00:00
0cc2b5fe99 Helper for encoding picture 2014-05-15 02:53:01 +00:00
dd89b919ea Address book list 2014-05-15 00:36:00 +00:00
ee16f4416f i2p.bote -> i2p.bote.android 2014-04-19 00:18:33 +00:00
3ab1f1aa19 Implemented send button action with sender as dummy recipient 2014-04-17 23:38:25 +00:00
1f30eef5dd Updated TODO 2014-04-17 05:44:42 +00:00
61ead34a9a Mark emails read/unread, move to folder 2014-04-16 13:09:11 +00:00
a22d1c1f52 Changed LogWriter tag 2014-04-16 13:07:59 +00:00
f1191b2f68 Email deletion 2014-04-15 02:51:43 +00:00
4aa2bd8b74 Update folder list text on folder changes 2014-04-14 21:36:54 +00:00
5ed7cf5553 Include precompiled native scrypt library for ARM5 - big usability speedup
Source:
ed8025e140/src/android/resources/lib/arm5/libscrypt.so
2014-04-14 12:53:58 +00:00
6e00e398f6 Upgraded to scrypt 1.4.0 2014-04-14 12:21:42 +00:00
c8e10cd3b0 Added TODO 2014-03-13 12:29:40 +00:00
13cc068011 Tweaked layout 2014-03-13 04:12:24 +00:00
71808a71d3 Added view of identity 2014-03-13 03:49:19 +00:00
1c6a654b21 Disable edit fields when saving 2014-03-12 03:33:00 +00:00
703cd94f5a New identity creation 2014-03-09 23:06:00 +00:00
f9d54e10e0 Fixed NPE 2014-03-09 09:34:40 +00:00
49332e7875 Implement required API 19 method override 2014-03-09 09:25:52 +00:00
d6984910ef Set default identity for new email correctly 2014-03-08 23:57:33 +00:00
f967ead55f Set default identity option 2014-03-08 22:49:16 +00:00
47067564d5 Improved edit identity UI 2014-03-08 22:37:56 +00:00
1fcc80edb5 Improved set password UI 2014-03-08 20:54:04 +00:00
dc98c05479 Fixed <intent> bug (targetPackage is package of application, not class) 2014-03-08 20:53:05 +00:00
ec3fe21378 Added missing menu 2014-03-08 20:05:28 +00:00
02c93ed154 Added missing icons 2014-03-08 20:04:51 +00:00
5411043405 Fixed settings headers after file moves 2014-03-08 19:50:56 +00:00
7bafb9546e Start of new email activity 2014-03-08 12:52:41 +00:00
3154a6927a Moved config and identities Activities into subpackage 2014-03-08 03:05:02 +00:00
4993d172ee Added edit page for identities 2014-03-08 01:18:06 +00:00
0f0fedef65 Show contact picture in email list and view (if available) 2014-03-06 23:51:16 +00:00
d5e0bd8429 Added list of EmailIdentities to settings 2014-03-05 20:38:13 +00:00
5c45ac2a14 Use preference headers in settings (prep for identity support) 2014-03-04 10:52:50 +00:00
7e48a8e438 Remove router.jar dependency, stub out Seedless use 2014-03-04 01:26:40 +00:00
e4efc71d99 Recover current status of password setting on config change 2014-03-03 04:44:44 +00:00
6be946aeb8 Recover enabled state of submit button after config change 2014-03-03 04:38:12 +00:00
e3ad5f9564 Fixed crash if navigated back from SetPasswordActivity 2014-03-03 04:19:14 +00:00
e0af648df5 Make password-setting AsyncTask survive configuration changes (screen rotation)
Robust code derived from:
http://stackoverflow.com/a/12303649
2014-03-03 00:35:27 +00:00
ccdfdcf914 Display password set/change progress 2014-03-02 10:33:05 +00:00
44cbabaa8b Dismiss keyboard when done 2014-03-02 08:17:51 +00:00
cd5d912240 Use AsyncTask for password checking so UI doesn't hang 2014-03-02 03:58:46 +00:00
55b16d952c Support for changing passwords 2014-03-02 03:33:48 +00:00
c2d6b8d131 Added password handling and input 2014-03-01 21:20:16 +00:00
2fccdf3d34 Remove unneeded GSE catching 2014-03-01 02:25:09 +00:00
a22e27c0de Renamed classes 2014-03-01 01:46:30 +00:00
c6f68c5f3e Removed debug line 2014-03-01 01:11:05 +00:00
03270d8625 Stubbed out IMAP and SMTP 2014-03-01 00:15:31 +00:00
045c8393c7 Added I2CP host/port settings 2014-02-28 21:46:24 +00:00
e1513de85c Added settings 2014-02-28 11:05:03 +00:00
bd9ab1fc36 Updated ignore 2014-02-28 09:44:25 +00:00
14445b4948 Ellipsize content in email list 2014-02-28 07:53:22 +00:00
1685b2713c Enable ActionBar app icon to behave as action to go back 2014-02-28 07:49:27 +00:00
e07c73fa03 Fully display emails 2014-02-28 07:29:55 +00:00
2dc77be456 Populate ViewPager with Loader 2014-02-28 05:59:00 +00:00
b2470d544c Factored out common Loader methods 2014-02-28 01:33:15 +00:00
ee0e420423 Email viewing structure 2014-02-28 01:04:06 +00:00
b88dc979ea Sort emails newest-first 2014-02-28 01:03:35 +00:00
e07a5fa574 Moved BoteHelper to i2p.bote.util 2014-02-27 20:44:48 +00:00
21ed7bc1a4 Added missing file 2014-02-27 20:36:43 +00:00
d658a57e1a Show sent date if set, show new emails with bold subject and from 2014-02-27 10:14:21 +00:00
eb1aecd58e JavaMail currently doesn't work on Android, use javamail-android instead
https://code.google.com/p/javamail-android/
2014-02-27 09:32:49 +00:00
167f4752ca FolderAdapter -> FolderListAdapter, highlight selected folder 2014-02-27 08:44:12 +00:00
4b7e270422 Increase min API to 9 2014-02-27 04:44:42 +00:00
522915f577 Fixed duplicate fragment on configuration change 2014-02-27 04:42:21 +00:00
256884e2a3 Pass exceptions through from BoteHelper (like in GeneralHelper)
Todo: handle them.
2014-02-27 04:18:06 +00:00
7f0c026d0e Speed fix: only show new count when there is data to show 2014-02-27 04:11:28 +00:00
09f7681ff6 Fixed dependency issues, only compile Bote once 2014-02-27 03:59:10 +00:00
ab4b1df657 Fixes 2014-02-27 02:33:05 +00:00
dff9b06017 Start of email item design in list, basic styling 2014-02-26 23:30:46 +00:00
89e143123c Bugfix (crashed on folder not existing) 2014-02-26 23:12:55 +00:00
560780723d Extend i2p.bote.util.GeneralHelper 2014-02-26 23:11:06 +00:00
35ec3c283a Bugfix, empty folder text, folder name as title, show new message counts 2014-02-26 11:19:25 +00:00
94837b91ce Set files directory 2014-02-26 07:38:06 +00:00
8ec6da0082 Fixed initial folder selection 2014-02-26 07:10:58 +00:00
7e6b5292bf Loader for email list 2014-02-26 07:08:41 +00:00
172463c96d Basic launcher icon for Bote
by str4d
License: Creative Commons BY-SA 4.0
2014-02-26 04:55:16 +00:00
9ef3747222 Started main interface, nav drawer with list of folders 2014-02-26 04:29:39 +00:00
0aae5a94c8 Clarify why classes are deleted from i2p.jar 2014-02-26 04:26:32 +00:00
b530cbe7b9 Fixed mistake 2014-02-26 04:17:04 +00:00
dacc53970e Copied net.i2p.util.* placeholders from i2p.android.base 2014-02-26 03:58:04 +00:00
77b945e36d Initial commit of Android project structure 2014-02-26 03:57:24 +00:00
765 changed files with 65177 additions and 23262 deletions

66
.github/workflows/sync.yaml vendored Normal file
View File

@ -0,0 +1,66 @@
# GitHub Actions workflow file to sync an external repository to this GitHub mirror.
# This file was automatically generated by go-github-sync.
#
# The workflow does the following:
# - Runs on a scheduled basis (and can also be triggered manually)
# - Clones the GitHub mirror repository
# - Fetches changes from the primary external repository
# - Applies those changes to the mirror repository
# - Pushes the updated content back to the GitHub mirror
#
# Authentication is handled by the GITHUB_TOKEN secret provided by GitHub Actions.
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Validate Github Actions Environment
run: if [ "$GITHUB_ACTIONS" != "true" ]; then echo 'This script must be run in a GitHub Actions environment.'; exit 1; fi
- name: Checkout GitHub Mirror
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Configure Git
run: |-
git config user.name 'GitHub Actions'
git config user.email 'actions@github.com'
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Sync Primary Repository
run: |-
# Add the primary repository as a remote
git remote add primary https://i2pgit.org/I2P_Developers/i2p.i2p-bote.git
# Fetch the latest changes from the primary repository
git fetch primary
# Check if the primary branch exists in the primary repository
if git ls-remote --heads primary master | grep -q master; then
echo "Primary branch master found in primary repository"
else
echo "Error: Primary branch master not found in primary repository"
exit 1
fi
# Check if we're already on the mirror branch
if git rev-parse --verify --quiet master; then
git checkout master
else
# Create the mirror branch if it doesn't exist
git checkout -b master
fi
# Force-apply all changes from primary, overriding any conflicts
echo "Performing force sync from primary/master to master"
git reset --hard primary/master
# Push changes back to the mirror repository
git push origin master
name: Sync Primary Repository to GitHub Mirror
"on":
push: {}
schedule:
- cron: 0 * * * *
workflow_dispatch: {}

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Config files
local.properties
signing.properties
# Gradle
.gradle/
build/
android/build
core/build
webapp/build
# I2P-specific ignores
android/src/main/res/raw/certificates_zip
# James Server dependencies
webapp/cache
webapp/libs
# Standalone WAR user files
webapp/i2pbote
webapp/logs

View File

@ -1,3 +1,24 @@
# General ignores
^.settings
# Android-specific ignores
lint.xml
local.properties
signing.properties
#IntelliJ IDEA
^.idea
.*.iml
.*.ipr
.*.iws
#Gradle
^.gradle
build
# I2P-specific ignores
^android/src/main/res/raw/certificates_zip
# Just to try and prevent some noob disasters.
# Use mtn add --no-respect-ignore foo.jar to ignore this ignore list
@ -10,6 +31,8 @@ i2pbote-update.xpi2p
# Temporary/build dirs
^ant_build
^plugin/plugin.tmp
^core/build/
^webapp/build/
# Build property overrides
override.properties
@ -34,6 +57,7 @@ commons-configuration-.*.jar
commons-io-.*.jar
commons-lang-.*.jar
commons-logging-.*.jar
csrfguard-.*.jar
james-server-filesystem-api-3.0.0-beta5-SNAPSHOT.jar
james-server-lifecycle-api-3.0.0-beta5-SNAPSHOT.jar
james-server-protocols-imap4-3.0.0-beta5-SNAPSHOT.jar

54
.travis.yml Normal file
View File

@ -0,0 +1,54 @@
language: java
jdk:
- oraclejdk11
- oraclejdk9
- oraclejdk8
- openjdk12
- openjdk11
- openjdk10
- openjdk9
- openjdk8
matrix:
include:
- jdk: openjdk7
sudo: required
before_install: # Work around missing crypto in openjdk7
- wget http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/gcc-5-base_5.4.0-6ubuntu1~16.04.10_amd64.deb
- wget http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/libgomp1_5.4.0-6ubuntu1~16.04.10_amd64.deb
- wget http://archive.ubuntu.com/ubuntu/pool/main/n/ncurses/libtinfo5_6.0+20160213-1ubuntu1_amd64.deb
- wget http://archive.ubuntu.com/ubuntu/pool/main/g/gettext/gettext_0.19.7-2ubuntu3.1_amd64.deb
- sudo dpkg -i gcc-5-base_5.4.0-6ubuntu1~16.04.10_amd64.deb
- sudo dpkg -i libgomp1_5.4.0-6ubuntu1~16.04.10_amd64.deb
- sudo dpkg -i libtinfo5_6.0+20160213-1ubuntu1_amd64.deb
- sudo dpkg -i gettext_0.19.7-2ubuntu3.1_amd64.deb
- export JAVA7_HOME=$(jdk_switcher home openjdk7)
- sudo wget "https://bouncycastle.org/download/bcprov-ext-jdk15on-158.jar" -O "${JAVA_HOME}/jre/lib/ext/bcprov-ext-jdk15on-158.jar"
- sudo perl -pi.bak -e 's/^(security\.provider\.)([0-9]+)/$1.($2+1)/ge' /etc/java-7-openjdk/security/java.security
- echo "security.provider.1=org.bouncycastle.jce.provider.BouncyCastleProvider" | sudo tee -a /etc/java-7-openjdk/security/java.security
install:
- export TARGET_JAVA_HOME=$JAVA_HOME
- jdk_switcher use oraclejdk8
- ./gradlew assemble
allow_failures:
- jdk: openjdk12
before_install:
- wget http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/gcc-5-base_5.4.0-6ubuntu1~16.04.10_amd64.deb
- wget http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/libgomp1_5.4.0-6ubuntu1~16.04.10_amd64.deb
- wget http://archive.ubuntu.com/ubuntu/pool/main/n/ncurses/libtinfo5_6.0+20160213-1ubuntu1_amd64.deb
- wget http://archive.ubuntu.com/ubuntu/pool/main/g/gettext/gettext_0.19.7-2ubuntu3.1_amd64.deb
- sudo dpkg -i gcc-5-base_5.4.0-6ubuntu1~16.04.10_amd64.deb
- sudo dpkg -i libgomp1_5.4.0-6ubuntu1~16.04.10_amd64.deb
- sudo dpkg -i libtinfo5_6.0+20160213-1ubuntu1_amd64.deb
- sudo dpkg -i gettext_0.19.7-2ubuntu3.1_amd64.deb
- export JAVA7_HOME=$(jdk_switcher home openjdk7)
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/

View File

@ -1,21 +1,42 @@
[main]
host = https://www.transifex.com
lang_map = ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, zh_CN: zh
lang_map = he: iw, id: in, pt_BR: pt-rBR, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
[I2P.plugin_i2pbote]
file_filter = locale/messages_<lang>.po
source_file = locale/messages_en.po
file_filter = webapp/src/main/locale/messages_<lang>.po
source_file = webapp/src/main/locale/messages_en.po
source_lang = en
minimum_perc = 40
[I2P.plugin_i2pbote_userguide]
file_filter = src/main/webapp/html/userGuide_<lang>.html
source_file = src/main/webapp/html/userGuide.html
file_filter = webapp/src/main/webapp/html/userGuide_<lang>.html
source_file = webapp/src/main/webapp/html/userGuide.html
source_lang = en
minimum_perc = 50
[I2P.plugin_i2pbote_faq]
file_filter = src/main/webapp/html/FAQ_<lang>.html
source_file = src/main/webapp/html/FAQ.html
file_filter = webapp/src/main/webapp/html/FAQ_<lang>.html
source_file = webapp/src/main/webapp/html/FAQ.html
source_lang = en
minimum_perc = 50
[I2P.android_bote]
file_filter = android/src/main/res/values-<lang>/strings.xml
source_file = android/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
minimum_perc = 50
[I2P.android_bote_help_start]
file_filter = android/src/main/res/raw-<lang>/help_start.html
source_file = android/src/main/res/raw/help_start.html
source_lang = en
type = HTML
minimum_perc = 50
[I2P.android_bote_help_identities]
file_filter = android/src/main/res/raw-<lang>/help_identities.html
source_file = android/src/main/res/raw/help_identities.html
source_lang = en
type = HTML
minimum_perc = 50

146
README.md
View File

@ -1,28 +1,140 @@
# I2P-Bote
[![Build Status](https://travis-ci.org/i2p/i2p.i2p-bote.svg?branch=master)](https://travis-ci.org/i2p/i2p.i2p-bote)
I2P-Bote is a plugin for I2P that allows users to send and receive emails while preserving privacy. It does not need a mail server because emails are stored in a distributed hash table. They are automatically encrypted and digitally signed, which ensures no one but the intended recipient can read the email, and third parties cannot forge them.
**Features:**
## Features
* Themeable webmail interface
* User interface translated in many languages
* One-click creation of email accounts (called email identities)
* Emails can be sent under a sender identity, or anonymously
* ElGamal, Elliptic Curve, and NTRU Encryption
* Encryption and signing is transparent, without the need to know about PGP
* Delivery confirmation
* Basic support for short recipient names
* IMAP / SMTP
- Themeable webmail interface
- User interface translated in many languages
- One-click creation of email accounts (called email identities)
- Emails can be sent under a sender identity, or anonymously
- ElGamal, Elliptic Curve, and NTRU Encryption
- Encryption and signing is transparent, without the need to know about PGP
- Delivery confirmation
- Basic support for short recipient names
- IMAP / SMTP
**Planned Features:**
### Planned Features
* Custom folders
* Sending and receiving via relays, similar to Mixmaster
* Lots of small improvements
- Custom folders
- Sending and receiving via relays, similar to Mixmaster
- Lots of small improvements
**More information**
## Build process
### Dependencies:
- Java SDK (preferably Oracle/Sun or OpenJDK) 1.7.0 or higher
- Apache Ant 1.8.0 or higher
- Gradle 2.14.1
### Gradle
The build system is based on Gradle. There are several methods for setting Gradle up:
* It can be downloaded from [the Gradle website](http://www.gradle.org/downloads).
* Most distributions will have Gradle packages. Be careful to check the provided version; Debian and Ubuntu have old versions in their main repositories. There is a [PPA](https://launchpad.net/~cwchien/+archive/gradle) for Ubuntu with the latest version of Gradle.
* A Gradle wrapper is provided in the codebase. It takes all the same commands as the regular `gradle` command. The first time that any command is run, it will automatically download, cache and use the correct version of Gradle. This is the simplest way to get started with the codebase. To use it, replace `gradle` with `./gradlew` (or `./gradlew.bat` on Windows) in the commands below.
Gradle will pull dependencies over the clearnet by default. To use Tor, create a `gradle.properties` file in `i2p.android.base` containing:
```
systemProp.socksProxyHost=localhost
systemProp.socksProxyPort=9150
```
### Building the I2P plugin
```
gradle :webapp:plugin
```
The plugin will be placed in `i2p.i2p-bote/webapp/build/plugin`.
### Building the standalone WAR
```
gradle :webapp:war
```
The WAR will be placed in `i2p.i2p-bote/webapp/build/libs`.
### Running the standalone WAR
Ensure you have an I2P router running locally with an I2CP server port open (on port 7654). Then run:
```
gradle :webapp:tomcatRunWar
```
This will build and run the WAR. (Jetty currently does not work.)
The data directory will be placed in `i2p.i2p-bote/webapp/i2pbote`; logs will be in `i2p.i2p-bote/webapp/logs`.
## Android build process
### Additional dependencies:
- [I2P source](https://github.com/i2p/i2p.i2p)
- Android SDK 25
- Android Build Tools 25.0.2
- Android Support Repository
### Preparation
1. Download the Android SDK. The simplest method is to download Android Studio.
2. Create a `local.properties` file in `i2p.i2p-bote/android` containing:
```
i2psrc=/path/to/i2p.i2p
```
3. If you want to use a local copy of the I2P Android client library, install it in your local Maven repository with:
```
cd path/to/i2p.android.base
./gradlew client:installArchives
```
### Building from the command line
1. Create a `local.properties` file in `i2p.i2p-bote` containing:
```
sdk.dir=/path/to/android-studio/sdk
```
2. `gradle :android:assembleDebug`
3. The APK will be placed in `i2p.i2p-bote/android/build/apk`.
### Building with Android Studio
1. Import `i2p.i2p-bote` into Android Studio. (This creates the `local.properties` file automatically).
2. Build and run the app (`Shift+F10`).
### Signing release builds
1. Create a `signing.properties` file in `i2p.i2p-bote` containing:
```
STORE_FILE=/path/to/android.keystore
STORE_PASSWORD=store.password
KEY_ALIAS=key.alias
KEY_PASSWORD=key.password
```
2. `gradle assembleRelease`
## More information
The links below only work within I2P, i.e., make sure you are running I2P and your browser is using the proxy at localhost:4444.
* http://i2pbote.i2p I2P-Bote homepage
* http://forum.i2p/viewforum.php?f=35 I2P-Bote forum
- http://i2pbote.i2p I2P-Bote homepage
- http://forum.i2p/viewforum.php?f=35 I2P-Bote forum

56
android/CHANGELOG Normal file
View File

@ -0,0 +1,56 @@
0.7
* Address book import/export
0.6 / 2015-06-21 / 77d67b4f2d465a5c528fb31af5785d176276b94b
* Identity switcher
* Language can be configured
* Block screenshots by default
* Fixed internal router
* Reorganized settings
* UI improvements, bug fixes, translation updates
0.5 / 2015-01-13 / f5eaab42b6faa25450b2772315c7849f1eb4a03a
* Attachments!
* Email content can be selected/copied (API 11+)
* Cc: and Bcc: fields
* Improved performance of email and contact lists
* Use ripple effect on API 21+
* Much better support for legacy devices
* Help and About screens
* UI improvements, bug fixes, translation updates
0.4 / 2014-12-19 / fdc9c619eff53745d49c987b353ce5b04278dfd6
* New email notifications!
* Fixed crash when clicking "Create new contact" button
* Fixed crash when sending email
* Fixed occasional crashes when Bote connects to / disconnects from the network
* Added "Copy to clipboard" button to identities and contacts
* Added labels to the address book actions menu
* UI improvements, bug fixes, translation updates
0.3 / 2014-12-01 / ebba8aac78d50eda9d25f936e8bd348553966649
* Migrated to new Material design from Android Lollipop
* Overhauled password usability
* Identicons for contacts without pictures
* Use new-style swipe for checking emails
* Users can now check emails from menu, or swipe on empty inbox
* "Forward" and "Reply all" actions for emails
* QR codes for sharing identities
* Separated viewing and editing contacts
* Improved network information page
* Bug fixes, translation updates
0.3-rc3 / 2014-08-23 / 6e7655836552d13d6fa6b7512e7dcf77cb3d413b
* Test release to Norway on Google Play
0.2 / 2014-07-10 / 6c9c580bf6272db575091a1b48f66315535380e0
* Fixed locale hiding in replies
* New notification icons
* New translation
* UI improvements and bug fixes
0.1.1 / 2014-06-20 / a9c83d573dc3c5fbeeb288b347f996f440b33621
* Fixed bugs in identity creation and password setting
0.1 / 2014-06-19 / 8457f58f367a2f362bdacd470c792b4bcd6d42f6
* Initial release

70
android/TODO Normal file
View File

@ -0,0 +1,70 @@
Fixes:
- Auto-comma the To: field when it loses focus
- Fix tick over selected emails
- Delete/read/unread/move actions on view email page that don't break everything
- Prevent router option being changed while Bote is running
- Remove internal router
Tasks:
- Show logged-in status in persistent notification
- Intro and setup
- More layouts (tune for each screen size)
- Subclass EmailListFragment for each default mailbox
- First-run content of inbox should tell users about pull-to-refresh.
- Cache Identicons for speed
- Refactor code
- Reorganize for clarity
- Optimize use of Android lifecycles
Silent Store approval checks to confirm/implement:
- Known Vulnerabilities
- Apps will be tested to ensure that they are not susceptible to known
publicly disclosed vulnerabilities. For example:
- Heartbleed
- Poodle
- MasterKey
- Common Path Traversal attacks
- Common SQL Injection attacks
- Network Security Protocols
- All Apps that require transmission of data from the App to a system that
does not exist on the device must use, at a minimum, TLS1.1 standards.
However, Blackphone would prefer the usage of TLS1.2.
- Apps must not use algorithms for cryptographic purposes that are considered
obsolete or outdated i.e. MD5, SHA1, RC4, DES, or any encryption algorithm
that is weaker than AES128.
- Transport Layer Protection
- All network communication should be encrypted
- Not vulnerable to SSl Strip
- Data Leakage
- No storage of sensitive data outside of application sandbox
- Files should not be created with MODE_WORLD_READABLE or MODE_WORLD_WRITABLE
- Copy & Paste will be evaluated on a case by case basis
- App logs should not contain sensitive information
- Authentication and Authorization
- Validate that authentication credentials are not stored on the device
- Must use an approved password-based key derivation function ie. PBKDF2, scrypt
- Data-at-rest Encryption
- Must use at a minimum AES128 with modes CCM or GCM
- Should not store the encryption key on the file system
- Permission Checks
- The App must function with all permissions disabled
- Apps must not hard crash if a permission is disabled
- Apps should ask users to enable permissions that are disabled if needed to
function properly and explain why the permission is necessary
- Privacy Policy
- Apps must have a privacy policy that details how customer data is used,
stored, shared, etc...
- Apps must be configured with the customer opted out by default
- App logs should not contain PII
- Error Handling
- Apps should follow best-practices for error handling and logging
Features:
- Search
- Fingerprints that users can compare to validate
- Public address book lookup
- "Write email" link in address book
- "Empty trash" option in Trash folder
- Overlay to explain
- Option to run only when connected to an outlet
- Option to run only when connected to wifi

View File

@ -0,0 +1,382 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024"
height="500"
id="svg3027"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="bote-feature.svg">
<defs
id="defs3029">
<linearGradient
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)"
y2="481.5072"
x2="523.5882"
y1="481.5072"
x1="340.4118"
gradientUnits="userSpaceOnUse"
id="linearGradient6049"
xlink:href="#linearGradient3903"
inkscape:collect="always" />
<linearGradient
id="linearGradient3903-0">
<stop
id="stop3905-0"
offset="0"
style="stop-color:#000000;stop-opacity:0.17647059;" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.25018752"
id="stop3915-03" />
<stop
id="stop3919-2"
offset="0.50031251"
style="stop-color:#000000;stop-opacity:0.05882353;" />
<stop
id="stop3917-29"
offset="0.75"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop3907-6"
offset="1"
style="stop-color:#000000;stop-opacity:0.17647059;" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="481.5072"
x2="523.5882"
y1="481.5072"
x1="340.4118"
id="linearGradient5962-5"
xlink:href="#linearGradient3903-0"
inkscape:collect="always" />
<linearGradient
id="linearGradient3903-3-2">
<stop
id="stop3905-2-3"
offset="0"
style="stop-color:#000000;stop-opacity:0.15686275;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.2"
id="stop3915-0-3" />
<stop
id="stop3919-4-8"
offset="0.50031251"
style="stop-color:#000000;stop-opacity:0.01960784;" />
<stop
id="stop3917-2-6"
offset="0.80000001"
style="stop-color:#000000;stop-opacity:0.05882353;" />
<stop
id="stop3907-9-6"
offset="1"
style="stop-color:#000000;stop-opacity:0.15686275;" />
</linearGradient>
<linearGradient
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)"
gradientUnits="userSpaceOnUse"
y2="342.44925"
x2="500.96887"
y1="342.44925"
x1="363.03113"
id="linearGradient5942"
xlink:href="#linearGradient3903-3"
inkscape:collect="always" />
<linearGradient
id="linearGradient3903-3">
<stop
id="stop3905-2"
offset="0"
style="stop-color:#000000;stop-opacity:0.17647059;" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.25018752"
id="stop3915-0" />
<stop
id="stop3919-4"
offset="0.50031251"
style="stop-color:#000000;stop-opacity:0.05882353;" />
<stop
id="stop3917-2"
offset="0.75"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop3907-9"
offset="1"
style="stop-color:#000000;stop-opacity:0.17647059;" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.99875033,0,0,0.99826344,0.53986459,0.7502204)"
gradientUnits="userSpaceOnUse"
y2="432.00003"
x2="832"
y1="432.00003"
x1="32.000004"
id="linearGradient3911-5"
xlink:href="#linearGradient3903-3"
inkscape:collect="always" />
<inkscape:perspective
id="perspective3012"
inkscape:persp3d-origin="432 : 288 : 1"
inkscape:vp_z="1566.8571 : 394.85714 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="-291.42857 : 94.857143 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient3903">
<stop
id="stop3905"
offset="0"
style="stop-color:#000000;stop-opacity:0.17647059;" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.25018752"
id="stop3915" />
<stop
id="stop3919"
offset="0.50031251"
style="stop-color:#000000;stop-opacity:0.05882353;" />
<stop
id="stop3917"
offset="0.75"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop3907"
offset="1"
style="stop-color:#000000;stop-opacity:0.17647059;" />
</linearGradient>
<linearGradient
id="linearGradient5944">
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0"
id="stop5952" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="1"
id="stop5954" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3-2"
id="linearGradient3248"
gradientUnits="userSpaceOnUse"
x1="23"
y1="431"
x2="841"
y2="431" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3-2"
id="linearGradient3250"
gradientUnits="userSpaceOnUse"
x1="267"
y1="522"
x2="597"
y2="522" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-0"
id="radialGradient3252"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.72727297,0,73.499943)"
cx="430.75"
cy="269.50003"
fx="430.75"
fy="269.50003"
r="123.75" />
<filter
id="filter3854"
y="-0.15"
height="1.3"
x="-0.15"
width="1.3"
inkscape:menu-tooltip="Aquarelle paper effect which can be used for pictures as for objects"
inkscape:menu="Textures"
inkscape:label="Rough paper"
color-interpolation-filters="sRGB">
<feTurbulence
id="feTurbulence3856"
type="fractalNoise"
baseFrequency="0.04"
numOctaves="5"
seed="0"
result="result4" />
<feDisplacementMap
id="feDisplacementMap3858"
in2="result4"
in="SourceGraphic"
yChannelSelector="G"
xChannelSelector="R"
scale="10"
result="result3" />
<feDiffuseLighting
id="feDiffuseLighting3860"
lighting-color="rgb(233,230,215)"
diffuseConstant="1"
surfaceScale="2"
result="result1"
in="result4">
<feDistantLight
id="feDistantLight3862"
azimuth="235"
elevation="40" />
</feDiffuseLighting>
<feComposite
id="feComposite3864"
in2="result1"
operator="in"
in="result3"
result="result2" />
<feComposite
id="feComposite3866"
in2="result1"
result="result5"
operator="arithmetic"
k1="1.7" />
<feBlend
id="feBlend3868"
in2="result3"
in="result5"
mode="normal" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="351.48063"
inkscape:cy="250"
inkscape:document-units="px"
inkscape:current-layer="layer2"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata3032">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Background"
style="opacity:0.3">
<rect
style="fill:#ffffde;fill-opacity:0.87058824000000001;fill-rule:nonzero;stroke:none;filter:url(#filter3854)"
id="rect3032"
width="1124"
height="600"
x="-50"
y="-50" />
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-552.36218)">
<g
id="g3224"
transform="matrix(0.40342298,0,0,0.40342298,91.309473,628.48688)">
<g
style="display:inline"
inkscape:label="Bg"
id="layer3">
<rect
y="145.99701"
x="32"
height="574.00299"
width="798.00293"
id="rect2994-9"
style="fill:#fafae6;fill-opacity:1;stroke:none;display:inline" />
</g>
<g
style="display:inline"
inkscape:label="Envelope"
id="layer1-6">
<rect
y="142"
x="32"
height="578"
width="800"
id="rect2994"
style="fill:url(#linearGradient3248);fill-opacity:1;stroke:#000000;stroke-width:18;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
d="M 32.99995,144.99995 431.99998,479.00212 831.00005,144.99995 M 375.41474,431.99882 33.000566,719.00005 m 797.998874,0 -342.41418,-287.00123"
style="fill:none;stroke:#000000;stroke-width:17.99989891;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3044-9" />
</g>
<g
style="display:inline"
id="g6041"
inkscape:label="Lock bg">
<path
inkscape:connector-curvature="0"
id="path6043"
d="m 267,359.5 0,325 330,0 0,-325 -330,0 z M 432,418 c 29.82337,0 54,24.17661 54,54 0,19.98456 -10.86345,37.41209 -27,46.75 l 0,107.25 -54,0 0,-107.25 c -16.13656,-9.33791 -27,-26.76544 -27,-46.75 0,-29.82339 24.17662,-54 54,-54 z"
style="fill:#ffda00;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8.89999962;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
style="fill:#faffff;fill-opacity:1;stroke:#000000;stroke-width:9;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 369.75735,359.49999 0,-57.36595 c 0.21395,-6.69058 1.48284,-13.34882 3.92587,-19.62604 8.34674,-22.92119 31.23869,-39.42587 55.47413,-39.71118 0.75084,-0.0301 1.52631,-0.0581 2.27586,-0.0572 23.23569,0.0265 45.61943,14.77505 54.96208,36.26801 3.26038,7.2537 5.06733,15.16666 5.34827,23.12659 l 0,57.36584 M 554.5,359.5 554.49909,302.13415 C 553.31086,234.20574 498.38227,179.5 430.74976,179.5 363.11725,179.5 308.18822,234.20574 307,302.13405 L 307,359.5"
id="path6045"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccscc" />
</g>
<g
inkscape:label="Lock"
id="g4234"
style="display:inline">
<path
inkscape:connector-curvature="0"
style="fill:url(#linearGradient3250);fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 267,359.5 0,325 330,0 0,-325 -330,0 z M 432,418 c 29.82337,0 54,24.17661 54,54 0,19.98456 -10.86345,37.41209 -27,46.75 l 0,107.25 -54,0 0,-107.25 c -16.13656,-9.33791 -27,-26.76544 -27,-46.75 0,-29.82339 24.17662,-54 54,-54 z"
id="path4236" />
<path
sodipodi:nodetypes="ccccccccccscc"
inkscape:connector-curvature="0"
id="path4238"
d="m 369.75735,359.49999 0,-57.36595 c 0.21395,-6.69058 1.48284,-13.34882 3.92587,-19.62604 8.34674,-22.92119 31.23869,-39.42587 55.47413,-39.71118 0.75084,-0.0301 1.52631,-0.0581 2.27586,-0.0572 23.23569,0.0265 45.61943,14.77505 54.96208,36.26801 3.26038,7.2537 5.06733,15.16666 5.34827,23.12659 l 0,57.36584 M 554.5,359.5 554.49909,302.13415 C 553.31086,234.20574 498.38227,179.5 430.74976,179.5 363.11725,179.5 308.18822,234.20574 307,302.13405 L 307,359.5"
style="fill:url(#radialGradient3252);fill-opacity:1;stroke:none" />
</g>
</g>
<text
xml:space="preserve"
style="font-size:40px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Webdings;-inkscape-font-specification:Webdings"
x="497.5329"
y="863.97107"
id="text3273"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
x="497.5329"
y="863.97107"
style="font-size:175px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3277">Bote.</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,296 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="864"
height="864"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ic_launcher_master.svg">`
<defs
id="defs4">
<linearGradient
id="linearGradient5944">
<stop
id="stop5952"
offset="0"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop5954"
offset="1"
style="stop-color:#000000;stop-opacity:0.09803922;" />
</linearGradient>
<linearGradient
id="linearGradient3903">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905" />
<stop
id="stop3915"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="-291.42857 : 94.857143 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1566.8571 : 394.85714 : 1"
inkscape:persp3d-origin="432 : 288 : 1"
id="perspective3012" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient3911-5"
x1="32.000004"
y1="432.00003"
x2="832"
y2="432.00003"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.99875033,0,0,0.99826344,0.53986459,0.7502204)" />
<linearGradient
id="linearGradient3903-3">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-2" />
<stop
id="stop3915-0"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-4" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-2" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-9" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient5942"
x1="363.03113"
y1="342.44925"
x2="500.96887"
y2="342.44925"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
<linearGradient
id="linearGradient3903-3-2">
<stop
style="stop-color:#000000;stop-opacity:0.15686275;"
offset="0"
id="stop3905-2-3" />
<stop
id="stop3915-0-3"
offset="0.2"
style="stop-color:#000000;stop-opacity:0.05882353;" />
<stop
style="stop-color:#000000;stop-opacity:0.01960784;"
offset="0.50031251"
id="stop3919-4-8" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.80000001"
id="stop3917-2-6" />
<stop
style="stop-color:#000000;stop-opacity:0.15686275;"
offset="1"
id="stop3907-9-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-0"
id="linearGradient5962-5"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3903-0">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-0" />
<stop
id="stop3915-03"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-2" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-29" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903"
id="linearGradient6049"
gradientUnits="userSpaceOnUse"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3-2"
id="linearGradient4246"
x1="23"
y1="431"
x2="841"
y2="431"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3-2"
id="linearGradient4254"
x1="267"
y1="522"
x2="597"
y2="522"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-0"
id="radialGradient4266"
cx="430.75"
cy="269.50003"
fx="430.75"
fy="269.50003"
r="123.75"
gradientTransform="matrix(1,0,0,0.72727297,0,73.499943)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.70710678"
inkscape:cx="355.79427"
inkscape:cy="460.20102"
inkscape:document-units="px"
inkscape:current-layer="g6041"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Bg"
style="display:inline">
<rect
style="fill:#fafae6;fill-opacity:1;stroke:none;display:inline"
id="rect2994-9"
width="798.00293"
height="574.00299"
x="32"
y="145.99701" />
</g>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Envelope"
style="display:inline;opacity:1">
<rect
style="fill:url(#linearGradient4246);fill-opacity:1.0;stroke:#000000;stroke-width:18;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect2994"
width="800"
height="578"
x="32"
y="142" />
<path
id="path3044-9"
style="fill:none;stroke:#000000;stroke-width:17.99989890999999886;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 32.99995,144.99995 431.99998,479.00212 831.00005,144.99995 M 375.41474,431.99882 33.000566,719.00005 m 797.998874,0 -342.41418,-287.00123"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:label="Lock bg"
id="g6041"
inkscape:groupmode="layer"
style="display:inline">
<path
style="fill:#ffda00;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-opacity:1;stroke-width:8.9;stroke-miterlimit:4;stroke-dasharray:none"
d="m 267,359.5 0,325 330,0 0,-325 -330,0 z M 432,418 c 29.82337,0 54,24.17661 54,54 0,19.98456 -10.86345,37.41209 -27,46.75 l 0,107.25 -54,0 0,-107.25 c -16.13656,-9.33791 -27,-26.76544 -27,-46.75 0,-29.82339 24.17662,-54 54,-54 z"
id="path6043"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccccscc"
inkscape:connector-curvature="0"
id="path6045"
d="m 369.75735,359.49999 0,-57.36595 c 0.21395,-6.69058 1.48284,-13.34882 3.92587,-19.62604 8.34674,-22.92119 31.23869,-39.42587 55.47413,-39.71118 0.75084,-0.0301 1.52631,-0.0581 2.27586,-0.0572 23.23569,0.0265 45.61943,14.77505 54.96208,36.26801 3.26038,7.2537 5.06733,15.16666 5.34827,23.12659 l 0,57.36584 M 554.5,359.5 554.49909,302.13415 C 553.31086,234.20574 498.38227,179.5 430.74976,179.5 363.11725,179.5 308.18822,234.20574 307,302.13405 L 307,359.5"
style="fill:#faffff;fill-opacity:1;stroke:#000000;stroke-width:9;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
style="display:inline"
inkscape:groupmode="layer"
id="g4234"
inkscape:label="Lock">
<path
id="path4236"
d="m 267,359.5 0,325 330,0 0,-325 -330,0 z M 432,418 c 29.82337,0 54,24.17661 54,54 0,19.98456 -10.86345,37.41209 -27,46.75 l 0,107.25 -54,0 0,-107.25 c -16.13656,-9.33791 -27,-26.76544 -27,-46.75 0,-29.82339 24.17662,-54 54,-54 z"
style="fill:url(#linearGradient4254);fill-opacity:1;fill-rule:nonzero;stroke:none"
inkscape:connector-curvature="0" />
<path
style="fill:url(#radialGradient4266);fill-opacity:1;stroke:none"
d="m 369.75735,359.49999 0,-57.36595 c 0.21395,-6.69058 1.48284,-13.34882 3.92587,-19.62604 8.34674,-22.92119 31.23869,-39.42587 55.47413,-39.71118 0.75084,-0.0301 1.52631,-0.0581 2.27586,-0.0572 23.23569,0.0265 45.61943,14.77505 54.96208,36.26801 3.26038,7.2537 5.06733,15.16666 5.34827,23.12659 l 0,57.36584 M 554.5,359.5 554.49909,302.13415 C 553.31086,234.20574 498.38227,179.5 430.74976,179.5 363.11725,179.5 308.18822,234.20574 307,302.13405 L 307,359.5"
id="path4238"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccscc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="864"
height="864"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ic_notif_master.svg">
<defs
id="defs4">
<linearGradient
id="linearGradient5944">
<stop
id="stop5952"
offset="0"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop5954"
offset="1"
style="stop-color:#000000;stop-opacity:0.09803922;" />
</linearGradient>
<linearGradient
id="linearGradient3903">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905" />
<stop
id="stop3915"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="-291.42857 : 94.857143 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1566.8571 : 394.85714 : 1"
inkscape:persp3d-origin="432 : 288 : 1"
id="perspective3012" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient3911-5"
x1="32.000004"
y1="432.00003"
x2="832"
y2="432.00003"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.99875033,0,0,0.99826344,0.53986459,0.7502204)" />
<linearGradient
id="linearGradient3903-3">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-2" />
<stop
id="stop3915-0"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-4" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-2" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-9" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient5942"
x1="363.03113"
y1="342.44925"
x2="500.96887"
y2="342.44925"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
<linearGradient
id="linearGradient3903-3-2">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-2-3" />
<stop
id="stop3915-0-3"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-4-8" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-2-6" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-9-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-0"
id="linearGradient5962-5"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3903-0">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-0" />
<stop
id="stop3915-03"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-2" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-29" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903"
id="linearGradient6049"
gradientUnits="userSpaceOnUse"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4056">
<path
style="fill:#000000;fill-opacity:1;stroke:none;display:inline"
d="M 57.46875,142 307,350.875 l 0,-56.25 C 308.18822,226.69669 363.11749,172 430.75,172 c 67.63251,0 122.56177,54.69659 123.75,122.625 l 0,57.375 1.15625,0 250.875,-210 -749.0625,0 z M 32,167.65625 32,696.375 267,499.40625 267,364.375 32,167.65625 z m 800,0 L 597,364.375 597,499.40625 832,696.375 832,167.65625 z M 431.4375,235.25 c -0.74955,-0.001 -1.53041,0.001 -2.28125,0.0312 -24.23544,0.28531 -47.12201,16.79756 -55.46875,39.71875 -2.44303,6.27722 -3.72355,12.93442 -3.9375,19.625 l 0,57.375 122,0 0,-57.375 c -0.28094,-7.95993 -2.08337,-15.8713 -5.34375,-23.125 -9.34265,-21.49296 -31.73306,-36.2235 -54.96875,-36.25 z M 432,404.5 c -34.79394,0 -63,28.20605 -63,63 0,21.40237 10.6868,40.3019 27,51.6875 l 0,120.3125 72,0 0,-120.3125 c 16.31319,-11.3856 27,-30.28513 27,-51.6875 0,-34.79395 -28.20607,-63 -63,-63 z M 267,546.34375 59.8125,720 804.1875,720 597,546.34375 597,692 l -330,0 0,-145.65625 z"
id="path4058"
inkscape:connector-curvature="0" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="659.75749"
inkscape:cy="504.31379"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Bg"
style="display:none" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Envelope"
style="display:inline">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect2994"
width="800"
height="578"
x="32"
y="142"
clip-path="url(#clipPath4056)" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Chain"
style="display:none" />
<g
inkscape:label="Lock bg"
id="g6041"
inkscape:groupmode="layer"
style="display:inline" />
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="864"
height="864"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ic_notif_premaster.svg">
<defs
id="defs4">
<linearGradient
id="linearGradient5944">
<stop
id="stop5952"
offset="0"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
id="stop5954"
offset="1"
style="stop-color:#000000;stop-opacity:0.09803922;" />
</linearGradient>
<linearGradient
id="linearGradient3903">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905" />
<stop
id="stop3915"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="-291.42857 : 94.857143 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1566.8571 : 394.85714 : 1"
inkscape:persp3d-origin="432 : 288 : 1"
id="perspective3012" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient3911-5"
x1="32.000004"
y1="432.00003"
x2="832"
y2="432.00003"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.99875033,0,0,0.99826344,0.53986459,0.7502204)" />
<linearGradient
id="linearGradient3903-3">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-2" />
<stop
id="stop3915-0"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-4" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-2" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-9" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-3"
id="linearGradient5942"
x1="363.03113"
y1="342.44925"
x2="500.96887"
y2="342.44925"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
<linearGradient
id="linearGradient3903-3-2">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-2-3" />
<stop
id="stop3915-0-3"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-4-8" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-2-6" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-9-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903-0"
id="linearGradient5962-5"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3903-0">
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="0"
id="stop3905-0" />
<stop
id="stop3915-03"
offset="0.25018752"
style="stop-color:#000000;stop-opacity:0.09803922;" />
<stop
style="stop-color:#000000;stop-opacity:0.05882353;"
offset="0.50031251"
id="stop3919-2" />
<stop
style="stop-color:#000000;stop-opacity:0.09803922;"
offset="0.75"
id="stop3917-29" />
<stop
style="stop-color:#000000;stop-opacity:0.17647059;"
offset="1"
id="stop3907-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3903"
id="linearGradient6049"
gradientUnits="userSpaceOnUse"
x1="340.4118"
y1="481.5072"
x2="523.5882"
y2="481.5072"
gradientTransform="matrix(1.4585784,0,0,1.4585784,-198.10585,-178.10619)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="312.75749"
inkscape:cy="504.31379"
inkscape:document-units="px"
inkscape:current-layer="g6041"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Bg"
style="display:inline">
<rect
style="fill:#fafae6;fill-opacity:1;stroke:none;display:inline"
id="rect2994-9"
width="798.00293"
height="574.00299"
x="32"
y="145.99701" />
</g>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Envelope"
style="display:inline">
<rect
style="fill:#b3b3b3;fill-opacity:1;stroke:none"
id="rect2994"
width="800"
height="578"
x="32"
y="142" />
<path
id="path3044-9"
style="fill:none;stroke:#000000;stroke-width:36;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 32.99995,144.99995 431.99998,479.00212 831.00005,144.99995 M 375.41474,431.99882 33.000566,719.00005 m 797.998874,0 -342.41418,-287.00123"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Chain"
style="display:none" />
<g
inkscape:label="Lock bg"
id="g6041"
inkscape:groupmode="layer"
style="display:inline">
<path
id="path6043"
d="m 267,352.00003 0,340 330,0 0,-340 -330,0 z m 165,80.54046 c 20.333,0 36.82476,17.28664 36.82476,38.59975 0,17.02222 -10.50755,31.44372 -25.10002,36.57132 l 0,103.74803 -23.44948,0 0,-103.74803 c -14.57211,-5.13964 -25.10002,-19.5643 -25.10002,-36.57131 0,-21.31312 16.49176,-38.59976 36.82476,-38.59976 z"
style="fill:#ffda00;fill-opacity:1;fill-rule:nonzero;stroke:none"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccccccccscc"
inkscape:connector-curvature="0"
id="path6045"
d="m 369.75735,351.99996 0,-57.36595 c 0.21395,-6.69058 1.48284,-13.34882 3.92587,-19.62604 8.34674,-22.92119 31.23869,-39.42587 55.47413,-39.71118 0.75084,-0.0301 1.52631,-0.0581 2.27586,-0.0572 23.23569,0.0265 45.61943,14.77505 54.96208,36.26801 3.26038,7.2537 5.06733,15.16666 5.34827,23.12659 l 0,57.36584 m 62.75644,-6e-5 -9.1e-4,-57.36585 C 553.31086,226.70571 498.38227,171.99997 430.74976,171.99997 363.11725,171.99997 308.18822,226.70571 307,294.63402 l 0,57.36595"
style="fill:#faffff;fill-opacity:1;stroke:none" />
<path
id="rect3796"
style="fill:#b3b3b3;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 396,511.50001 72,0 0,128 -72,0 z m 99,-43.99996 c 0,34.79393 -28.20606,62.99999 -62.99999,62.99999 -34.79394,0 -63.00001,-28.20606 -63.00001,-62.99999 0,-34.79395 28.20607,-63.00001 63.00001,-63.00001 34.79393,0 62.99999,28.20606 62.99999,63.00001 z"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ic_scan_qr_code_24px.svg">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs12" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="719"
id="namedview10"
showgrid="false"
inkscape:zoom="22.583333"
inkscape:cx="6.0442804"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<path
d="M0 0h24v24h-24z"
fill="none"
id="path8" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Camera"
style="display:inline">
<path
d="M 8 6.09375 C 6.9396133 6.09375 6.09375 6.9396133 6.09375 8 C 6.09375 9.0603867 6.9396133 9.90625 8 9.90625 L 8 8 L 9.90625 8 C 9.90625 6.9396133 9.0603867 6.09375 8 6.09375 z "
id="circle4-5"
style="fill:#000000" />
<path
d="M 6.1875 2 L 5.09375 3.1875 L 3.1875 3.1875 C 2.5275 3.1875 2 3.74625 2 4.40625 L 2 11.59375 C 2 12.25375 2.5275 12.8125 3.1875 12.8125 L 8 12.8125 L 8 11 C 6.344 11 5 9.656 5 8 C 5 6.344 6.344 5 8 5 C 9.656 5 11 6.344 11 8 L 14 8 L 14 4.40625 C 14 3.74625 13.4725 3.1875 12.8125 3.1875 L 10.90625 3.1875 L 9.8125 2 L 6.1875 2 z "
id="path6-5"
style="fill:#000000" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="QR Code"
style="display:inline">
<path
style="fill:#000000;stroke:none"
d="m 8.625,8.625 0,4.46875 4.46875,0 0,-4.46875 -4.46875,0 z m 6.34375,0 0,0.65625 0.65625,0 0,-0.65625 -0.65625,0 z m 1.28125,0 0,1.28125 0.625,0 0,-1.28125 -0.625,0 z m 1.28125,0 0,4.46875 4.4375,0 0,-4.46875 -4.4375,0 z m -8.25,0.65625 3.15625,0 0,3.15625 -3.15625,0 0,-3.15625 z m 8.875,0 3.1875,0 0,3.15625 -3.1875,0 0,-3.15625 z"
id="path6"
inkscape:connector-curvature="0" />
<path
style="fill:#000000;stroke:none"
d="M 9.90625 9.90625 L 9.90625 11.8125 L 11.8125 11.8125 L 11.8125 9.90625 L 9.90625 9.90625 z M 13.71875 9.90625 L 13.71875 10.53125 L 14.34375 10.53125 L 14.34375 9.90625 L 13.71875 9.90625 z M 14.96875 9.90625 L 14.96875 10.53125 L 15.625 10.53125 L 15.625 9.90625 L 14.96875 9.90625 z M 18.78125 9.90625 L 18.78125 11.8125 L 20.6875 11.8125 L 20.6875 9.90625 L 18.78125 9.90625 z M 16.25 10.53125 L 16.25 11.1875 L 15.625 11.1875 L 15.625 11.8125 L 14.96875 11.8125 L 14.96875 11.1875 L 14.34375 11.1875 L 14.34375 12.4375 L 14.96875 12.4375 L 14.96875 14.34375 L 15.625 14.34375 L 15.625 12.4375 L 16.25 12.4375 L 16.25 11.8125 L 16.875 11.8125 L 16.875 10.53125 L 16.25 10.53125 z M 16.25 12.4375 L 16.25 13.09375 L 16.875 13.09375 L 16.875 12.4375 L 16.25 12.4375 z M 15.625 14.34375 L 15.625 15.625 L 14.96875 15.625 L 14.96875 16.25 L 15.625 16.25 L 15.625 16.875 L 13.71875 16.875 L 13.71875 19.4375 L 14.34375 19.4375 L 14.34375 20.0625 L 13.71875 20.0625 L 13.71875 21.96875 L 14.34375 21.96875 L 14.34375 21.34375 L 14.96875 21.34375 L 14.96875 20.0625 L 15.625 20.0625 L 15.625 21.96875 L 16.875 21.96875 L 16.875 21.34375 L 16.25 21.34375 L 16.25 20.6875 L 17.53125 20.6875 L 17.53125 20.0625 L 16.875 20.0625 L 16.875 18.78125 L 15.625 18.78125 L 15.625 19.4375 L 14.96875 19.4375 L 14.96875 18.78125 L 14.34375 18.78125 L 14.34375 18.15625 L 15.625 18.15625 L 15.625 17.53125 L 16.25 17.53125 L 16.25 16.25 L 16.875 16.25 L 16.875 15.625 L 16.25 15.625 L 16.25 14.34375 L 15.625 14.34375 z M 16.25 14.34375 L 16.875 14.34375 L 16.875 14.96875 L 17.53125 14.96875 L 17.53125 14.34375 L 18.15625 14.34375 L 18.15625 13.71875 L 16.25 13.71875 L 16.25 14.34375 z M 18.15625 14.34375 L 18.15625 14.96875 L 18.78125 14.96875 L 18.78125 15.625 L 18.15625 15.625 L 18.15625 16.25 L 18.78125 16.25 L 18.78125 16.875 L 19.4375 16.875 L 19.4375 15.625 L 20.0625 15.625 L 20.0625 14.34375 L 18.15625 14.34375 z M 20.0625 14.34375 L 20.6875 14.34375 L 20.6875 13.71875 L 20.0625 13.71875 L 20.0625 14.34375 z M 20.6875 14.34375 L 20.6875 15.625 L 21.34375 15.625 L 21.34375 16.875 L 21.96875 16.875 L 21.96875 14.34375 L 20.6875 14.34375 z M 19.4375 16.875 L 19.4375 17.53125 L 18.15625 17.53125 L 18.15625 18.15625 L 20.6875 18.15625 L 20.6875 19.4375 L 20.0625 19.4375 L 20.0625 20.0625 L 21.34375 20.0625 L 21.34375 18.15625 L 21.96875 18.15625 L 21.96875 17.53125 L 20.0625 17.53125 L 20.0625 16.875 L 19.4375 16.875 z M 21.34375 20.0625 L 21.34375 20.6875 L 21.96875 20.6875 L 21.96875 20.0625 L 21.34375 20.0625 z M 21.34375 20.6875 L 20.6875 20.6875 L 20.6875 21.96875 L 21.96875 21.96875 L 21.96875 21.34375 L 21.34375 21.34375 L 21.34375 20.6875 z M 18.15625 16.25 L 17.53125 16.25 L 17.53125 16.875 L 18.15625 16.875 L 18.15625 16.25 z M 17.53125 16.875 L 16.875 16.875 L 16.875 17.53125 L 16.25 17.53125 L 16.25 18.15625 L 17.53125 18.15625 L 17.53125 16.875 z M 17.53125 20.0625 L 18.15625 20.0625 L 18.15625 19.4375 L 17.53125 19.4375 L 17.53125 20.0625 z M 18.15625 20.0625 L 18.15625 21.34375 L 18.78125 21.34375 L 18.78125 20.6875 L 19.4375 20.6875 L 19.4375 20.0625 L 18.15625 20.0625 z M 19.4375 20.6875 L 19.4375 21.34375 L 20.0625 21.34375 L 20.0625 20.6875 L 19.4375 20.6875 z M 19.4375 21.34375 L 18.78125 21.34375 L 18.78125 21.96875 L 19.4375 21.96875 L 19.4375 21.34375 z M 13.71875 16.875 L 13.71875 16.25 L 12.4375 16.25 L 12.4375 16.875 L 13.71875 16.875 z M 12.4375 16.25 L 12.4375 15.625 L 13.09375 15.625 L 13.09375 14.96875 L 12.4375 14.96875 L 12.4375 14.34375 L 14.34375 14.34375 L 14.34375 12.4375 L 13.71875 12.4375 L 13.71875 13.71875 L 11.1875 13.71875 L 11.1875 14.34375 L 11.8125 14.34375 L 11.8125 16.25 L 12.4375 16.25 z M 11.1875 14.34375 L 10.53125 14.34375 L 10.53125 14.96875 L 11.1875 14.96875 L 11.1875 14.34375 z M 10.53125 14.96875 L 9.90625 14.96875 L 9.90625 15.625 L 10.53125 15.625 L 10.53125 14.96875 z M 9.90625 15.625 L 8.625 15.625 L 8.625 16.875 L 9.90625 16.875 L 9.90625 15.625 z M 9.90625 14.96875 L 9.90625 14.34375 L 10.53125 14.34375 L 10.53125 13.71875 L 8.625 13.71875 L 8.625 14.34375 L 9.28125 14.34375 L 9.28125 14.96875 L 9.90625 14.96875 z "
id="path10" />
<path
style="fill:#000000;stroke:none"
d="m 13.71875,14.96875 0,1.28125 0.625,0 0,-1.28125 -0.625,0 z m 3.8125,0 0,0.65625 0.625,0 0,-0.65625 -0.625,0 z m -7,1.28125 0,0.625 0.65625,0 0,-0.625 -0.65625,0 z m -1.90625,1.28125 0,4.4375 4.46875,0 0,-4.4375 -4.46875,0 z m 0.65625,0.625 3.15625,0 0,3.1875 -3.15625,0 0,-3.1875 z"
id="path14"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
style="fill:#000000;stroke:none"
d="m 9.9043479,18.791304 0,1.904348 1.9043481,0 0,-1.904348 -1.9043481,0 m 8.8869561,0 0,0.634783 0.634783,0 0,-0.634783 -0.634783,0 z"
id="path18" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

3421
android/art/intro_1.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 215 KiB

232
android/art/intro_3.svg Normal file
View File

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="550"
height="330"
id="svg3593"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="intro_3.svg">
<defs
id="defs3595">
<inkscape:path-effect
effect="spiro"
id="path-effect10505"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect10509"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect10513"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect10517"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect10527"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.8066667"
inkscape:cx="273.05915"
inkscape:cy="216.42064"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3598">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-72.047244,-414.12837)">
<g
id="g3656"
transform="matrix(1.8527508,0,0,1.8527508,-534.03482,-567.62578)">
<g
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="stroke:#5f8dd3;stroke-opacity:0.26600988"
id="g10533"
transform="translate(310.49464,529.52136)">
<path
style="fill:none;stroke:#5f8dd3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter"
d="m 53.577152,36.539389 c 3.446669,6.821656 9.246643,12.426248 16.182357,15.637227 6.935714,3.210979 14.961735,4.00731 22.393192,2.221824 7.907299,-1.899812 14.832479,-6.515235 21.932259,-10.481067 3.54988,-1.982916 7.19909,-3.826658 11.06232,-5.095157 3.86324,-1.268499 7.9621,-1.950918 12.01023,-1.568465 7.55605,0.713868 14.29924,5.037997 20.0543,9.986012 5.75505,4.948014 10.88297,10.652598 17.09252,15.016659 12.23228,8.596828 27.64271,11.323994 40.71864,18.573413 6.04873,3.353473 11.52302,7.634008 17.23318,11.53619 5.71015,3.902183 11.78069,7.481432 18.48492,9.180305 5.1565,1.30667 10.61142,1.45602 15.8165,0.35874 5.20509,-1.09728 10.15089,-3.446136 14.24242,-6.845596 4.09153,-3.39946 7.31575,-7.848629 9.2019,-12.822496 1.88615,-4.973867 2.42149,-10.462917 1.45781,-15.694384 -1.62813,-8.838564 -7.38236,-16.470616 -14.28877,-22.221494 -6.90641,-5.750877 -14.97858,-9.888348 -22.85805,-14.210969 -6.67516,-3.661949 -13.34554,-7.524675 -20.68086,-9.564462 -3.66766,-1.019893 -7.48659,-1.570017 -11.28788,-1.364723 -3.80128,0.205295 -7.58759,1.182729 -10.89298,3.071203 -4.13421,2.362008 -7.4027,6.094007 -9.57536,10.330791 -2.17267,4.236784 -3.28771,8.963787 -3.66217,13.710427 -0.74893,9.493278 1.38814,18.940272 2.93547,28.336495 1.54732,9.396223 2.47833,19.246711 -0.57059,28.268198 -1.52447,4.51075 -4.06131,8.72983 -7.62149,11.89147 -3.56017,3.16164 -8.17786,5.2126 -12.93779,5.33022 -4.16366,0.10288 -8.27321,-1.25405 -11.87295,-3.34893 -3.59974,-2.09488 -6.73235,-4.90518 -9.64832,-7.87903 -5.83193,-5.94769 -11.09784,-12.779115 -18.483,-16.632163 -4.39315,-2.292031 -9.39515,-3.403741 -14.34559,-3.188348 -4.95043,0.215392 -9.83697,1.757353 -14.01444,4.422291 -4.17747,2.664938 -7.63548,6.446226 -9.91745,10.84461 -2.281978,4.39838 -3.38224,9.40291 -3.155521,14.35284"
id="path10503"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#5f8dd3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter"
d="m 62.863858,97.260161 c 5.097542,1.2417 10.535263,1.054311 15.535232,-0.535361 4.99997,-1.589672 9.547503,-4.576956 12.992021,-8.534487 3.444517,-3.957531 5.775903,-8.873685 6.660624,-14.045147 C 98.936456,68.973704 98.371914,63.562122 96.438873,58.684612 93.295457,50.753045 86.579149,44.460097 78.70377,41.178446 70.828391,37.896796 61.897847,37.510872 53.577152,39.396837 45.07097,41.324844 37.068097,45.628355 31.011904,51.904889 c -6.056192,6.276533 -10.10073,14.535604 -11.009767,23.210049 -0.659048,6.28893 0.316775,12.723156 2.661565,18.595715 2.34479,5.872559 6.042984,11.181417 10.628449,15.535547 9.17093,8.70828 21.746528,13.41706 34.373791,14.1186 12.627264,0.70153 25.285435,-2.41925 36.744388,-7.77016 11.45896,-5.35091 21.79045,-12.87585 31.31845,-21.191927 17.74857,-15.491021 32.97169,-33.867347 51.88219,-47.916416 9.45525,-7.024534 19.85036,-12.939244 31.11597,-16.378996 11.26561,-3.439753 23.45429,-4.322673 34.87158,-1.425894 16.92854,4.295087 31.28951,16.988129 38.57555,32.860653 6.64539,14.476878 7.55739,31.493879 2.49737,46.59809 -5.06003,15.10422 -16.03914,28.1376 -30.06439,35.68966 -14.02525,7.55206 -30.94989,9.54377 -46.34516,5.45394 -15.39528,-4.08982 -29.10011,-14.21837 -37.5283,-27.73528"
id="path10507"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#5f8dd3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter"
d="m 210.7368,15.108529 c -9.52495,3.060099 -18.19707,8.730176 -24.82036,16.228234 -6.62329,7.498058 -11.1797,16.803687 -13.04083,26.633487 -3.11209,16.436863 1.4141,33.760561 10.32072,47.92134 8.90661,14.16078 21.56713,26.2605 36.11281,34.23029 13.44766,7.36817 29.50755,13.57769 44.641,11.10619 11.28635,-1.84322 21.62967,-9.64003 28.92782,-18.44436 9.26901,-11.18194 14.01979,-26.08798 16.20543,-40.446713 1.96182,-12.888262 2.02067,-26.992939 -3.08496,-38.988294 -3.92476,-9.220957 -10.69253,-18.662768 -19.95598,-22.486142 -10.45699,-4.315992 -23.22057,-1.704106 -33.87266,2.105018 -27.98558,10.007472 -49.35913,33.035776 -72.31941,52.153432 -11.48014,9.558828 -23.61256,18.515379 -37.12743,24.880379 -13.51486,6.36499 -28.55064,10.05696 -43.426629,8.68963 C 83.922493,117.27793 68.836585,110.10932 59.292048,97.974523 51.598811,88.193446 47.783569,75.44405 48.83938,63.044831 49.895191,50.645612 55.811132,38.72484 65.047363,30.385331 74.283594,22.045822 86.744566,17.373849 99.186856,17.585462 c 12.442294,0.211614 24.737184,5.304626 33.684484,13.953393"
id="path10511"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csssaaaaaaasssssssc" />
<path
style="fill:none;stroke:#5f8dd3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter"
d="m 172.87561,57.255888 c 8.01552,7.276306 13.79209,16.986143 16.36168,27.502338 2.56959,10.516195 1.92171,21.795824 -1.83508,31.948644 -3.75678,10.15282 -10.60712,19.13738 -19.40305,25.44807 -8.79593,6.31069 -19.50154,9.92173 -30.32279,10.22799 -10.82124,0.30626 -21.71395,-2.69351 -30.85273,-8.49661 -9.13877,-5.8031 -16.486304,-14.38584 -20.8113,-24.30993 C 81.687344,109.6523 80.402541,98.427374 82.373237,87.78268 84.343934,77.137987 89.562085,67.116978 97.153235,59.398974 108.15303,48.215368 123.29475,42.333858 137.87187,36.539389 c 28.70028,-11.408483 58.29155,-23.895549 89.1345,-22.291834 15.42148,0.801857 30.87843,5.329118 43.31677,14.480748 12.43835,9.151631 21.59546,23.178461 23.27965,38.528653 1.04157,9.493175 -0.76057,19.216885 -4.6936,27.919559 -3.93303,8.702675 -9.9518,16.396405 -17.13383,22.691145 -14.36406,12.58946 -32.98622,19.40885 -51.75186,22.96858 -18.05976,3.42583 -37.42238,3.91954 -54.12871,-3.7479 -8.35317,-3.83373 -15.85684,-9.70893 -21.03065,-17.30527 -5.17382,-7.59635 -7.91434,-16.95018 -6.99227,-26.094719 0.88562,-8.783118 5.05751,-17.008373 10.68216,-23.812107 5.62466,-6.803734 12.6598,-12.302867 19.94401,-17.289627 14.56842,-9.973521 30.87199,-18.599268 40.81003,-33.191916"
id="path10515"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssssssssssssssssssssc" />
<path
style="fill:none;stroke:#5f8dd3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter"
d="m 216.95343,76.218535 c -0.63775,-10.663931 2.74188,-21.52057 9.31969,-29.938324 6.5778,-8.417754 16.29534,-14.321824 26.79712,-16.281092 10.12874,-1.889671 21.15578,0.01023 29.3712,6.228668 4.10771,3.10922 7.45652,7.247127 9.48852,11.981208 2.03199,4.734081 2.723,10.059732 1.80327,15.128719 -0.82355,4.538908 -2.9037,8.791225 -5.62962,12.512686 -2.72593,3.721461 -6.08741,6.938042 -9.64973,9.868873 -7.12465,5.861663 -15.13974,10.67971 -21.59516,17.271167 -6.10626,6.23494 -10.59583,13.82967 -15.41085,21.10816 -4.81503,7.27849 -10.1631,14.45867 -17.42261,19.30226 -4.70252,3.13755 -10.14495,5.20747 -15.77111,5.759 -5.62616,0.55153 -11.42693,-0.44266 -16.44787,-3.04036 -5.02094,-2.5977 -9.2248,-6.81889 -11.60028,-11.9487 -2.37548,-5.12981 -2.86461,-11.1488 -1.13733,-16.53159 1.51394,-4.71795 4.62952,-8.8005 8.31707,-12.11003 3.68756,-3.30954 7.94829,-5.909015 12.23973,-8.385821 4.29144,-2.476805 8.65526,-4.860296 12.64033,-7.804834 3.98507,-2.944537 7.61316,-6.500118 9.9915,-10.846904 2.4769,-4.526912 3.50341,-9.800373 3.11774,-14.94617 -0.38567,-5.145796 -2.15822,-10.155315 -4.91446,-14.517782 -5.51247,-8.724936 -14.77814,-14.642151 -24.66663,-17.596744 -9.88849,-2.954594 -20.40488,-3.170152 -30.69277,-2.350934 -10.28789,0.819217 -20.46984,2.645355 -30.73654,3.697344 -8.44667,0.865498 -17.15082,1.255551 -24.9183,4.685068 -3.88374,1.714758 -7.47926,4.203998 -10.114932,7.532218 -2.635675,3.328219 -4.260174,7.533457 -4.11436,11.7764 0.166509,4.845137 2.611199,9.412323 5.975762,12.902712 3.36456,3.490389 7.60074,6.014831 11.9365,8.183757 4.33577,2.168926 8.83001,4.028261 13.04511,6.423272 4.21509,2.395011 8.19738,5.386788 10.96844,9.364766 3.08088,4.422741 4.5049,9.937482 4.13305,15.314672 -0.37185,5.37719 -2.50704,10.5913 -5.87043,14.8032 -3.36338,4.2119 -7.92575,7.42411 -12.96108,9.34712 -5.03532,1.92302 -10.52909,2.57136 -15.895,2.06197 C 95.817577,134.1537 85.842178,128.53798 78.297211,120.83841 70.752243,113.13884 65.474495,103.46263 61.625888,93.392963 55.045111,76.174732 52.432838,57.453444 54.048935,39.091463"
id="path10525"
inkscape:connector-curvature="0" />
</g>
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 528.73107,597.28713 9.13163,5.2711 0,10.54963 -7.93635,4.58066 0,18.03313 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.886 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
id="rect7319-3"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10477"
d="m 448.27081,614.25901 9.13163,5.2711 0,10.54964 -7.93635,4.58064 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57322 0,-10.54964 9.13162,-5.2711 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 484.18906,576.53886 9.13163,5.2711 0,10.54963 -7.93635,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
id="path10479"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10481"
d="m 444.47926,552.78386 9.13163,5.2711 0,10.54963 -7.93635,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 408.06273,577.35447 9.13162,5.2711 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
id="path10483"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10485"
d="m 409.49657,636.84092 9.13162,5.2711 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 373.31363,615.62797 9.13163,5.27111 0,10.54963 -7.93635,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
id="path10487"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10489"
d="m 364.72877,558.55827 9.13163,5.27111 0,10.54963 -7.93635,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 519.15159,537.4864 9.13162,5.27111 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
id="path10491"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10493"
d="m 490.91156,638.98513 9.13162,5.27111 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 532.20067,661.00753 9.13162,5.27111 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
id="path10495"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
inkscape:connector-curvature="0"
id="path10497"
d="m 567.87483,620.45653 9.13162,5.27111 0,10.54964 -7.93634,4.58066 0,18.0331 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23757 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54964 9.13162,-5.27111 z"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 563.92217,551.69123 9.13162,5.2711 0,10.54963 -7.93634,4.58066 0,18.03311 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23758 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.2711 z"
id="path10499"
inkscape:connector-curvature="0" />
<path
inkscape:export-ydpi="166.74196"
inkscape:export-xdpi="166.74196"
inkscape:export-filename="/home/rlafuente/Projects/GPG/ilustras-site/weboftrust.png"
style="color:#000000;fill:#5f8dd3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 602.43919,583.17759 9.13162,5.27111 0,10.54963 -7.93634,4.58066 0,18.0331 c 0,0.53513 -0.43001,0.96513 -0.96513,0.96513 l -0.46772,0 c -0.53513,0 -0.96513,-0.43 -0.96513,-0.96513 l 0,-0.48999 -0.69044,0 -3.50418,0 0,-1.04681 1.31408,0 c 0.13383,0 0.24499,-0.28144 0.24499,-0.40831 0,-0.12684 -0.11116,-0.4306 -0.24499,-0.4306 l -0.59393,0 0,-0.57907 c 0,0 0.24499,-0.10375 0.24499,-0.23757 l 0,-0.0668 c 0,-0.13382 -0.11116,-0.24498 -0.24499,-0.24498 l -0.72015,0 0,-0.7053 0.95029,0 c 0.13385,0 0.24501,-0.11115 0.24501,-0.24498 l 0,-0.77211 c 0,-0.13385 -0.11116,-0.24498 -0.24501,-0.24498 l -0.95029,0 0,-0.68301 3.50418,0 0.69044,0 0,-11.88598 -7.92892,-4.57324 0,-10.54963 9.13162,-5.27111 z"
id="path10501"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

5092
android/art/intro_4.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 326 KiB

139
android/build.gradle Normal file
View File

@ -0,0 +1,139 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
versionCode 15
versionName '0.6.1'
minSdkVersion 14
targetSdkVersion 26
multiDexEnabled true
// For Espresso
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
lintOptions {
abortOnError false
}
packagingOptions {
exclude 'LICENSE.txt'
}
dexOptions {
javaMaxHeapSize '2g'
jumboMode = true
}
}
def supportVersion = '25.4.0'
configurations.all {
resolutionStrategy {
force "com.android.support:design:$supportVersion"
force "com.android.support:support-annotations:$supportVersion"
}
}
dependencies {
// Local dependencies
implementation(project(':core')) {
// Replaced with Android-specific JavaMail
exclude group: 'com.sun.mail'
exclude group: 'com.sun.activation'
// BouncyCastle is replaced with SpongyCastle
exclude group: 'org.bouncycastle'
}
implementation project(':crypto')
implementation fileTree(dir: 'libs', include: '*.jar')
// Android Support Repository dependencies
//noinspection GradleCompatible
implementation 'com.android.support:multidex:1.0.3'
implementation "com.android.support:support-annotations:$supportVersion"
implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:preference-v7:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:recyclerview-v7:$supportVersion"
// Remote dependencies
implementation 'com.sun.mail:android-mail:1.6.3'
implementation 'com.sun.mail:android-activation:1.6.3'
implementation "net.i2p:router:$rootProject.i2pVersion"
implementation "net.i2p.android:client:$rootProject.i2pVersion@aar"
implementation 'net.i2p.android:helper:0.9.5@aar'
implementation 'net.i2p.android.ext:floatingactionbutton:1.10.1'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation 'com.lambdaworks:scrypt:1.4.0'
implementation 'com.google.zxing:core:3.3.0'
implementation 'com.google.zxing:android-integration:3.3.0'
implementation 'com.androidplot:androidplot-core:1.5.6'
implementation 'com.inkapplications.viewpageindicator:library:2.4.4'
implementation 'com.pnikosis:materialish-progress:1.7'
implementation 'com.mikepenz:iconics-core:2.7.2@aar'
implementation 'com.mikepenz:google-material-typeface:2.2.0.3.original@aar'
implementation('com.mikepenz:materialdrawer:5.4.0@aar') {
transitive = true
}
implementation 'org.sufficientlysecure:html-textview:3.1'
// Testing-only dependencies
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
}
project.ext.i2pbase = '../i2p.i2p'
def Properties props = new Properties()
def propFile = new File(project(':android').projectDir, 'local.properties')
if (propFile.canRead()) {
props.load(new FileInputStream(propFile))
if (props != null &&
props.containsKey('i2psrc')) {
i2pbase = props['i2psrc']
} else {
println 'local.properties found but some entries are missing'
}
} else {
println 'local.properties not found'
}
task certificatesZip(type: Zip) {
archiveName = 'certificates_zip'
into('reseed') {
from files('' + i2pbase + '/installer/resources/certificates/reseed')
}
into('ssl') {
from files('' + i2pbase + '/installer/resources/certificates/ssl')
}
}
task copyI2PResources(type: Copy) {
// Force this to always run: Copy only detects source changes, not if missing in destination
outputs.upToDateWhen { false }
into 'src/main/res/raw'
from certificatesZip
}
task cleanI2PResources(type: Delete) {
delete file('src/main/res/raw/certificates_zip')
}
preBuild.dependsOn copyI2PResources
clean.dependsOn cleanI2PResources
apply from: "${project.rootDir}/gradle/signing.gradle"

Binary file not shown.

Binary file not shown.

22
android/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,22 @@
-dontobfuscate
-dontoptimize
-dontpreverify
-dontshrink
-dontwarn com.sun.mail.handlers.handler_base
-dontwarn java.awt.**
-dontwarn java.beans.Beans
-dontwarn javax.naming.**
-dontwarn javax.security.sasl.**
-dontwarn javax.security.auth.callback.NameCallback
-dontwarn android.test.**
-dontwarn org.junit.**
-dontwarn net.sf.ntru.**
-keepclassmembers class i2p.bote.crypto.ECUtils {
public static java.security.spec.ECParameterSpec getParameters(java.lang.String);
public static byte[] encodePoint(java.security.spec.ECParameterSpec, java.security.spec.ECPoint, boolean);
public static java.security.spec.ECPoint decodePoint(java.security.spec.EllipticCurve, byte[]);
}

View File

@ -0,0 +1,81 @@
package i2p.bote.android;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.widget.DrawerLayout;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import i2p.bote.android.config.SettingsActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeDown;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
@RunWith(AndroidJUnit4.class)
public class EmailListActivityTest {
@Rule
public IntentsTestRule<EmailListActivity> mActivityRule = new IntentsTestRule<>(EmailListActivity.class);
@Before
public void closeIntro() {
try {
// Close intro on first open
onView(withId(R.id.skip_intro)).perform(click());
// Close nav drawer on first open
// FIXME: 6/20/15 doesn't work
onView(withClassName(equalTo(DrawerLayout.class.getName()))).perform(swipeRight());
} catch (NoMatchingViewException e) {
}
}
@Test
public void checkEmailFromActionMenuWhenNotConnected() {
openActionBarOverflowOrOptionsMenu(mActivityRule.getActivity());
onView(withText(R.string.check_email)).perform(click());
onView(withText(R.string.bote_needs_to_be_connected)).inRoot(withDecorView(not(mActivityRule.getActivity().getWindow().getDecorView()))) .check(matches(isDisplayed()));
}
@Test
public void checkEmailByPullWhenNotConnected() {
onView(withId(R.id.swipe_refresh)).perform(swipeDown());
onView(withText(R.string.bote_needs_to_be_connected)).inRoot(withDecorView(not(mActivityRule.getActivity().getWindow().getDecorView()))) .check(matches(isDisplayed()));
}
@Test
public void newEmail() {
onView(withId(R.id.promoted_action)).perform(click());
intended(hasComponent(NewEmailActivity.class.getName()));
}
@Test
public void openSettings() {
openActionBarOverflowOrOptionsMenu(mActivityRule.getActivity());
onView(withText(R.string.action_settings)).perform(click());
intended(hasComponent(SettingsActivity.class.getName()));
}
@Test
public void openHelp() {
openActionBarOverflowOrOptionsMenu(mActivityRule.getActivity());
onView(withText(R.string.help)).perform(click());
intended(hasComponent(HelpActivity.class.getName()));
}
}

View File

@ -0,0 +1,35 @@
package i2p.bote.android.intro;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import i2p.bote.android.R;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
@RunWith(AndroidJUnit4.class)
public class IntroActivityTest {
@Rule
public ActivityTestRule<IntroActivity> mRule = new ActivityTestRule<>(IntroActivity.class);
@Test
public void enterSetup() {
onView(withId(R.id.pager)).perform(swipeLeft(), swipeLeft(), swipeLeft(), swipeLeft(), swipeLeft());
onView(withId(R.id.start_setup_wizard)).perform(click());
// TODO check result
}
@Test
public void closeIntro() {
onView(withId(R.id.skip_intro)).perform(click());
// TODO check result
}
}

View File

@ -0,0 +1,84 @@
package i2p.bote.android.intro;
import android.app.Activity;
import android.app.Instrumentation;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import i2p.bote.android.R;
import i2p.bote.android.config.SetPasswordActivity;
import i2p.bote.android.identities.EditIdentityActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.Intents.intending;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class SetupActivityTest {
@Rule
public IntentsTestRule<SetupActivity> mRule = new IntentsTestRule<>(SetupActivity.class);
@Test
public void setPassword() {
// Check we are on "Set password" page
onView(withId(R.id.textView)).check(matches(withText(R.string.set_password)));
onView(withId(R.id.button_set_password)).perform(click());
intended(hasComponent(SetPasswordActivity.class.getName()));
}
@Test
public void nextPageAfterSetPassword() {
// Check we are on "Set password" page
onView(withId(R.id.textView)).check(matches(withText(R.string.set_password)));
intending(hasComponent(SetPasswordActivity.class.getName())).respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
onView(withId(R.id.button_set_password)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.create_identity)));
}
@Test
public void createIdentity() {
onView(withId(R.id.textView)).check(matches(withText(R.string.set_password)));
onView(withId(R.id.button_skip)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.create_identity)));
onView(withId(R.id.button_set_password)).perform(click());
intended(hasComponent(EditIdentityActivity.class.getName()));
}
@Test
public void nextPageAfterCreateIdentity() {
// Check we are on "Create identity" page
onView(withId(R.id.button_skip)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.create_identity)));
intending(hasComponent(EditIdentityActivity.class.getName())).respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
onView(withId(R.id.button_set_password)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.setup_finished)));
}
@Test
public void setupFinished() {
onView(withId(R.id.textView)).check(matches(withText(R.string.set_password)));
onView(withId(R.id.button_skip)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.create_identity)));
onView(withId(R.id.button_skip)).perform(click());
onView(withId(R.id.textView)).check(matches(withText(R.string.setup_finished)));
onView(withId(R.id.button_finish)).perform(click());
// TODO check result
}
}

View File

@ -0,0 +1,213 @@
package i2p.bote.android.provider;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.ProviderTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Part;
import javax.mail.internet.MimeBodyPart;
import i2p.bote.I2PBote;
import i2p.bote.android.InitActivities;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ContentAttachment;
import i2p.bote.email.Attachment;
import i2p.bote.email.Email;
import i2p.bote.folder.EmailFolder;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AttachmentProviderTests extends ProviderTestCase2<AttachmentProvider> {
private static final String URI_PREFIX = "content://" + AttachmentProvider.AUTHORITY;
private static final String INVALID_URI = URI_PREFIX + "/invalid";
private static final String NO_MATCH_URI = URI_PREFIX + "/foo/bar/1/RAW";
public AttachmentProviderTests() {
super(AttachmentProvider.class, AttachmentProvider.AUTHORITY);
setContext(InstrumentationRegistry.getTargetContext());
}
@Before
public void setUp() throws Exception {
super.setUp();
testAndroidTestCaseSetupProperly();
new InitActivities(getMockContext().getDir("botetest", Context.MODE_PRIVATE).getAbsolutePath()).initialize();
}
@Test(expected = IllegalArgumentException.class)
public void queryWithInvalidUriThrows() {
Uri invalidUri = Uri.parse(INVALID_URI);
getMockContentResolver().query(invalidUri, null, null, null, null);
}
@Test
public void getTypeWithInvalidUriReturnsNull() {
Uri invalidUri = Uri.parse(INVALID_URI);
String type = getMockContentResolver().getType(invalidUri);
assertThat("Type was not null", type, is(nullValue()));
}
@Test(expected = FileNotFoundException.class)
public void openFileWithInvalidUriThrows() throws FileNotFoundException {
Uri invalidUri = Uri.parse(INVALID_URI);
getMockContentResolver().openFileDescriptor(invalidUri, "r");
}
@Test
public void queryWithNoMatchReturnsNull() {
Uri noMatchUri = Uri.parse(NO_MATCH_URI);
Cursor c = getMockContentResolver().query(noMatchUri, null, null, null, null);
assertThat(c, is(nullValue()));
}
@Test
public void queryWithValidTextUri() throws Exception {
ContentAttachment attachment = createTextAttachment();
Uri uri = createEmailWithAttachment(attachment);
Cursor c = getMockContentResolver().query(uri, null, null, null, null);
assertThat(c.getCount(), is(1));
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = c.getColumnIndex(OpenableColumns.SIZE);
c.moveToFirst();
assertThat(c.getString(nameIndex), is(equalTo(attachment.getFileName())));
assertThat(c.getLong(sizeIndex), is(equalTo(attachment.getSize())));
}
@Test
public void getTypeWithValidTextUri() throws Exception {
ContentAttachment attachment = createTextAttachment();
Uri uri = createEmailWithAttachment(attachment);
String type = getMockContentResolver().getType(uri);
assertThat("Type was not correct", type, is("text/plain"));
}
@Test
public void getTypeWithValidImageUri() throws Exception {
ContentAttachment attachment = createImageAttachment();
Uri uri = createEmailWithAttachment(attachment);
String type = getMockContentResolver().getType(uri);
assertThat("Type was not correct", type, is("image/png"));
}
@Test
public void openFileWithValidTextUri() throws Exception {
ContentAttachment attachment = createTextAttachment();
Uri uri = createEmailWithAttachment(attachment);
openFileWithValidUri(attachment, uri);
}
@Test
public void openFileWithValidImageUri() throws Exception {
ContentAttachment attachment = createImageAttachment();
Uri uri = createEmailWithAttachment(attachment);
openFileWithValidUri(attachment, uri);
assertThat("Image could not be decoded",
BitmapFactory.decodeStream(getMockContentResolver().openInputStream(uri)),
is(notNullValue()));
}
private void openFileWithValidUri(ContentAttachment attachment, Uri uri) throws Exception {
InputStream inProv = getMockContentResolver().openInputStream(uri);
InputStream inOrig = attachment.getDataHandler().getInputStream();
ByteArrayOutputStream outProv = new ByteArrayOutputStream();
ByteArrayOutputStream outOrig = new ByteArrayOutputStream();
BoteHelper.copyStream(inProv, outProv);
BoteHelper.copyStream(inOrig, outOrig);
assertThat("Provider content does not match original content",
outProv.toByteArray(), is(equalTo(outOrig.toByteArray())));
}
private ContentAttachment createTextAttachment() throws Exception {
return createAttachment("test.txt", "text/plain", "Test file content".getBytes());
}
private ContentAttachment createImageAttachment() throws Exception {
Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.intro_1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
return createAttachment("intro_1.png", "image/png", out.toByteArray());
}
private ContentAttachment createAttachment(final String fileName, final String mimeType, final byte[] content) throws Exception {
Part part = new MimeBodyPart();
part.setDataHandler(new DataHandler(new DataSource() {
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(content);
}
@Override
public OutputStream getOutputStream() throws IOException {
throw new IOException("Cannot write to attachments");
}
@Override
public String getContentType() {
return mimeType;
}
@Override
public String getName() {
return fileName;
}
}));
part.setFileName(fileName);
return new ContentAttachment(getMockContext(), part);
}
private Uri createEmailWithAttachment(Attachment attachment) throws Exception {
List<Attachment> attachments = new ArrayList<Attachment>();
attachments.add(attachment);
Email email = new Email(false);
email.setContent("", attachments);
I2PBote.getInstance().getInbox().add(email);
return AttachmentProvider.getUriForAttachment(
I2PBote.getInstance().getInbox().getName(),
email.getMessageID(),
1
);
}
@After
public void tearDown() throws Exception {
super.tearDown();
EmailFolder inbox = I2PBote.getInstance().getInbox();
for (Email email : BoteHelper.getEmails(inbox, null, true)) {
inbox.delete(email.getMessageID());
}
System.setProperty("i2pbote.initialized", "false");
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Bote DEBUG</string>
</resources>

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="i2p.bote.android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="net.i2p">
<uses-sdk xmlns:tools="http://schemas.android.com/tools"
tools:overrideLibrary="android.support.v14.preference" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name="android.support.multidex.MultiDexApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Bote">
<service android:name=".service.BoteService"/>
<activity
android:name=".EmailListActivity"
android:label="@string/app_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".intro.IntroActivity"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".intro.SetupActivity"
android:label="@string/title_activity_setup"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".ViewEmailActivity"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".NewEmailActivity"
android:label="@string/compose"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".addressbook.AddressBookActivity"
android:label="@string/address_book"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".addressbook.ViewContactActivity"
android:parentActivityName=".addressbook.AddressBookActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.addressbook.AddressBookActivity"/>
</activity>
<activity
android:name=".addressbook.EditContactActivity"
android:label="@string/action_new_contact"
android:parentActivityName=".addressbook.ViewContactActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.addressbook.ViewContactActivity"/>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:host="ext"
android:pathPrefix="/i2p.bote:contact"
android:scheme="vnd.android.nfc"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>
</activity>
<activity
android:name=".addressbook.AddressBookShipActivity"
android:parentActivityName=".addressbook.AddressBookActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.addressbook.AddressBookActivity"/>
</activity>
<activity
android:name=".NetworkInfoActivity"
android:label="@string/network_status"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".config.SettingsActivity"
android:label="@string/action_settings"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".config.SetPasswordActivity"
android:label="@string/pref_title_change_password"
android:parentActivityName=".config.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.SettingsActivity"/>
</activity>
<activity
android:name=".identities.IdentityListActivity"
android:label="@string/pref_title_identities"
android:parentActivityName=".config.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.SettingsActivity"/>
</activity>
<activity
android:name=".identities.ViewIdentityActivity"
android:parentActivityName=".identities.IdentityListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.identities.IdentityListActivity"/>
</activity>
<activity
android:name=".identities.EditIdentityActivity"
android:label="@string/title_new_identity"
android:parentActivityName=".identities.ViewIdentityActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".identities.ViewIdentityActivity"/>
</activity>
<activity
android:name=".identities.IdentityShipActivity"
android:parentActivityName=".identities.IdentityListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.identities.IdentityListActivity"/>
</activity>
<activity
android:name=".HelpActivity"
android:label="@string/help"
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<provider
android:name=".provider.AttachmentProvider"
android:authorities="${applicationId}.attachmentprovider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true">
</provider>
</application>
</manifest>

View File

@ -0,0 +1,44 @@
/**
* Copyright (C) 2017 str4d@mail.i2p
* <p>
* This file is part of I2P-Bote.
* I2P-Bote 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.
* <p>
* I2P-Bote 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.android;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.DhtPeerStatsRenderer;
/**
* Renders UI strings for DHT peer stats.
* @see DhtPeerStatsRenderer
*/
class AndroidPeerStatsRenderer implements DhtPeerStatsRenderer {
AndroidPeerStatsRenderer() {
}
@Override
public String translateHeading(DhtPeerStats.Columns column) {
// No-op, headings currently unused
return "";
}
@Override
public String translateContent(DhtPeerStats.Content content) {
// No-op, content currently unused
return "";
}
}

View File

@ -0,0 +1,44 @@
package i2p.bote.android;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import i2p.bote.android.util.LocaleManager;
@SuppressLint("Registered")
public class BoteActivityBase extends AppCompatActivity {
private final LocaleManager localeManager = new LocaleManager();
@Override
protected void onCreate(Bundle savedInstanceState) {
localeManager.onCreate(this);
super.onCreate(savedInstanceState);
// Initialize I2P settings
InitActivities init = new InitActivities(this);
init.initialize();
// Initialize screen security
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
prefs.getBoolean("pref_screen_security", true))
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
else
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
@Override
public void onResume() {
super.onResume();
localeManager.onResume(this);
}
public void notifyLocaleChanged() {
localeManager.onResume(this);
}
}

View File

@ -0,0 +1,16 @@
package i2p.bote.android;
public class Constants {
public static final String ANDROID_LOG_TAG = "I2P-Bote";
public static final String SHARED_PREFS = "i2p.bote";
public static final String PREF_SELECTED_IDENTITY = "selectedIdentity";
public static final String EMAILDEST_SCHEME = "bote";
public static final String NDEF_DOMAIN = "i2p.bote";
public static final String NDEF_TYPE_CONTACT = "contact";
public static final String NDEF_TYPE_CONTACT_DESTINATION = "contactDestination";
public static final String NDEF_LEGACY_TYPE_CONTACT = NDEF_DOMAIN + ":" + NDEF_TYPE_CONTACT;
public static final String NDEF_LEGACY_TYPE_CONTACT_DESTINATION = NDEF_DOMAIN + ":" + NDEF_TYPE_CONTACT_DESTINATION;
}

View File

@ -0,0 +1,734 @@
package i2p.bote.android;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.typeface.IIcon;
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.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
import net.i2p.android.ui.I2PAndroidHelper;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.mail.MessagingException;
import i2p.bote.I2PBote;
import i2p.bote.android.addressbook.AddressBookActivity;
import i2p.bote.android.config.SettingsActivity;
import i2p.bote.android.intro.IntroActivity;
import i2p.bote.android.intro.SetupActivity;
import i2p.bote.android.service.BoteService;
import i2p.bote.android.service.Init;
import i2p.bote.android.service.Init.RouterChoice;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.MoveToDialogFragment;
import i2p.bote.email.EmailIdentity;
import i2p.bote.email.IdentitiesListener;
import i2p.bote.fileencryption.PasswordCacheListener;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.FolderListener;
import i2p.bote.network.NetworkStatusListener;
public class EmailListActivity extends BoteActivityBase implements
EmailListFragment.OnEmailSelectedListener,
MoveToDialogFragment.MoveToDialogListener,
PasswordCacheListener,
NetworkStatusListener {
private I2PAndroidHelper mHelper;
private RouterChoice mRouterChoice;
private SharedPreferences mSharedPrefs;
/**
* Navigation drawer variables
*/
private AccountHeader mAccountHeader;
private Drawer mDrawer;
private long mSelected;
private static final String PREF_FIRST_START = "firstStart";
private static final int SHOW_INTRODUCTION = 1;
private static final int RUN_SETUP = 2;
private static final int ID_ADDRESS_BOOK = 1;
private static final int ID_NET_STATUS = 2;
private static final int ID_ALL_MAIL = 3;
private static final int ID_LOCKED = 4;
private static final int LOADER_IDENTITIES = 0;
private static final int LOADER_DRAWER_FOLDERS = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Initialize variables
mHelper = new I2PAndroidHelper(this);
mSharedPrefs = getSharedPreferences(Constants.SHARED_PREFS, 0);
mAccountHeader = new AccountHeaderBuilder()
.withActivity(this)
.withHeaderBackground(R.drawable.drawer_header_background)
.withSelectionListEnabledForSingleProfile(false)
.addProfiles(getLockedProfile())
.withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() {
@Override
public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) {
if (profile.getIdentifier() == ID_LOCKED)
findViewById(R.id.action_log_in).performClick();
else if (!currentProfile)
identitySelected(profile);
return false;
}
})
.withSavedInstance(savedInstanceState)
.build();
IDrawerItem addressBook = new PrimaryDrawerItem()
.withIdentifier(ID_ADDRESS_BOOK)
.withName(R.string.address_book)
.withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_contacts).colorRes(R.color.md_grey_600).sizeDp(24))
.withIconTintingEnabled(true)
.withSelectedIconColorRes(R.color.primary);
IDrawerItem networkStatus = getNetStatusItem(
R.string.network_status, GoogleMaterial.Icon.gmd_cloud_off, R.color.md_grey_600, 0);
// Set the drawer width per Material design spec
// http://www.google.com/design/spec/layout/structure.html#structure-side-nav-1
// Mobile: side nav width = min(screen width - app bar height, 320dp)
// Desktop: side nav width = min(screen width - app bar height, 400dp)
int maxWidth = getResources().getDimensionPixelSize(R.dimen.nav_max_width);
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int drawerWidth = Math.min(dm.widthPixels - toolbar.getLayoutParams().height, maxWidth);
mDrawer = new DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withDrawerWidthPx(drawerWidth)
.withShowDrawerOnFirstLaunch(true)
.withAccountHeader(mAccountHeader)
.addStickyDrawerItems(addressBook, networkStatus)
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
@Override
public boolean onItemClick(View view, int i, IDrawerItem iDrawerItem) {
long id = iDrawerItem.getIdentifier();
if (id == ID_ADDRESS_BOOK) {
mDrawer.setSelection(mSelected, false);
mDrawer.closeDrawer();
Intent ai = new Intent(EmailListActivity.this, AddressBookActivity.class);
startActivity(ai);
return true;
} else if (id == ID_NET_STATUS) {
mDrawer.setSelection(mSelected, false);
netStatusSelected();
return true;
} else {
drawerFolderSelected((EmailFolder) iDrawerItem.getTag(), mSelected == i);
mSelected = mDrawer.getCurrentSelection();
return false;
}
}
})
.withSavedInstance(savedInstanceState)
.build();
mSelected = mDrawer.getCurrentSelection();
// Enable ActionBar app icon to behave as action to toggle nav drawer
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
mDrawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(true);
if (savedInstanceState == null) {
EmailListFragment f = EmailListFragment.newInstance("inbox");
getSupportFragmentManager().beginTransaction()
.add(R.id.list_fragment, f).commit();
}
// If first start, go to introduction and setup wizard
if (mSharedPrefs.getBoolean(PREF_FIRST_START, true)) {
mSharedPrefs.edit().putBoolean(PREF_FIRST_START, false).apply();
Intent i = new Intent(EmailListActivity.this, IntroActivity.class);
startActivityForResult(i, SHOW_INTRODUCTION);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mAccountHeader.saveInstanceState(outState);
mDrawer.saveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu
getMenuInflater().inflate(R.menu.main, menu);
if (isBoteServiceRunning())
menu.findItem(R.id.action_start_bote).setVisible(false);
else
menu.findItem(R.id.action_stop_bote).setVisible(false);
return super.onCreateOptionsMenu(menu);
}
@Override
protected void onStart() {
super.onStart();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (prefs.getBoolean("i2pbote.router.auto", true) ||
prefs.getString("i2pbote.router.use", "internal").equals("android")) {
// Try to bind to I2P Android
mHelper.bind();
}
I2PBote.getInstance().addPasswordCacheListener(this);
I2PBote.getInstance().addNetworkStatusListener(this);
// Fetch current network status
networkStatusChanged();
}
@Override
public void onResume() {
super.onResume();
if (I2PBote.getInstance().isPasswordRequired()) {
// Ensure any existing data is destroyed.
getSupportLoaderManager().destroyLoader(LOADER_IDENTITIES);
} else {
// Password is cached, or not set.
getSupportLoaderManager().initLoader(LOADER_IDENTITIES, null, new IdentityLoaderCallbacks());
}
getSupportLoaderManager().initLoader(LOADER_DRAWER_FOLDERS, null, new DrawerFolderLoaderCallbacks());
}
@Override
protected void onStop() {
super.onStop();
mHelper.unbind();
I2PBote.getInstance().removePasswordCacheListener(this);
I2PBote.getInstance().removeNetworkStatusListener(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_start_bote:
// Init from settings
Init init = new Init(this);
mRouterChoice = init.initialize(mHelper);
if (mRouterChoice == RouterChoice.ANDROID) {
if (!mHelper.isI2PAndroidInstalled()) {
// I2P Android not installed
mHelper.promptToInstall(this);
} else if (!mHelper.isI2PAndroidRunning()) {
// Ask user to start I2P Android
mHelper.requestI2PAndroidStart(this);
} else
startBote();
} else
startBote();
return true;
case R.id.action_stop_bote:
Intent stop = new Intent(this, BoteService.class);
stopService(stop);
supportInvalidateOptionsMenu();
return true;
case R.id.action_settings:
Intent si = new Intent(this, SettingsActivity.class);
startActivity(si);
return true;
case R.id.action_help:
Intent hi = new Intent(this, HelpActivity.class);
startActivity(hi);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SHOW_INTRODUCTION) {
if (resultCode == RESULT_OK) {
Intent i = new Intent(EmailListActivity.this, SetupActivity.class);
startActivityForResult(i, RUN_SETUP);
}
} else if (requestCode == RUN_SETUP) {
if (resultCode == RESULT_OK) {
// TODO implement a UI tutorial?
}
} else if (requestCode == I2PAndroidHelper.REQUEST_START_I2P) {
if (resultCode == RESULT_OK) {
startBote();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@SuppressWarnings("ConstantConditions")
@Override
public void setTitle(CharSequence title) {
getSupportActionBar().setTitle(title);
}
//
// Helpers
//
private IProfile getLockedProfile() {
return new ProfileDrawerItem()
.withIdentifier(ID_LOCKED)
.withEmail(getString(R.string.touch_lock_to_log_in))
.withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_lock).color(Color.WHITE).sizeRes(com.mikepenz.materialdrawer.R.dimen.material_drawer_account_header_selected));
}
private IDrawerItem getNetStatusItem(int nameRes, IIcon icon, int iconColorRes, int padding) {
return new PrimaryDrawerItem()
.withIdentifier(ID_NET_STATUS)
.withName(nameRes)
.withIcon(new IconicsDrawable(this, icon).colorRes(iconColorRes).sizeDp(24).paddingDp(padding))
.withIconTintingEnabled(true)
.withSelectedIconColorRes(R.color.primary);
}
private void identitySelected(IProfile profile) {
EmailIdentity identity = (EmailIdentity) ((ProfileDrawerItem) profile).getTag();
mSharedPrefs.edit()
.putString(Constants.PREF_SELECTED_IDENTITY,
identity == null ? null : identity.getKey())
.apply();
// Trigger the drawer folder loader to update the drawer badges
getSupportLoaderManager().restartLoader(LOADER_DRAWER_FOLDERS, null, new DrawerFolderLoaderCallbacks());
EmailListFragment f = (EmailListFragment) getSupportFragmentManager()
.findFragmentById(R.id.list_fragment);
f.onIdentitySelected();
}
private void drawerFolderSelected(EmailFolder folder, boolean alreadySelected) {
if (!alreadySelected) {
// Create the new fragment
EmailListFragment f = EmailListFragment.newInstance(folder.getName());
// Insert the fragment
getSupportFragmentManager().beginTransaction()
.replace(R.id.list_fragment, f).commit();
}
// Close the drawer
mDrawer.closeDrawer();
}
private void netStatusSelected() {
int boteNotStartedMessage = R.string.network_info_unavailable;
switch (I2PBote.getInstance().getNetworkStatus()) {
case DELAY:
boteNotStartedMessage = R.string.network_info_unavailable_delay;
case NOT_STARTED:
DialogFragment df = NetStatusDialogFragment.newInstance(boteNotStartedMessage);
df.show(getSupportFragmentManager(), "noinfo");
break;
default:
mDrawer.closeDrawer();
Intent nii = new Intent(EmailListActivity.this, NetworkInfoActivity.class);
startActivity(nii);
}
}
private boolean isBoteServiceRunning() {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (BoteService.class.getName().equals(service.service.getClassName()))
return true;
}
return false;
}
private void startBote() {
Intent start = new Intent(this, BoteService.class);
start.putExtra(BoteService.ROUTER_CHOICE, mRouterChoice);
startService(start);
supportInvalidateOptionsMenu();
}
public static class NetStatusDialogFragment extends DialogFragment {
public static DialogFragment newInstance(int message) {
DialogFragment f = new NetStatusDialogFragment();
Bundle args = new Bundle();
args.putInt("message", message);
f.setArguments(args);
return f;
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
int message = getArguments().getInt("message");
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
return builder.create();
}
}
//
// Loaders
//
private class IdentityLoaderCallbacks implements LoaderManager.LoaderCallbacks<ArrayList<IProfile>> {
@Override
public Loader<ArrayList<IProfile>> onCreateLoader(int id, Bundle args) {
return new DrawerIdentityLoader(EmailListActivity.this);
}
@Override
public void onLoadFinished(Loader<ArrayList<IProfile>> loader, ArrayList<IProfile> data) {
mAccountHeader.setProfiles(data);
String selectedIdentity = mSharedPrefs.getString(Constants.PREF_SELECTED_IDENTITY, null);
for (IProfile profile : data) {
EmailIdentity identity = (EmailIdentity) ((ProfileDrawerItem) profile).getTag();
if ((identity == null && selectedIdentity == null) ||
(identity != null && identity.getKey().equals(selectedIdentity))) {
mAccountHeader.setActiveProfile(profile, true);
break;
}
}
}
@Override
public void onLoaderReset(Loader<ArrayList<IProfile>> loader) {
mAccountHeader.clear();
mAccountHeader.addProfiles(getLockedProfile());
}
}
private static class DrawerIdentityLoader extends BetterAsyncTaskLoader<ArrayList<IProfile>> implements IdentitiesListener {
private int identiconSize;
public DrawerIdentityLoader(Context context) {
super(context);
// Must be a multiple of nine
identiconSize = context.getResources().getDimensionPixelSize(R.dimen.identicon);
}
@Override
public ArrayList<IProfile> loadInBackground() {
ArrayList<IProfile> profiles = new ArrayList<>();
try {
// Fetch the identities first, so we trigger any exceptions
Collection<EmailIdentity> identities = I2PBote.getInstance().getIdentities().getAll();
profiles.add(new ProfileDrawerItem()
.withIdentifier(ID_ALL_MAIL)
.withTag(null)
.withEmail(getContext().getString(R.string.all_mail))
.withIcon(getContext().getResources().getDrawable(R.drawable.ic_contact_picture))
);
for (EmailIdentity identity : identities) {
profiles.add(getIdentityDrawerItem(identity));
}
} catch (PasswordException e) {
// TODO handle, but should not get here
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return profiles;
}
private IProfile getIdentityDrawerItem(EmailIdentity identity) {
return new ProfileDrawerItem()
.withIdentifier(identity.hashCode())
.withTag(identity)
.withName(identity.getDescription())
.withEmail(identity.getPublicName() + " <" + identity.getKey().substring(0, 4) + ">")
.withIcon(BoteHelper.getIdentityPicture(identity, identiconSize, identiconSize));
}
@Override
protected void onStartMonitoring() {
I2PBote.getInstance().getIdentities().addIdentitiesListener(this);
}
@Override
protected void onStopMonitoring() {
I2PBote.getInstance().getIdentities().removeIdentitiesListener(this);
}
@Override
protected void releaseResources(ArrayList<IProfile> data) {
}
// IdentitiesListener
@Override
public void identityAdded(String s) {
onContentChanged();
}
@Override
public void identityUpdated(String s) {
onContentChanged();
}
@Override
public void identityRemoved(String s) {
onContentChanged();
}
}
private class DrawerFolderLoaderCallbacks implements LoaderManager.LoaderCallbacks<ArrayList<IDrawerItem>> {
@Override
public Loader<ArrayList<IDrawerItem>> onCreateLoader(int id, Bundle args) {
return new DrawerFolderLoader(EmailListActivity.this, I2PBote.getInstance().getEmailFolders());
}
@Override
public void onLoadFinished(Loader<ArrayList<IDrawerItem>> loader, ArrayList<IDrawerItem> data) {
if (mDrawer.getDrawerItems() == null || mDrawer.getDrawerItems().size() == 0)
mDrawer.setItems(data);
else {
// Assumes that no folders have been added or removed
// TODO change this if necessary when user folders are implemented
for (IDrawerItem item : data) {
mDrawer.updateItem(item);
}
}
}
@Override
public void onLoaderReset(Loader<ArrayList<IDrawerItem>> loader) {
mDrawer.removeAllItems();
}
}
private static class DrawerFolderLoader extends BetterAsyncTaskLoader<ArrayList<IDrawerItem>> implements FolderListener {
private List<EmailFolder> mFolders;
public DrawerFolderLoader(Context context, List<EmailFolder> folders) {
super(context);
mFolders = folders;
}
@Override
public ArrayList<IDrawerItem> loadInBackground() {
ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
for (EmailFolder folder : mFolders) {
drawerItems.add(getFolderDrawerItem(folder));
}
return drawerItems;
}
private IDrawerItem getFolderDrawerItem(EmailFolder folder) {
PrimaryDrawerItem item = new PrimaryDrawerItem()
.withIdentifier(folder.hashCode())
.withTag(folder)
.withIconTintingEnabled(true)
.withSelectedIconColorRes(R.color.primary)
.withIcon(BoteHelper.getFolderIcon(getContext(), folder))
.withName(BoteHelper.getFolderDisplayName(getContext(), folder));
try {
int numNew = BoteHelper.getNumNewEmails(getContext(), folder);
if (numNew > 0)
item.withBadge("" + numNew);
} catch (PasswordException e) {
// Password fetching is handled in EmailListFragment
} catch (MessagingException | GeneralSecurityException | IOException e) {
e.printStackTrace();
}
return item;
}
@Override
protected void onStartMonitoring() {
if (mFolders != null) {
for (EmailFolder folder : mFolders) {
folder.addFolderListener(this);
}
}
}
@Override
protected void onStopMonitoring() {
if (mFolders != null) {
for (EmailFolder folder : mFolders) {
folder.removeFolderListener(this);
}
}
}
@Override
protected void releaseResources(ArrayList<IDrawerItem> data) {
}
// FolderListener
@Override
public void elementAdded(String s) {
onContentChanged();
}
@Override
public void elementUpdated() {
onContentChanged();
}
@Override
public void elementRemoved(String s) {
onContentChanged();
}
}
//
// Interfaces
//
// FolderFragment.OnEmailSelectedListener
@Override
public void onEmailSelected(String folderName, String messageId) {
// In single-pane mode, simply start the detail activity
// for the selected message ID.
Intent detailIntent = new Intent(this, ViewEmailActivity.class);
detailIntent.putExtra(ViewEmailActivity.FOLDER_NAME, folderName);
detailIntent.putExtra(ViewEmailActivity.MESSAGE_ID, messageId);
startActivity(detailIntent);
}
// MoveToDialogFragment.MoveToDialogListener
@Override
public void onFolderSelected(EmailFolder newFolder) {
EmailListFragment f = (EmailListFragment) getSupportFragmentManager().findFragmentById(R.id.list_fragment);
f.onFolderSelected(newFolder);
}
// PasswordCacheListener
@Override
public void passwordProvided() {
// Password is cached, or not set.
getSupportLoaderManager().restartLoader(LOADER_IDENTITIES, null, new IdentityLoaderCallbacks());
// Trigger the drawer folder loader to show the drawer badges
getSupportLoaderManager().restartLoader(LOADER_DRAWER_FOLDERS, null, new DrawerFolderLoaderCallbacks());
}
@Override
public void passwordCleared() {
// Ensure any existing data is destroyed.
getSupportLoaderManager().destroyLoader(LOADER_IDENTITIES);
// Trigger the drawer folder loader to hide the drawer badges
getSupportLoaderManager().restartLoader(LOADER_DRAWER_FOLDERS, null, new DrawerFolderLoaderCallbacks());
// Hide account selection list
if (mAccountHeader.isSelectionListShown())
mAccountHeader.toggleSelectionList(this);
}
// NetworkStatusListener
@Override
public void networkStatusChanged() {
// Update network status
final int statusText;
final IIcon statusIcon;
final int colorRes;
final int padding;
switch (I2PBote.getInstance().getNetworkStatus()) {
case DELAY:
statusText = R.string.connect_delay;
statusIcon = GoogleMaterial.Icon.gmd_av_timer;
colorRes = R.color.md_grey_600;
padding = 3;
break;
case CONNECTING:
statusText = R.string.connecting;
statusIcon = GoogleMaterial.Icon.gmd_cloud_queue;
colorRes = R.color.md_grey_600;
padding = 0;
break;
case CONNECTED:
statusText = R.string.connected;
statusIcon = GoogleMaterial.Icon.gmd_cloud_done;
colorRes = R.color.md_grey_600;
padding = 0;
break;
case ERROR:
statusText = R.string.error;
statusIcon = GoogleMaterial.Icon.gmd_error;
colorRes = R.color.red;
padding = 2;
break;
case NOT_STARTED:
default:
statusText = R.string.not_started;
statusIcon = GoogleMaterial.Icon.gmd_cloud_off;
colorRes = R.color.md_grey_600;
padding = 0;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mDrawer.updateStickyFooterItem(getNetStatusItem(statusText, statusIcon, colorRes, padding));
}
});
}
}

View File

@ -0,0 +1,319 @@
package i2p.bote.android;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.mail.Part;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.MultiSelectionUtil;
import i2p.bote.email.Email;
public class EmailListAdapter extends MultiSelectionUtil.SelectableAdapter<RecyclerView.ViewHolder> {
private static final DateFormat DATE_BEFORE_THIS_YEAR = DateFormat.getDateInstance(DateFormat.MEDIUM);
private static final DateFormat DATE_THIS_YEAR = new SimpleDateFormat(
((SimpleDateFormat) SimpleDateFormat.getDateInstance(DateFormat.MEDIUM))
.toPattern().replaceAll(",?\\W?[Yy]+\\W?", "")
);
private static final DateFormat DATE_TODAY = DateFormat.getTimeInstance();
private Calendar BOUNDARY_DAY;
private Calendar BOUNDARY_YEAR;
private Context mCtx;
private String mFolderName;
private EmailListFragment.OnEmailSelectedListener mListener;
private boolean mIsOutbox;
private List<Email> mEmails;
private int mIncompleteEmails;
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(View itemView) {
super(itemView);
}
}
public static class EmailViewHolder extends RecyclerView.ViewHolder {
public ImageView picture;
//public ImageView emailSelected;
public TextView subject;
public TextView address;
public TextView content;
public TextView sent;
public ImageView emailAttachment;
public TextView emailStatus;
public ImageView emailDelivered;
public EmailViewHolder(View itemView) {
super(itemView);
picture = (ImageView) itemView.findViewById(R.id.contact_picture);
//emailSelected = view.findViewById(R.id.email_selected);
subject = (TextView) itemView.findViewById(R.id.email_subject);
address = (TextView) itemView.findViewById(R.id.email_address);
content = (TextView) itemView.findViewById(R.id.email_content);
sent = (TextView) itemView.findViewById(R.id.email_sent);
emailAttachment = (ImageView) itemView.findViewById(R.id.email_attachment);
emailStatus = (TextView) itemView.findViewById(R.id.email_status);
emailDelivered = (ImageView) itemView.findViewById(R.id.email_delivered);
}
}
public EmailListAdapter(Context context, String folderName,
EmailListFragment.OnEmailSelectedListener listener) {
super();
mCtx = context;
mFolderName = folderName;
mListener = listener;
mIsOutbox = BoteHelper.isOutbox(folderName);
mIncompleteEmails = 0;
setHasStableIds(true);
setDateBoundaries();
}
/**
* Set up the boundaries for date display formats.
* <p/>
* TODO: call this method at midnight to refresh the UI
*/
public void setDateBoundaries() {
BOUNDARY_DAY = Calendar.getInstance();
BOUNDARY_DAY.set(Calendar.HOUR, 0);
BOUNDARY_DAY.set(Calendar.MINUTE, 0);
BOUNDARY_DAY.set(Calendar.SECOND, 0);
BOUNDARY_YEAR = Calendar.getInstance();
BOUNDARY_YEAR.set(Calendar.MONTH, Calendar.JANUARY);
BOUNDARY_YEAR.set(Calendar.DAY_OF_MONTH, 1);
BOUNDARY_YEAR.set(Calendar.HOUR, 0);
BOUNDARY_YEAR.set(Calendar.MINUTE, 0);
BOUNDARY_YEAR.set(Calendar.SECOND, 0);
if (mEmails != null)
notifyDataSetChanged();
}
public void setEmails(List<Email> emails) {
mEmails = emails;
notifyDataSetChanged();
}
public Email getEmail(int position) {
if (mIncompleteEmails > 0)
position--;
if (position < 0)
return null;
return mEmails.get(position);
}
public void setIncompleteEmails(int incompleteEmails) {
if (incompleteEmails > 0) {
if (mIncompleteEmails == 0) {
mIncompleteEmails = incompleteEmails;
notifyItemInserted(0);
} else {
mIncompleteEmails = incompleteEmails;
notifyItemChanged(0);
}
} else if (mIncompleteEmails > 0) {
mIncompleteEmails = 0;
notifyItemRemoved(0);
}
}
@Override
public int getItemViewType(int position) {
if (mEmails == null || mEmails.isEmpty())
return R.layout.listitem_empty;
if (mIncompleteEmails > 0)
position--;
return position < 0 ? R.layout.listitem_incomplete : R.layout.listitem_email;
}
// Create new views (invoked by the layout manager)
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
switch (viewType) {
case R.layout.listitem_email:
return new EmailViewHolder(v);
default:
return new SimpleViewHolder(v);
}
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case R.layout.listitem_empty:
((TextView) holder.itemView).setText(
mCtx.getResources().getString(R.string.folder_empty));
break;
case R.layout.listitem_incomplete:
((TextView) holder.itemView).setText(
mCtx.getResources().getQuantityString(R.plurals.incomplete_emails,
mIncompleteEmails, mIncompleteEmails));
break;
case R.layout.listitem_email:
final EmailViewHolder evh = (EmailViewHolder) holder;
final Email email = getEmail(position);
evh.picture.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectEmail(evh.getAdapterPosition(), evh.getItemId(), true);
}
});
evh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectEmail(evh.getAdapterPosition(), evh.getItemId(), false);
}
});
evh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
selectEmail(evh.getAdapterPosition(), evh.getItemId(), true);
return true;
}
});
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
evh.itemView.setSelected(isSelected(position));
else
evh.itemView.setActivated(isSelected(position));
// TODO fix
//holder.emailSelected.setVisibility(isSelected(position) ? View.VISIBLE : View.GONE);
try {
boolean isSentEmail = BoteHelper.isSentEmail(email);
String otherAddress;
if (isSentEmail)
otherAddress = email.getOneRecipient();
else
otherAddress = email.getOneFromAddress();
Bitmap pic = BoteHelper.getPictureForAddress(otherAddress);
if (pic != null)
evh.picture.setImageBitmap(pic);
else if (isSentEmail || !email.isAnonymous()) {
ViewGroup.LayoutParams lp = evh.picture.getLayoutParams();
evh.picture.setImageBitmap(BoteHelper.getIdenticonForAddress(otherAddress, lp.width, lp.height));
} else
evh.picture.setImageDrawable(
mCtx.getResources().getDrawable(R.drawable.ic_contact_picture));
evh.subject.setText(email.getSubject());
evh.address.setText(BoteHelper.getNameAndShortDestination(otherAddress));
Date date = email.getSentDate();
if (date == null)
date = email.getReceivedDate();
if (date != null) {
DateFormat df;
if (date.before(BOUNDARY_DAY.getTime())) {
if (date.before(BOUNDARY_YEAR.getTime())) // Sent before this year
df = DATE_BEFORE_THIS_YEAR;
else // Sent this year before today
df = DATE_THIS_YEAR;
} else // Sent today
df = DATE_TODAY;
evh.sent.setText(df.format(date));
evh.sent.setVisibility(View.VISIBLE);
} else
evh.sent.setVisibility(View.GONE);
evh.emailAttachment.setVisibility(View.GONE);
for (Part part : email.getParts()) {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
evh.emailAttachment.setVisibility(View.VISIBLE);
break;
}
}
evh.subject.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
evh.address.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
if (email.isAnonymous() && !isSentEmail) {
if (email.isUnread())
evh.address.setTypeface(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
else
evh.address.setTypeface(Typeface.DEFAULT, Typeface.ITALIC);
}
// Set email sending status if this is the outbox,
// or set email delivery status if we sent it.
if (mIsOutbox) {
evh.emailStatus.setText(BoteHelper.getEmailStatusText(
mCtx, email, false));
evh.emailStatus.setVisibility(View.VISIBLE);
} else if (isSentEmail) {
if (email.isDelivered()) {
evh.emailStatus.setVisibility(View.GONE);
} else {
evh.emailStatus.setText(email.getDeliveryPercentage() + "%");
evh.emailStatus.setVisibility(View.VISIBLE);
}
}
evh.emailDelivered.setVisibility(
!mIsOutbox && isSentEmail && email.isDelivered() ?
View.VISIBLE : View.GONE);
} catch (Exception e) {
evh.subject.setText("ERROR: " + e.getMessage());
}
evh.content.setText(email.getText());
break;
default:
break;
}
}
private void selectEmail(int position, long id, boolean selectorOnly) {
if (selectorOnly || getSelector().inActionMode()) {
getSelector().selectItem(position, id);
} else {
final Email email = getEmail(position);
mListener.onEmailSelected(mFolderName, email.getMessageID());
}
}
// Return the size of the dataset (invoked by the layout manager)
@Override
public int getItemCount() {
if (mEmails == null || mEmails.isEmpty())
return 1;
return mIncompleteEmails > 0 ? mEmails.size() + 1 : mEmails.size();
}
public long getItemId(int position) {
if (mEmails == null || mEmails.isEmpty())
return 0;
Email email = getEmail(position);
return email == null ? 1 : email.getMessageID().hashCode();
}
}

View File

@ -0,0 +1,577 @@
package i2p.bote.android;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.pnikosis.materialishprogress.ProgressWheel;
import net.i2p.I2PAppContext;
import net.i2p.util.Log;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import javax.mail.Address;
import javax.mail.Flags.Flag;
import javax.mail.MessagingException;
import i2p.bote.I2PBote;
import i2p.bote.android.util.AuthenticatedFragment;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.MoveToDialogFragment;
import i2p.bote.android.util.MultiSelectionUtil;
import i2p.bote.android.widget.DividerItemDecoration;
import i2p.bote.android.widget.LoadingRecyclerView;
import i2p.bote.android.widget.MultiSwipeRefreshLayout;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.FolderListener;
public class EmailListFragment extends AuthenticatedFragment implements
LoaderManager.LoaderCallbacks<List<Email>>,
MoveToDialogFragment.MoveToDialogListener,
SwipeRefreshLayout.OnRefreshListener {
public static final String FOLDER_NAME = "folder_name";
private static final int EMAIL_LIST_LOADER = 1;
OnEmailSelectedListener mCallback;
private MultiSwipeRefreshLayout mSwipeRefreshLayout;
private AsyncTask<Void, Void, Void> mCheckingTask;
private LoadingRecyclerView mEmailsList;
private EmailListAdapter mAdapter;
private EmailFolder mFolder;
private ImageButton mNewEmail;
private MenuItem mCheckEmail;
// The Controller which provides CHOICE_MODE_MULTIPLE_MODAL-like functionality
private MultiSelectionUtil.Controller mMultiSelectController;
private ModalChoiceListener mModalChoiceListener;
public static EmailListFragment newInstance(String folderName) {
EmailListFragment f = new EmailListFragment();
Bundle args = new Bundle();
args.putString(FOLDER_NAME, folderName);
f.setArguments(args);
return f;
}
// Container Activity must implement this interface
public interface OnEmailSelectedListener {
public void onEmailSelected(String folderName, String messageId);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnEmailSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnEmailSelectedListener");
}
}
@Override
public View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
String folderName = getArguments().getString(FOLDER_NAME);
mFolder = BoteHelper.getMailFolder(folderName);
boolean isInbox = BoteHelper.isInbox(mFolder);
View v = inflater.inflate(
isInbox ? R.layout.fragment_list_emails_with_refresh : R.layout.fragment_list_emails,
container, false);
mEmailsList = (LoadingRecyclerView) v.findViewById(R.id.emails_list);
View empty = v.findViewById(R.id.empty);
ProgressWheel loading = (ProgressWheel) v.findViewById(R.id.loading);
mEmailsList.setLoadingView(empty, loading);
mNewEmail = (ImageButton) v.findViewById(R.id.promoted_action);
mNewEmail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startNewEmail();
}
});
if (isInbox) {
mSwipeRefreshLayout = (MultiSwipeRefreshLayout) v;
// Set up the MultiSwipeRefreshLayout
mSwipeRefreshLayout.setColorSchemeResources(
R.color.primary, R.color.accent, R.color.primary, R.color.accent);
mSwipeRefreshLayout.setSwipeableChildren(R.id.emails_list);
mSwipeRefreshLayout.setOnRefreshListener(this);
}
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mEmailsList.setHasFixedSize(true);
mEmailsList.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
// Use a linear layout manager
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mEmailsList.setLayoutManager(mLayoutManager);
// Set the adapter for the list view
mAdapter = new EmailListAdapter(getActivity(), mFolder.getName(), mCallback);
mEmailsList.setAdapter(mAdapter);
// Attach a MultiSelectionUtil.Controller to the ListView, giving it an instance of
// ModalChoiceListener (see below)
mModalChoiceListener = new ModalChoiceListener();
mMultiSelectController = MultiSelectionUtil
.attachMultiSelectionController(mEmailsList, (AppCompatActivity) getActivity(),
mModalChoiceListener);
// Allow the Controller to restore itself
mMultiSelectController.restoreInstanceState(savedInstanceState);
if (mFolder == null) {
mFolder = I2PBote.getInstance().getInbox();
Toast.makeText(getActivity(), R.string.folder_does_not_exist, Toast.LENGTH_SHORT).show();
}
getActivity().setTitle(
BoteHelper.getFolderDisplayName(getActivity(), mFolder));
}
@Override
public void onStart() {
super.onStart();
if (mSwipeRefreshLayout != null) {
boolean isChecking = I2PBote.getInstance().isCheckingForMail();
mSwipeRefreshLayout.setRefreshing(isChecking);
if (isChecking)
onRefresh();
}
}
@Override
public void onStop() {
super.onStop();
if (mCheckingTask != null) {
mCheckingTask.cancel(true);
mCheckingTask = null;
mSwipeRefreshLayout.setRefreshing(false);
}
}
/**
* Start loading the list of emails from this folder.
* Only called when we have a password cached, or no
* password is required.
*/
protected void onInitializeFragment() {
if (mFolder == null)
return;
if (BoteHelper.isInbox(mFolder)) {
mAdapter.setIncompleteEmails(I2PBote.getInstance().getNumIncompleteEmails());
}
getLoaderManager().initLoader(EMAIL_LIST_LOADER, null, this);
}
protected void onDestroyFragment() {
mAdapter.setIncompleteEmails(0);
getLoaderManager().destroyLoader(EMAIL_LIST_LOADER);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Allow the Controller to save it's instance state so that any checked items are
// stored
if (mMultiSelectController != null)
mMultiSelectController.saveInstanceState(outState);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.email_list, menu);
mCheckEmail = menu.findItem(R.id.action_check_email);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean passwordRequired = I2PBote.getInstance().isPasswordRequired();
mNewEmail.setVisibility(passwordRequired ? View.GONE : View.VISIBLE);
mCheckEmail.setVisible(mSwipeRefreshLayout != null && !passwordRequired);
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setEnabled(!passwordRequired);
if (mSwipeRefreshLayout.isRefreshing()) {
mCheckEmail.setTitle(R.string.checking_email);
mCheckEmail.setEnabled(false);
} else {
mCheckEmail.setTitle(R.string.check_email);
mCheckEmail.setEnabled(true);
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_check_email:
if (!mSwipeRefreshLayout.isRefreshing()) {
mSwipeRefreshLayout.setRefreshing(true);
onRefresh();
getActivity().supportInvalidateOptionsMenu();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void startNewEmail() {
Intent nei = new Intent(getActivity(), NewEmailActivity.class);
startActivity(nei);
}
private class ModalChoiceListener implements MultiSelectionUtil.MultiChoiceModeListener {
private boolean areUnread;
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int numChecked = mAdapter.getSelectedItemCount();
mode.setTitle(getResources().getString(R.string.items_selected, numChecked));
if (checked && numChecked == 1) { // This is the first checked item
Email email = mAdapter.getEmail(position);
areUnread = email.isUnread();
mode.invalidate();
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Respond to clicks on the actions in the CAB
switch (item.getItemId()) {
case R.id.action_delete:
List<Integer> toDelete = mAdapter.getSelectedItems();
if (toDelete.size() == 0)
return false;
for (int i = (toDelete.size() - 1); i >= 0; i--) {
Email email = mAdapter.getEmail(toDelete.get(i));
BoteHelper.revokeAttachmentUriPermissions(
getActivity(),
mFolder.getName(),
email);
// The Loader will update mAdapter
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
}
mode.finish();
return true;
case R.id.action_mark_read:
case R.id.action_mark_unread:
List<Integer> selected = mAdapter.getSelectedItems();
for (int i = (selected.size() - 1); i >= 0; i--) {
Email email = mAdapter.getEmail(selected.get(i));
try {
// The Loader will update mAdapter
mFolder.setNew(email, !areUnread);
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
areUnread = !areUnread;
mode.invalidate();
return true;
case R.id.action_move_to:
DialogFragment f = MoveToDialogFragment.newInstance(mFolder);
f.show(getFragmentManager(), "moveTo");
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate the menu for the CAB
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.email_list_context, menu);
MenuItem markRead = menu.findItem(R.id.action_mark_read);
MenuItem markUnread = menu.findItem(R.id.action_mark_unread);
MenuItem moveTo = menu.findItem(R.id.action_move_to);
menu.findItem(R.id.action_delete).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_delete));
markRead.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_drafts));
markUnread.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_markunread));
moveTo.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_folder));
if (BoteHelper.isOutbox(mFolder)) {
markRead.setVisible(false);
markUnread.setVisible(false);
}
// Only allow moving from the trash
// TODO change this when user folders are implemented
if (!BoteHelper.isTrash(mFolder))
moveTo.setVisible(false);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Here you can perform updates to the CAB due to
// an invalidate() request
if (!BoteHelper.isOutbox(mFolder)) {
menu.findItem(R.id.action_mark_read).setVisible(areUnread);
menu.findItem(R.id.action_mark_unread).setVisible(!areUnread);
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
}
// Called by EmailListActivity.onIdentitySelected()
public void onIdentitySelected() {
getLoaderManager().restartLoader(EMAIL_LIST_LOADER, null, this);
}
// Called by EmailListActivity.onFolderSelected()
public void onFolderSelected(EmailFolder newFolder) {
List<Integer> toMove = mAdapter.getSelectedItems();
for (int i = (toMove.size() - 1); i >= 0; i--) {
Email email = mAdapter.getEmail(toMove.get(i));
mFolder.move(email, newFolder);
}
mMultiSelectController.finish();
}
// LoaderManager.LoaderCallbacks<List<Email>>
public Loader<List<Email>> onCreateLoader(int id, Bundle args) {
return new EmailListLoader(getActivity(), mFolder,
getActivity().getSharedPreferences(Constants.SHARED_PREFS, 0)
.getString(Constants.PREF_SELECTED_IDENTITY, null));
}
private static class EmailListLoader extends BetterAsyncTaskLoader<List<Email>> implements
FolderListener {
private EmailFolder mFolder;
private String mSelectedIdentityKey;
public EmailListLoader(Context context, EmailFolder folder, String selectedIdentityKey) {
super(context);
mFolder = folder;
mSelectedIdentityKey = selectedIdentityKey;
}
@Override
public List<Email> loadInBackground() {
List<Email> emails = null;
try {
List<Email> allEmails = BoteHelper.getEmails(mFolder, null, true);
if (mSelectedIdentityKey != null) {
emails = new ArrayList<>();
for (Email email : allEmails) {
boolean add = false;
if (BoteHelper.isSentEmail(email)) {
String senderDest = BoteHelper.extractEmailDestination(email.getOneFromAddress());
if (mSelectedIdentityKey.equals(senderDest))
add = true;
} else {
for (Address recipient : email.getAllRecipients()) {
String recipientDest = BoteHelper.extractEmailDestination(recipient.toString());
if (mSelectedIdentityKey.equals(recipientDest)) {
add = true;
break;
}
}
}
if (add)
emails.add(email);
}
} else
emails = allEmails;
} catch (PasswordException pe) {
// XXX: Should not get here.
} catch (MessagingException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return emails;
}
protected void onStartMonitoring() {
mFolder.addFolderListener(this);
}
protected void onStopMonitoring() {
mFolder.removeFolderListener(this);
}
protected void releaseResources(List<Email> data) {
}
// FolderListener
@Override
public void elementAdded(String messageId) {
onContentChanged();
}
@Override
public void elementUpdated() {
onContentChanged();
}
@Override
public void elementRemoved(String messageId) {
onContentChanged();
}
}
public void onLoadFinished(Loader<List<Email>> loader,
List<Email> data) {
// Clear recent flags
for (Email email : data)
try {
email.setFlag(Flag.RECENT, false);
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mAdapter.setEmails(data);
try {
getActivity().setTitle(
BoteHelper.getFolderDisplayNameWithNew(getActivity(), mFolder));
} catch (PasswordException e) {
// Should not get here.
Log log = I2PAppContext.getGlobalContext().logManager().getLog(EmailListFragment.class);
if (log.shouldLog(Log.WARN))
log.warn("Email list loader finished, but password is no longer cached", e);
} catch (MessagingException | GeneralSecurityException | IOException e) {
e.printStackTrace();
}
}
public void onLoaderReset(Loader<List<Email>> loader) {
mAdapter.setEmails(null);
getActivity().setTitle(
BoteHelper.getFolderDisplayName(getActivity(), mFolder));
}
// SwipeRefreshLayout.OnRefreshListener
public void onRefresh() {
// If we are already checking, do nothing else
if (mCheckingTask != null)
return;
I2PBote bote = I2PBote.getInstance();
if (bote.isConnected()) {
try {
if (!bote.isCheckingForMail())
bote.checkForMail();
mCheckingTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
while (I2PBote.getInstance().isCheckingForMail()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (isCancelled()) {
break;
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
mAdapter.setIncompleteEmails(I2PBote.getInstance().getNumIncompleteEmails());
// Notify PullToRefreshLayout that the refresh has finished
mSwipeRefreshLayout.setRefreshing(false);
getActivity().supportInvalidateOptionsMenu();
}
};
mCheckingTask.execute();
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
mSwipeRefreshLayout.setRefreshing(false);
Toast.makeText(getActivity(), R.string.bote_needs_to_be_connected, Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -0,0 +1,59 @@
package i2p.bote.android;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.htmltextview.HtmlTextView;
public class HelpAboutFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_help_about, container, false);
TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
versionText.setText(getString(R.string.version) + " " + getVersion());
TextView licenseText = (TextView) view.findViewById(R.id.help_about_license);
licenseText.setText(getString(R.string.license, "GPLv3+"));
HtmlTextView aboutLibsView = (HtmlTextView) view.findViewById(R.id.help_about_libraries);
// load html from raw resource (Parsing handled by HtmlTextView library)
aboutLibsView.setHtml(R.raw.help_about_libraries);
// no flickering when clicking textview for Android < 4
aboutLibsView.setTextColor(getResources().getColor(android.R.color.black));
return view;
}
/**
* Get the current package version.
*
* @return The current version.
*/
private String getVersion() {
String result = "";
try {
PackageManager manager = getActivity().getPackageManager();
PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
result = String.format("%s (%s)", info.versionName, info.versionCode);
} catch (NameNotFoundException e) {
Log.w(Constants.ANDROID_LOG_TAG, "Unable to get application version: " + e.getMessage());
result = "Unable to get application version.";
}
return result;
}
}

View File

@ -0,0 +1,98 @@
package i2p.bote.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import com.viewpagerindicator.TitlePageIndicator;
public class HelpActivity extends BoteActivityBase {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
*/
SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
*/
ViewPager mViewPager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_help);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Create the sections adapter.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
// Bind the page indicator to the pager.
TitlePageIndicator pageIndicator = (TitlePageIndicator) findViewById(R.id.page_indicator);
pageIndicator.setViewPager(mViewPager);
}
/**
* A {@link android.support.v4.app.FragmentPagerAdapter} that returns a fragment corresponding to
* one of the help sections.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 1:
return getString(R.string.pref_title_identities);
case 2:
return getString(R.string.changelog);
case 3:
return getString(R.string.about);
case 0:
default:
return getString(R.string.start);
}
}
@Override
public Fragment getItem(int position) {
switch (position) {
case 1:
return HelpHtmlFragment.newInstance(R.raw.help_identities);
case 2:
return HelpHtmlFragment.newInstance(R.raw.help_changelog);
case 3:
return new HelpAboutFragment();
case 0:
default:
return HelpHtmlFragment.newInstance(R.raw.help_start);
}
}
@Override
public int getCount() {
return 4;
}
}
}

View File

@ -0,0 +1,35 @@
package i2p.bote.android;
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.ScrollView;
import org.sufficientlysecure.htmltextview.HtmlTextView;
public class HelpHtmlFragment extends Fragment {
public static final String ARG_HTML_FILE = "htmlFile";
static HelpHtmlFragment newInstance(int htmlFile) {
HelpHtmlFragment f = new HelpHtmlFragment();
Bundle args = new Bundle();
args.putInt(ARG_HTML_FILE, htmlFile);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ScrollView scroller = new ScrollView(getActivity());
HtmlTextView text = new HtmlTextView(getActivity());
scroller.addView(text);
int padH = getResources().getDimensionPixelOffset(R.dimen.activity_horizontal_margin);
int padV = getResources().getDimensionPixelOffset(R.dimen.activity_vertical_margin);
text.setPadding(padH, padV, padH, padV);
text.setHtml(getArguments().getInt(ARG_HTML_FILE));
text.setTextColor(getResources().getColor(R.color.primary_text_default_material_light));
return scroller;
}
}

View File

@ -0,0 +1,34 @@
package i2p.bote.android;
import android.content.Context;
import java.security.Security;
public class InitActivities {
static {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
private final String myDir;
public InitActivities(Context c) {
this(c.getFilesDir().getAbsolutePath());
}
public InitActivities(String i2pBaseDir) {
myDir = i2pBaseDir;
}
public void initialize() {
// Don't initialize twice
if (System.getProperty("i2pbote.initialized", "false").equals("true"))
return;
// Set up the locations so settings can find them
System.setProperty("i2p.dir.base", myDir);
System.setProperty("i2p.dir.config", myDir);
System.setProperty("wrapper.logfile", myDir + "/wrapper.log");
System.setProperty("i2pbote.initialized", "true");
}
}

View File

@ -0,0 +1,25 @@
package i2p.bote.android;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
public class NetworkInfoActivity extends BoteActivityBase {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
NetworkInfoFragment f = new NetworkInfoFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f).commit();
}
}
}

View File

@ -0,0 +1,234 @@
package i2p.bote.android;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
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 android.widget.Toast;
import com.androidplot.pie.PieChart;
import com.androidplot.pie.Segment;
import com.androidplot.pie.SegmentFormatter;
import net.i2p.android.ui.I2PAndroidHelper;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Set;
import i2p.bote.I2PBote;
import i2p.bote.network.BannedPeer;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.DhtPeerStatsRow;
import i2p.bote.network.RelayPeer;
public class NetworkInfoFragment extends Fragment {
private Exception mConnectError;
private PieChart mKademliaPie;
private TextView mKademliaPeers;
private PieChart mRelayPie;
private TextView mRelayPeers;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mConnectError = I2PBote.getInstance().getConnectError();
if (mConnectError == null)
return inflater.inflate(R.layout.fragment_network_info, container, false);
else
return inflater.inflate(R.layout.fragment_network_error, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mConnectError == null) {
mKademliaPie = (PieChart) view.findViewById(R.id.kademlia_peers_pie);
mKademliaPeers = (TextView) view.findViewById(R.id.kademlia_peers);
mRelayPie = (PieChart) view.findViewById(R.id.relay_peers_pie);
mRelayPeers = (TextView) view.findViewById(R.id.relay_peers);
setupKademliaPeers();
setupRelayPeers();
Collection<BannedPeer> bannedPeers = I2PBote.getInstance().getBannedPeers();
((TextView) view.findViewById(R.id.banned_peers)).setText(
"" + bannedPeers.size());
} else {
((TextView) view.findViewById(R.id.error)).setText(mConnectError.toString());
view.findViewById(R.id.copy_error).setOnClickListener(new View.OnClickListener() {
@SuppressWarnings("deprecation")
@Override
public void onClick(View view) {
String fullError = joinStackTrace(mConnectError);
Object clipboardService = getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) clipboardService;
clipboard.setText(fullError);
} else {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) clipboardService;
android.content.ClipData clip = android.content.ClipData.newPlainText(
getString(R.string.bote_connection_error), fullError);
clipboard.setPrimaryClip(clip);
}
Toast.makeText(getActivity(), R.string.full_error_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
});
if ((new I2PAndroidHelper(getActivity())).isI2PAndroidInstalled())
view.findViewById(R.id.error_page_i2p_content).setVisibility(View.VISIBLE);
}
}
private void setupSegmentFormatter(SegmentFormatter sf) {
sf.getLabelPaint().setTextSize(20);
}
private void setupKademliaPeers() {
DhtPeerStats dhtStats = I2PBote.getInstance().getDhtStats(new AndroidPeerStatsRenderer());
if (dhtStats != null) {
if (dhtStats.getData().size() == 0) {
Segment n = new Segment("", 100);
SegmentFormatter nf = new SegmentFormatter(getResources().getColor(android.R.color.darker_gray));
setupSegmentFormatter(nf);
mKademliaPie.addSeries(n, nf);
} else {
int reachable = 0;
for (DhtPeerStatsRow row : dhtStats.getData()) {
if (row.isReachable())
reachable += 1;
}
int unreachable = dhtStats.getData().size() - reachable;
mKademliaPeers.setText("" + dhtStats.getData().size());
if (reachable > 0) {
Segment r = new Segment(getString(R.string.reachable), reachable);
SegmentFormatter rf = new SegmentFormatter(getResources().getColor(R.color.green));
setupSegmentFormatter(rf);
mKademliaPie.addSeries(r, rf);
}
if (unreachable > 0) {
Segment u = new Segment(getString(R.string.unreachable), dhtStats.getData().size() - reachable);
SegmentFormatter uf = new SegmentFormatter(getResources().getColor(R.color.error_color));
setupSegmentFormatter(uf);
mKademliaPie.addSeries(u, uf);
}
}
}
mKademliaPie.getBorderPaint().setColor(Color.TRANSPARENT);
mKademliaPie.getBackgroundPaint().setColor(Color.TRANSPARENT);
}
private void setupRelayPeers() {
Set<RelayPeer> relayPeers = I2PBote.getInstance().getRelayPeers();
mRelayPeers.setText("" + relayPeers.size());
if (relayPeers.size() == 0) {
Segment n = new Segment("", 100);
SegmentFormatter nf = new SegmentFormatter(getResources().getColor(android.R.color.darker_gray));
setupSegmentFormatter(nf);
mRelayPie.addSeries(n, nf);
} else {
int good = 0;
int untested = 0;
for (RelayPeer relayPeer : relayPeers) {
int reachability = relayPeer.getReachability();
if (reachability == 0)
untested += 1;
else if (reachability > 80)
good += 1;
}
int bad = relayPeers.size() - good - untested;
if (good > 0) {
Segment g = new Segment(getString(R.string.good), good);
SegmentFormatter gf = new SegmentFormatter(getResources().getColor(R.color.green));
setupSegmentFormatter(gf);
mRelayPie.addSeries(g, gf);
}
if (bad > 0) {
Segment b = new Segment(getString(R.string.unreliable), bad);
SegmentFormatter bf = new SegmentFormatter(getResources().getColor(R.color.red));
setupSegmentFormatter(bf);
mRelayPie.addSeries(b, bf);
}
if (untested > 0) {
Segment u = new Segment(getString(R.string.untested), untested);
SegmentFormatter uf = new SegmentFormatter(getResources().getColor(R.color.accent));
setupSegmentFormatter(uf);
mRelayPie.addSeries(u, uf);
}
}
mRelayPie.getBorderPaint().setColor(Color.TRANSPARENT);
mRelayPie.getBackgroundPaint().setColor(Color.TRANSPARENT);
}
private static String joinStackTrace(Throwable e) {
StringWriter writer = null;
try {
writer = new StringWriter();
joinStackTrace(e, writer);
return writer.toString();
}
finally {
if (writer != null)
try {
writer.close();
} catch (IOException e1) {
// ignore
}
}
}
private static void joinStackTrace(Throwable e, StringWriter writer) {
PrintWriter printer = null;
try {
printer = new PrintWriter(writer);
while (e != null) {
printer.println(e);
StackTraceElement[] trace = e.getStackTrace();
for (StackTraceElement aTrace : trace) printer.println("\tat " + aTrace);
e = e.getCause();
if (e != null)
printer.println("Caused by:\r\n");
}
}
finally {
if (printer != null)
printer.close();
}
}
}

View File

@ -0,0 +1,57 @@
package i2p.bote.android;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.widget.Toast;
public class NewEmailActivity extends BoteActivityBase implements
NewEmailFragment.Callbacks {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
NewEmailFragment f;
String quoteMsgFolder = null;
String quoteMsgId = null;
NewEmailFragment.QuoteMsgType quoteMsgType = null;
Bundle args = getIntent().getExtras();
if (args != null) {
quoteMsgFolder = args.getString(NewEmailFragment.QUOTE_MSG_FOLDER);
quoteMsgId = args.getString(NewEmailFragment.QUOTE_MSG_ID);
quoteMsgType =
(NewEmailFragment.QuoteMsgType) args.getSerializable(NewEmailFragment.QUOTE_MSG_TYPE);
}
f = NewEmailFragment.newInstance(quoteMsgFolder, quoteMsgId, quoteMsgType);
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f).commit();
}
}
@Override
public void onBackPressed() {
NewEmailFragment f = (NewEmailFragment) getSupportFragmentManager().findFragmentById(R.id.container);
f.onBackPressed();
}
// NewEmailFragment.Callbacks
public void onTaskFinished() {
Toast.makeText(this, R.string.email_queued_for_sending,
Toast.LENGTH_SHORT).show();
finish();
}
@Override
public void onBackPressAllowed() {
super.onBackPressed();
}
}

View File

@ -0,0 +1,712 @@
package i2p.bote.android;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import com.tokenautocomplete.FilteredArrayAdapter;
import net.i2p.data.DataFormatException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import i2p.bote.I2PBote;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ContentAttachment;
import i2p.bote.android.util.Person;
import i2p.bote.android.widget.ContactsCompletionView;
import i2p.bote.email.Attachment;
import i2p.bote.email.Email;
import i2p.bote.email.EmailIdentity;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class NewEmailFragment extends Fragment {
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks {
public void onTaskFinished();
public void onBackPressAllowed();
}
private static Callbacks sDummyCallbacks = new Callbacks() {
public void onTaskFinished() {
}
public void onBackPressAllowed() {
}
};
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof Callbacks))
throw new IllegalStateException("Activity must implement fragment's callbacks.");
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
mCallbacks = sDummyCallbacks;
}
public static final String QUOTE_MSG_FOLDER = "sender";
public static final String QUOTE_MSG_ID = "recipient";
public static enum QuoteMsgType {
REPLY,
REPLY_ALL,
FORWARD
}
public static final String QUOTE_MSG_TYPE = "type";
private static final long MAX_RECOMMENDED_ATTACHMENT_SIZE = 1048576;
private static final int REQUEST_FILE = 1;
private String mSenderKey;
Spinner mSpinner;
int mDefaultPos;
ArrayAdapter<Person> mAdapter;
ImageView mMore;
ContactsCompletionView mTo;
ContactsCompletionView mCc;
ContactsCompletionView mBcc;
EditText mSubject;
EditText mContent;
LinearLayout mAttachments;
private long mTotalAttachmentSize;
private View mAttachmentSizeWarning;
boolean mMoreVisible;
boolean mDirty;
public static NewEmailFragment newInstance(String quoteMsgFolder, String quoteMsgId,
QuoteMsgType quoteMsgType) {
NewEmailFragment f = new NewEmailFragment();
Bundle args = new Bundle();
args.putString(QUOTE_MSG_FOLDER, quoteMsgFolder);
args.putString(QUOTE_MSG_ID, quoteMsgId);
args.putSerializable(QUOTE_MSG_TYPE, quoteMsgType);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_new_email, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mSpinner = (Spinner) view.findViewById(R.id.sender_spinner);
mMore = (ImageView) view.findViewById(R.id.more);
mTo = (ContactsCompletionView) view.findViewById(R.id.to);
mCc = (ContactsCompletionView) view.findViewById(R.id.cc);
mBcc = (ContactsCompletionView) view.findViewById(R.id.bcc);
mSubject = (EditText) view.findViewById(R.id.subject);
mContent = (EditText) view.findViewById(R.id.message);
mAttachments = (LinearLayout) view.findViewById(R.id.attachments);
String quoteMsgFolder = getArguments().getString(QUOTE_MSG_FOLDER);
String quoteMsgId = getArguments().getString(QUOTE_MSG_ID);
QuoteMsgType quoteMsgType = (QuoteMsgType) getArguments().getSerializable(QUOTE_MSG_TYPE);
boolean hide = I2PBote.getInstance().getConfiguration().getHideLocale();
List<Person> toRecipients = new ArrayList<Person>();
List<Person> ccRecipients = new ArrayList<Person>();
String origSubject = null;
String origContent = null;
String origFrom = null;
try {
Email origEmail = BoteHelper.getEmail(quoteMsgFolder, quoteMsgId);
if (origEmail != null) {
mSenderKey = BoteHelper.extractEmailDestination(
BoteHelper.getOneLocalRecipient(origEmail).toString());
if (quoteMsgType == QuoteMsgType.REPLY) {
String recipient = BoteHelper.getNameAndDestination(
origEmail.getReplyAddress(I2PBote.getInstance().getIdentities()));
toRecipients.add(extractPerson(recipient));
} else if (quoteMsgType == QuoteMsgType.REPLY_ALL) {
// TODO split between To and Cc
// TODO don't include our address
// What happens if an email is received by multiple local identities?
for (Address address : origEmail.getAllAddresses(true)) {
Person person = extractPerson(address.toString());
if (person != null)
toRecipients.add(person);
}
}
origSubject = origEmail.getSubject();
origContent = origEmail.getText();
origFrom = BoteHelper.getShortSenderName(origEmail.getOneFromAddress(), 50);
}
} catch (PasswordException e) {
// Should not happen, we cannot get to this page without authenticating
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// Set up identities spinner
IdentityAdapter identities = new IdentityAdapter(getActivity());
mSpinner.setAdapter(identities);
mSpinner.setSelection(mDefaultPos);
// Set up Cc/Bcc button
mMore.setImageDrawable(new IconicsDrawable(getActivity(), GoogleMaterial.Icon.gmd_unfold_more).colorRes(R.color.md_grey_600).sizeDp(24).paddingDp(3));
mMore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mCc.setVisibility(mMoreVisible ? View.GONE : View.VISIBLE);
mBcc.setVisibility(mMoreVisible ? View.GONE : View.VISIBLE);
mMore.setImageDrawable(new IconicsDrawable(getActivity(), mMoreVisible ?
GoogleMaterial.Icon.gmd_unfold_more : GoogleMaterial.Icon.gmd_unfold_less)
.colorRes(R.color.md_grey_600)
.sizeDp(24)
.paddingDp(mMoreVisible ? 3 : 4));
mMoreVisible = !mMoreVisible;
}
});
// Set up contacts auto-complete
List<Person> contacts = new ArrayList<Person>();
try {
for (Contact contact : I2PBote.getInstance().getAddressBook().getAll()) {
contacts.add(new Person(contact.getName(), contact.getBase64Dest(),
BoteHelper.decodePicture(contact.getPictureBase64())));
}
} catch (PasswordException e) {
// TODO handle
e.printStackTrace();
}
mAdapter = new FilteredArrayAdapter<Person>(getActivity(), android.R.layout.simple_list_item_1, contacts) {
@Override
protected boolean keepObject(Person obj, String mask) {
mask = mask.toLowerCase(Locale.US);
return obj.getName().toLowerCase(Locale.US).startsWith(mask) || obj.getAddress().toLowerCase(Locale.US).startsWith(mask);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null)
v = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.listitem_contact, parent, false);
else
v = convertView;
setViewContent(v, position);
return v;
}
private void setViewContent(View v, int position) {
Person person = getItem(position);
((TextView) v.findViewById(R.id.contact_name)).setText(person.getName());
ImageView picView = (ImageView) v.findViewById(R.id.contact_picture);
Bitmap picture = person.getPicture();
if (picture == null) {
ViewGroup.LayoutParams lp = picView.getLayoutParams();
picture = BoteHelper.getIdenticonForAddress(person.getAddress(), lp.width, lp.height);
}
picView.setImageBitmap(picture);
}
};
mTo.setAdapter(mAdapter);
mCc.setAdapter(mAdapter);
mBcc.setAdapter(mAdapter);
for (Person recipient : toRecipients) {
mTo.addObject(recipient);
}
for (Person recipient : ccRecipients) {
mCc.addObject(recipient);
}
if (origSubject != null) {
String subjectPrefix;
if (quoteMsgType == QuoteMsgType.FORWARD) {
subjectPrefix = getResources().getString(
hide ? R.string.subject_prefix_fwd_hide
: R.string.subject_prefix_fwd);
} else {
subjectPrefix = getResources().getString(
hide ? R.string.response_prefix_re_hide
: R.string.response_prefix_re);
}
if (!origSubject.startsWith(subjectPrefix))
origSubject = subjectPrefix + " " + origSubject;
mSubject.setText(origSubject);
}
if (origContent != null) {
StringBuilder quotation = new StringBuilder();
quotation.append("\n\n");
quotation.append(getResources().getString(
hide ? R.string.response_quote_wrote_hide
: R.string.response_quote_wrote,
origFrom));
String[] lines = origContent.split("\r?\n|\r");
for (String line : lines)
quotation = quotation.append("\n> ").append(line);
mContent.setText(quotation);
}
if (savedInstanceState == null) {
mTo.setPrefix(getResources().getString(R.string.email_to) + " ");
mCc.setPrefix(getResources().getString(R.string.email_cc) + " ");
mBcc.setPrefix(getResources().getString(R.string.email_bcc) + " ");
}
TextWatcher dirtyWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDirty = true;
}
@Override
public void afterTextChanged(Editable s) {
}
};
mSubject.addTextChangedListener(dirtyWatcher);
mContent.addTextChangedListener(dirtyWatcher);
}
private Person extractPerson(String recipient) {
if (recipient.equals("Anonymous"))
return null;
String recipientName = BoteHelper.extractName(recipient);
try {
recipientName = BoteHelper.getName(recipient);
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
String recipientAddr = BoteHelper.extractEmailDestination(recipient);
if (recipientAddr == null) { // Assume external address
recipientAddr = recipient;
if (recipientName.isEmpty())
recipientName = recipientAddr;
return new Person(recipientName, recipientAddr, null, true);
} else {
if (recipientName.isEmpty()) // Dest with no name
recipientName = recipientAddr.substring(0, 5);
Bitmap recipientPic = null;
try {
recipientPic = BoteHelper.getPictureForDestination(recipientAddr);
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
return new Person(recipientName, recipientAddr, recipientPic);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.new_email, menu);
menu.findItem(R.id.action_attach_file).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_attach_file));
menu.findItem(R.id.action_send_email).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_send));
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_attach_file:
requestFile();
return true;
case R.id.action_send_email:
if (sendEmail())
mCallbacks.onTaskFinished();
return true;
case android.R.id.home:
if (mDirty) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.stop_composing_email)
.setMessage(R.string.all_changes_will_be_discarded)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
mCallbacks.onBackPressAllowed();
else
getActivity().onNavigateUp();
}
}).setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.cancel();
}
}).show();
return true;
} else
return super.onOptionsItemSelected(item);
default:
return super.onOptionsItemSelected(item);
}
}
private void requestFile() {
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.setType("*/*");
i.addCategory(Intent.CATEGORY_OPENABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(
Intent.createChooser(i,
getResources().getString(R.string.select_attachment)),
REQUEST_FILE);
}
@SuppressLint("NewApi")
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_CANCELED) {
System.out.println("Cancelled");
}
return;
}
switch (requestCode) {
case REQUEST_FILE:
addAttachment(data.getData());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
data.getClipData() != null) {
ClipData clipData = data.getClipData();
for (int i = 0; i < clipData.getItemCount(); i++) {
addAttachment(clipData.getItemAt(i).getUri());
}
}
break;
}
}
private void addAttachment(Uri uri) {
// Try to create a ContentAttachment using the provided Uri.
try {
final ContentAttachment attachment = new ContentAttachment(getActivity(), uri);
final View v = getActivity().getLayoutInflater().inflate(R.layout.listitem_attachment, mAttachments, false);
v.setTag(attachment);
((TextView) v.findViewById(R.id.filename)).setText(attachment.getFileName());
((TextView) v.findViewById(R.id.size)).setText(attachment.getHumanReadableSize());
ImageView attachmentAction = (ImageView) v.findViewById(R.id.attachment_action);
attachmentAction.setImageDrawable(new IconicsDrawable(getActivity(), GoogleMaterial.Icon.gmd_clear).colorRes(R.color.md_grey_600).sizeDp(24).paddingDp(5));
attachmentAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
updateAttachmentSizeCount(attachment.getSize(), false);
attachment.clean();
mAttachments.removeView(v);
}
});
mAttachments.addView(v);
updateAttachmentSizeCount(attachment.getSize(), true);
} catch (IllegalArgumentException iae) {
Log.e(Constants.ANDROID_LOG_TAG, "Failed to get attachment", iae);
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e(Constants.ANDROID_LOG_TAG, "File not found: " + uri);
}
}
private void updateAttachmentSizeCount(long size, boolean increase) {
if (increase) {
mTotalAttachmentSize += size;
if (mTotalAttachmentSize > MAX_RECOMMENDED_ATTACHMENT_SIZE &&
mAttachmentSizeWarning == null) {
mAttachmentSizeWarning = getActivity().getLayoutInflater().inflate(
R.layout.listitem_attachment_warning, mAttachments, false);
TextView warning = (TextView) mAttachmentSizeWarning.findViewById(
R.id.attachment_warning_text);
warning.setText(
getString(R.string.attachment_size_warning,
BoteHelper.getHumanReadableSize(
getActivity(), MAX_RECOMMENDED_ATTACHMENT_SIZE))
);
mAttachments.addView(mAttachmentSizeWarning, 0);
}
} else {
mTotalAttachmentSize -= size;
if (mTotalAttachmentSize <= MAX_RECOMMENDED_ATTACHMENT_SIZE &&
mAttachmentSizeWarning != null) {
mAttachments.removeView(mAttachmentSizeWarning);
mAttachmentSizeWarning = null;
}
}
}
private boolean sendEmail() {
Email email = new Email(I2PBote.getInstance().getConfiguration().getIncludeSentTime());
try {
// Set sender
EmailIdentity sender = (EmailIdentity) mSpinner.getSelectedItem();
InternetAddress ia = new InternetAddress(
sender == null ? "Anonymous" :
BoteHelper.getNameAndDestination(sender.getKey()));
email.setFrom(ia);
// We must continue to set "Sender:" even with only one mailbox
// in "From:", which is against RFC 2822 but required for older
// Bote versions to see a sender (and validate the signature).
email.setSender(ia);
for (Object obj : mTo.getObjects()) {
Person person = (Person) obj;
email.addRecipient(Message.RecipientType.TO, new InternetAddress(
person.getAddress(), person.getName()));
}
if (mMoreVisible) {
for (Object obj : mCc.getObjects()) {
Person person = (Person) obj;
email.addRecipient(Message.RecipientType.CC, new InternetAddress(
person.getAddress(), person.getName()));
}
for (Object obj : mBcc.getObjects()) {
Person person = (Person) obj;
email.addRecipient(Message.RecipientType.BCC, new InternetAddress(
person.getAddress(), person.getName()));
}
}
// Check that we have someone to send to
Address[] rcpts = email.getAllRecipients();
if (rcpts == null || rcpts.length == 0) {
// No recipients
mTo.setError(getActivity().getString(R.string.add_one_recipient));
mTo.requestFocus();
return false;
} else {
mTo.setError(null);
}
email.setSubject(mSubject.getText().toString(), "UTF-8");
// Extract the attachments
List<Attachment> attachments = new ArrayList<Attachment>();
for (int i = 0; i < mAttachments.getChildCount(); i++) {
View v = mAttachments.getChildAt(i);
// Warning views don't have tags set
if (v.getTag() != null)
attachments.add((Attachment) v.getTag());
}
// Set the text and add attachments
email.setContent(mContent.getText().toString(), attachments);
// Cache the fact that we sent this email
BoteHelper.setEmailSent(email, true);
// Send the email
I2PBote.getInstance().sendEmail(email);
// Clean up attachments
for (Attachment attachment : attachments) {
if (!attachment.clean())
Log.e(Constants.ANDROID_LOG_TAG, "Can't clean up attachment: <" + attachment + ">");
}
return true;
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (AddressException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
private class IdentityAdapter extends ArrayAdapter<EmailIdentity> {
private LayoutInflater mInflater;
public IdentityAdapter(Context context) {
super(context, android.R.layout.simple_spinner_item);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
try {
Collection<EmailIdentity> identities = I2PBote.getInstance().getIdentities().getAll();
mDefaultPos = 0;
String selectedIdentity = getActivity().getSharedPreferences(Constants.SHARED_PREFS, 0)
.getString(Constants.PREF_SELECTED_IDENTITY, null);
for (EmailIdentity identity : identities) {
add(identity);
boolean isDefaultIdentity = selectedIdentity == null ?
identity.isDefaultIdentity() :
identity.getKey().equals(selectedIdentity);
boolean selectByDefault = mSenderKey == null ?
isDefaultIdentity :
identity.getKey().equals(mSenderKey);
if (selectByDefault)
mDefaultPos = getPosition(identity);
}
} catch (PasswordException e) {
// TODO Handle
e.printStackTrace();
} catch (IOException e) {
// TODO Handle
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Handle
e.printStackTrace();
}
}
@Override
public EmailIdentity getItem(int position) {
if (position > 0)
return super.getItem(position - 1);
else
return null;
}
@Override
public int getPosition(EmailIdentity item) {
if (item != null)
return super.getPosition(item) + 1;
else
return 0;
}
@Override
public int getCount() {
return super.getCount() + 1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null)
v = mInflater.inflate(android.R.layout.simple_spinner_item, parent, false);
else
v = convertView;
setViewText(v, position);
return v;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null)
v = mInflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false);
else
v = convertView;
setViewText(v, position);
return v;
}
private void setViewText(View v, int position) {
TextView text = (TextView) v.findViewById(android.R.id.text1);
EmailIdentity identity = getItem(position);
if (identity == null)
text.setText("Anonymous");
else
text.setText(identity.getPublicName());
}
}
public void onBackPressed() {
if (mDirty) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.stop_composing_email)
.setMessage(R.string.all_changes_will_be_discarded)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
mCallbacks.onBackPressAllowed();
}
}).setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.cancel();
}
}).show();
} else
mCallbacks.onBackPressAllowed();
}
}

View File

@ -0,0 +1,215 @@
package i2p.bote.android;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.FolderListener;
public class ViewEmailActivity extends BoteActivityBase implements
LoaderManager.LoaderCallbacks<List<String>> {
public static final String FOLDER_NAME = "folder_name";
public static final String MESSAGE_ID = "message_id";
private static final int MESSAGE_ID_LIST_LOADER = 1;
private EmailFolder mFolder;
// The messageId of the currently-viewed Email
private String mMessageId;
private ViewPager mPager;
private ViewEmailPagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_email);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(false);
Intent i = getIntent();
String folderName = i.getStringExtra(FOLDER_NAME);
mFolder = BoteHelper.getMailFolder(
folderName == null ? "inbox" : folderName);
mMessageId = i.getStringExtra(MESSAGE_ID);
// Instantiate the ViewPager and PagerAdapter
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ViewEmailPagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
mMessageId = mPagerAdapter.getMessageId(position);
// Mark the visible email as not new
if (mMessageId != null) {
try {
if (!BoteHelper.isOutbox(mFolder))
mFolder.setNew(mMessageId, false);
mFolder.setRecent(mMessageId, false);
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
// Fire off a Loader to fetch the list of Emails
getSupportLoaderManager().initLoader(MESSAGE_ID_LIST_LOADER, null, this);
}
private class ViewEmailPagerAdapter extends FragmentStatePagerAdapter {
private List<String> mIds;
public ViewEmailPagerAdapter(FragmentManager fm) {
super(fm);
}
public void setData(List<String> data) {
mIds = data;
notifyDataSetChanged();
}
public int getPosition(String messageId) {
if (mIds == null)
return 0;
else
return mIds.indexOf(messageId);
}
public String getMessageId(int position) {
if (mIds == null)
return null;
else
return mIds.get(position);
}
@Override
public Fragment getItem(int position) {
if (mIds == null)
return null;
else
return ViewEmailFragment.newInstance(
mFolder.getName(), mIds.get(position));
}
@Override
public int getCount() {
if (mIds == null)
return 0;
else
return mIds.size();
}
}
// LoaderManager.LoaderCallbacks<List<String>>
public Loader<List<String>> onCreateLoader(int id, Bundle args) {
return new MessageIdListLoader(this, mFolder);
}
private static class MessageIdListLoader extends BetterAsyncTaskLoader<List<String>> implements
FolderListener {
private EmailFolder mFolder;
public MessageIdListLoader(Context context, EmailFolder folder) {
super(context);
mFolder = folder;
}
@Override
public List<String> loadInBackground() {
List<String> messageIds = null;
try {
List<Email> emails = BoteHelper.getEmails(mFolder, null, true);
messageIds = new ArrayList<String>();
for (Email email : emails)
messageIds.add(email.getMessageID());
} catch (PasswordException pe) {
// TODO: Handle this error properly (get user to log in)
}
return messageIds;
}
protected void onStartMonitoring() {
mFolder.addFolderListener(this);
}
protected void onStopMonitoring() {
mFolder.removeFolderListener(this);
}
protected void releaseResources(List<String> data) {
}
// FolderListener
@Override
public void elementAdded(String messageId) {
onContentChanged();
}
@Override
public void elementUpdated() {
onContentChanged();
}
@Override
public void elementRemoved(String messageId) {
onContentChanged();
}
}
public void onLoadFinished(Loader<List<String>> loader,
List<String> data) {
mPagerAdapter.setData(data);
mPager.setCurrentItem(
mPagerAdapter.getPosition(mMessageId));
// Mark the current email as not new
if (mMessageId != null) {
try {
if (!BoteHelper.isOutbox(mFolder))
mFolder.setNew(mMessageId, false);
mFolder.setRecent(mMessageId, false);
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void onLoaderReset(Loader<List<String>> loader) {
mPagerAdapter.setData(null);
}
}

View File

@ -0,0 +1,324 @@
package i2p.bote.android;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.Fragment;
import android.support.v7.widget.PopupMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.text.DateFormat;
import java.util.List;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.android.provider.AttachmentProvider;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ContentAttachment;
import i2p.bote.email.Attachment;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
public class ViewEmailFragment extends Fragment {
private String mFolderName;
private String mMessageId;
private boolean mIsAnonymous;
public static ViewEmailFragment newInstance(
String folderName, String messageId) {
ViewEmailFragment f = new ViewEmailFragment();
Bundle args = new Bundle();
args.putString("folderName", folderName);
args.putString("messageId", messageId);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mFolderName = getArguments() != null ? getArguments().getString("folderName") : "inbox";
mMessageId = getArguments() != null ? getArguments().getString("messageId") : "1";
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_view_email, container, false);
try {
Email e = BoteHelper.getEmail(mFolderName, mMessageId);
if (e != null) {
displayEmail(e, v);
} else {
TextView subject = (TextView) v.findViewById(R.id.email_subject);
subject.setText(R.string.email_not_found);
}
} catch (PasswordException e) {
// TODO: Handle
e.printStackTrace();
}
return v;
}
private void displayEmail(Email email, View v) {
View sigInvalid = v.findViewById(R.id.signature_invalid);
TextView subject = (TextView) v.findViewById(R.id.email_subject);
ImageView picture = (ImageView) v.findViewById(R.id.picture);
TextView sender = (TextView) v.findViewById(R.id.email_sender);
LinearLayout toRecipients = (LinearLayout) v.findViewById(R.id.email_to);
TextView sent = (TextView) v.findViewById(R.id.email_sent);
TextView received = (TextView) v.findViewById(R.id.email_received);
TextView content = (TextView) v.findViewById(R.id.email_content);
LinearLayout attachments = (LinearLayout) v.findViewById(R.id.attachments);
try {
String fromAddress = email.getOneFromAddress();
subject.setText(email.getSubject());
Bitmap pic = BoteHelper.getPictureForAddress(fromAddress);
if (pic != null)
picture.setImageBitmap(pic);
else if (!email.isAnonymous()) {
ViewGroup.LayoutParams lp = picture.getLayoutParams();
picture.setImageBitmap(BoteHelper.getIdenticonForAddress(fromAddress, lp.width, lp.height));
}
final String senderDisplay = BoteHelper.getDisplayAddress(fromAddress);
if (!email.isSignatureValid() && !email.isAnonymous()) {
sigInvalid.setVisibility(View.VISIBLE);
sigInvalid.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getActivity(), getString(R.string.signature_invalid, senderDisplay), Toast.LENGTH_LONG).show();
}
});
}
sender.setText(senderDisplay);
if (email.isAnonymous() && !BoteHelper.isSentEmail(email))
sender.setTypeface(Typeface.DEFAULT, Typeface.ITALIC);
Address[] emailToRecipients = email.getToAddresses();
if (emailToRecipients != null) {
for (Address recipient : emailToRecipients) {
TextView tv = new TextView(getActivity());
tv.setText(BoteHelper.getDisplayAddress(recipient.toString()));
tv.setTextAppearance(getActivity(), R.style.TextAppearance_AppCompat_Secondary);
toRecipients.addView(tv);
}
}
Address[] emailCcRecipients = email.getCCAddresses();
if (emailCcRecipients != null) {
v.findViewById(R.id.email_cc_row).setVisibility(View.VISIBLE);
LinearLayout ccRecipients = (LinearLayout) v.findViewById(R.id.email_cc);
for (Address recipient : emailCcRecipients) {
TextView tv = new TextView(getActivity());
tv.setText(BoteHelper.getDisplayAddress(recipient.toString()));
tv.setTextAppearance(getActivity(), R.style.TextAppearance_AppCompat_Secondary);
ccRecipients.addView(tv);
}
}
Address[] emailBccRecipients = email.getBCCAddresses();
if (emailBccRecipients != null) {
v.findViewById(R.id.email_bcc_row).setVisibility(View.VISIBLE);
LinearLayout bccRecipients = (LinearLayout) v.findViewById(R.id.email_bcc);
for (Address recipient : emailBccRecipients) {
TextView tv = new TextView(getActivity());
tv.setText(BoteHelper.getDisplayAddress(recipient.toString()));
tv.setTextAppearance(getActivity(), R.style.TextAppearance_AppCompat_Secondary);
bccRecipients.addView(tv);
}
}
if (email.getSentDate() != null)
sent.setText(DateFormat.getInstance().format(
email.getSentDate()));
if (email.getReceivedDate() != null)
received.setText(DateFormat.getInstance().format(
email.getReceivedDate()));
content.setText(email.getText());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
content.setTextIsSelectable(true);
List<Part> parts = email.getParts();
for (int partIndex = 0; partIndex < parts.size(); partIndex++) {
Part part = parts.get(partIndex);
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
final ContentAttachment attachment = new ContentAttachment(getActivity(), part);
View a = getActivity().getLayoutInflater().inflate(R.layout.listitem_attachment, attachments, false);
((TextView) a.findViewById(R.id.filename)).setText(attachment.getFileName());
((TextView) a.findViewById(R.id.size)).setText(attachment.getHumanReadableSize());
final ImageView action = (ImageView) a.findViewById(R.id.attachment_action);
action.setImageDrawable(new IconicsDrawable(getActivity(), GoogleMaterial.Icon.gmd_more_vert).colorRes(R.color.md_grey_600).sizeDp(24).paddingDp(4));
action.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
PopupMenu popup = new PopupMenu(getActivity(), action);
popup.inflate(R.menu.attachment);
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.save_attachment:
saveAttachment(attachment);
return true;
default:
return false;
}
}
});
popup.show();
}
});
final Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(AttachmentProvider.getUriForAttachment(mFolderName, mMessageId, partIndex));
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
a.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(i);
}
});
attachments.addView(a);
}
}
// Prepare fields for replying
mIsAnonymous = email.isAnonymous();
} catch (MessagingException e) {
// TODO Handle
e.printStackTrace();
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (BoteHelper.isOutbox(mFolderName)) {
((TextView) v.findViewById(R.id.email_status)).setText(
BoteHelper.getEmailStatusText(getActivity(), email, true));
v.findViewById(R.id.email_status_row).setVisibility(View.VISIBLE);
}
}
private void saveAttachment(Attachment attachment) {
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
String fileName = attachment.getFileName();
int extInd = fileName.lastIndexOf('.');
String name = fileName.substring(0, extInd);
String ext = fileName.substring(extInd);
File outFile = new File(downloadsDir, fileName);
for (int i = 1; outFile.exists() && i < 32; i++) {
fileName = name + "-" + i + ext;
outFile = new File(downloadsDir, fileName);
}
if (outFile.exists()) {
Toast.makeText(getActivity(), R.string.file_exists_in_downloads, Toast.LENGTH_SHORT).show();
return;
}
FileOutputStream out = null;
try {
out = new FileOutputStream(outFile);
attachment.getDataHandler().writeTo(out);
Toast.makeText(getActivity(),
getResources().getString(R.string.saved_to_downloads, fileName),
Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.could_not_save_to_downloads, Toast.LENGTH_SHORT).show();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
}
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.view_email, menu);
MenuItem reply = menu.findItem(R.id.action_reply);
reply.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_reply));
menu.findItem(R.id.action_reply_all).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_reply_all));
menu.findItem(R.id.action_forward).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_forward));
if (mIsAnonymous)
reply.setVisible(false);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reply:
case R.id.action_reply_all:
case R.id.action_forward:
Intent nei = new Intent(getActivity(), NewEmailActivity.class);
nei.putExtra(NewEmailFragment.QUOTE_MSG_FOLDER, mFolderName);
nei.putExtra(NewEmailFragment.QUOTE_MSG_ID, mMessageId);
NewEmailFragment.QuoteMsgType type = null;
switch (item.getItemId()) {
case R.id.action_reply:
type = NewEmailFragment.QuoteMsgType.REPLY;
break;
case R.id.action_reply_all:
type = NewEmailFragment.QuoteMsgType.REPLY_ALL;
break;
case R.id.action_forward:
type = NewEmailFragment.QuoteMsgType.FORWARD;
}
nei.putExtra(NewEmailFragment.QUOTE_MSG_TYPE, type);
startActivity(nei);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View File

@ -0,0 +1,73 @@
package i2p.bote.android.addressbook;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
import i2p.bote.packet.dht.Contact;
public class AddressBookActivity extends BoteActivityBase implements
AddressBookFragment.OnContactSelectedListener {
static final int ALTER_CONTACT_LIST = 1;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
AddressBookFragment f = new AddressBookFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f).commit();
}
}
@Override
public void onContactSelected(Contact contact) {
if (Intent.ACTION_PICK.equals(getIntent().getAction())) {
Intent result = new Intent();
result.putExtra(ViewContactFragment.ADDRESS, contact.getBase64Dest());
setResult(Activity.RESULT_OK, result);
finish();
} else {
Intent i = new Intent(this, ViewContactActivity.class);
i.putExtra(ViewContactFragment.ADDRESS, contact.getBase64Dest());
startActivityForResult(i, ALTER_CONTACT_LIST);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (scanResult != null) {
String content = scanResult.getContents();
if (content != null && content.startsWith(Constants.EMAILDEST_SCHEME)) {
String destination = content.substring(Constants.EMAILDEST_SCHEME.length() + 1);
Intent nci = new Intent(this, EditContactActivity.class);
nci.putExtra(EditContactFragment.NEW_DESTINATION, destination);
startActivityForResult(nci, ALTER_CONTACT_LIST);
}
} else if (requestCode == ALTER_CONTACT_LIST) {
if (resultCode == Activity.RESULT_OK) {
AddressBookFragment f = (AddressBookFragment) getSupportFragmentManager().findFragmentById(R.id.container);
f.updateContactList();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@ -0,0 +1,219 @@
package i2p.bote.android.addressbook;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import com.google.zxing.integration.android.IntentIntegrator;
import com.pnikosis.materialishprogress.ProgressWheel;
import java.util.SortedSet;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.AuthenticatedFragment;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.widget.DividerItemDecoration;
import i2p.bote.android.widget.LoadingRecyclerView;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class AddressBookFragment extends AuthenticatedFragment implements
LoaderManager.LoaderCallbacks<SortedSet<Contact>> {
OnContactSelectedListener mCallback;
private LoadingRecyclerView mContactsList;
private ContactAdapter mAdapter;
private View mPromotedActions;
// Container Activity must implement this interface
public interface OnContactSelectedListener {
public void onContactSelected(Contact contact);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnContactSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnContactSelectedListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Only so we can show/hide the FAM
setHasOptionsMenu(true);
}
@Override
public View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_list_contacts, container, false);
mContactsList = (LoadingRecyclerView) v.findViewById(R.id.contacts_list);
View empty = v.findViewById(R.id.empty);
ProgressWheel loading = (ProgressWheel) v.findViewById(R.id.loading);
mContactsList.setLoadingView(empty, loading);
mPromotedActions = v.findViewById(R.id.promoted_actions);
ImageButton b = (ImageButton) v.findViewById(R.id.action_new_contact);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startNewContact();
}
});
b = (ImageButton) v.findViewById(R.id.action_scan_qr_code);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startScanQrCode();
}
});
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mContactsList.setHasFixedSize(true);
mContactsList.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
// Use a linear layout manager
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mContactsList.setLayoutManager(mLayoutManager);
// Set the adapter for the list view
mAdapter = new ContactAdapter(getActivity(), mCallback);
mContactsList.setAdapter(mAdapter);
}
/**
* Start loading the address book.
* Only called when we have a password cached, or no
* password is required.
*/
protected void onInitializeFragment() {
getLoaderManager().initLoader(0, null, this);
}
protected void onDestroyFragment() {
getLoaderManager().destroyLoader(0);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.address_book, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean passwordRequired = I2PBote.getInstance().isPasswordRequired();
menu.findItem(R.id.export_address_book).setVisible(!passwordRequired);
menu.findItem(R.id.import_address_book).setVisible(!passwordRequired);
mPromotedActions.setVisibility(passwordRequired ? View.GONE : View.VISIBLE);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.export_address_book:
Intent ei = new Intent(getActivity(), AddressBookShipActivity.class);
ei.putExtra(AddressBookShipActivity.EXPORTING, true);
startActivity(ei);
return true;
case R.id.import_address_book:
Intent ii = new Intent(getActivity(), AddressBookShipActivity.class);
ii.putExtra(AddressBookShipActivity.EXPORTING, false);
startActivity(ii);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void startNewContact() {
Intent nci = new Intent(getActivity(), EditContactActivity.class);
getActivity().startActivityForResult(nci, AddressBookActivity.ALTER_CONTACT_LIST);
}
private void startScanQrCode() {
IntentIntegrator integrator = new IntentIntegrator(getActivity());
integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES);
}
protected void updateContactList() {
getLoaderManager().restartLoader(0, null, this);
}
// LoaderManager.LoaderCallbacks<SortedSet<Contact>>
public Loader<SortedSet<Contact>> onCreateLoader(int id, Bundle args) {
return new AddressBookLoader(getActivity());
}
private static class AddressBookLoader extends BetterAsyncTaskLoader<SortedSet<Contact>> {
public AddressBookLoader(Context context) {
super(context);
}
@Override
public SortedSet<Contact> loadInBackground() {
SortedSet<Contact> contacts = null;
try {
contacts = I2PBote.getInstance().getAddressBook().getAll();
} catch (PasswordException e) {
// TODO handle, but should not get here
e.printStackTrace();
}
return contacts;
}
@Override
protected void onStartMonitoring() {
}
@Override
protected void onStopMonitoring() {
}
@Override
protected void releaseResources(SortedSet<Contact> data) {
}
}
@Override
public void onLoadFinished(Loader<SortedSet<Contact>> loader,
SortedSet<Contact> data) {
mAdapter.setContacts(data);
}
@Override
public void onLoaderReset(Loader<SortedSet<Contact>> loader) {
mAdapter.setContacts(null);
}
}

View File

@ -0,0 +1,26 @@
package i2p.bote.android.addressbook;
import android.widget.Toast;
import i2p.bote.android.R;
import i2p.bote.android.util.DataShipActivity;
import i2p.bote.android.util.DataShipFragment;
public class AddressBookShipActivity extends DataShipActivity {
@Override
protected DataShipFragment getDataShipFragment() {
return AddressBookShipFragment.newInstance(mExporting);
}
// DataShipFragment.Callbacks
public void onTaskFinished() {
Toast.makeText(this,
mExporting ?
R.string.address_book_exported:
R.string.address_book_imported,
Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
}

View File

@ -0,0 +1,95 @@
package i2p.bote.android.addressbook;
import android.os.Bundle;
import android.view.View;
import java.io.File;
import java.io.FileDescriptor;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.DataShipFragment;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.fileencryption.PasswordException;
public abstract class AddressBookShipFragment extends DataShipFragment {
public static DataShipFragment newInstance(boolean exporting) {
return exporting ?
new ExportAddressBookFragment() :
new ImportAddressBookFragment();
}
public static class ExportAddressBookFragment extends ExportDataFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mExportFilename.setText("addressBook");
}
@Override
protected RobustAsyncTask<Object, String, String> getExportWaiter() {
return new ExportWaiter();
}
@Override
protected int getTitle() {
return R.string.export_address_book;
}
private class ExportWaiter extends RobustAsyncTask<Object, String, String> {
@Override
protected String doInBackground(Object... params) {
try {
publishProgress(getResources().getString(R.string.exporting_address_book));
I2PBote.getInstance().getAddressBook().export(
(File) params[0],
(String) params[1]);
return null;
} catch (Throwable e) {
cancel(false);
return e.getMessage();
}
}
}
}
public static class ImportAddressBookFragment extends ImportDataFragment {
@Override
protected RobustAsyncTask<Object, String, String> getImportWaiter() {
return new ImportWaiter();
}
@Override
protected int getTitle() {
return R.string.import_address_book;
}
private class ImportWaiter extends RobustAsyncTask<Object, String, String> {
@Override
protected String doInBackground(Object... params) {
try {
publishProgress(getResources().getString(R.string.importing_address_book));
boolean success = I2PBote.getInstance().getAddressBook().importFromFileDescriptor(
(FileDescriptor) params[0],
(String) params[1],
(Boolean) params[2],
(Boolean) params[3]);
if (success)
return null;
else {
cancel(false);
return (params[1] == null) ?
getResources().getString(R.string.no_contacts_found_maybe_encrypted) :
getResources().getString(R.string.no_contacts_found);
}
} catch (Throwable e) {
e.printStackTrace();
cancel(false);
if (e instanceof PasswordException)
return getResources().getString(R.string.password_incorrect);
return e.getLocalizedMessage();
}
}
}
}
}

View File

@ -0,0 +1,131 @@
package i2p.bote.android.addressbook;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.packet.dht.Contact;
public class ContactAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mCtx;
private List<Contact> mContacts;
private AddressBookFragment.OnContactSelectedListener mListener;
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(View itemView) {
super(itemView);
}
}
public static class ContactViewHolder extends RecyclerView.ViewHolder {
public ImageView mPicture;
public TextView mName;
public ContactViewHolder(View itemView) {
super(itemView);
mPicture = (ImageView) itemView.findViewById(R.id.contact_picture);
mName = (TextView) itemView.findViewById(R.id.contact_name);
}
}
public ContactAdapter(Context context, AddressBookFragment.OnContactSelectedListener listener) {
mCtx = context;
mListener = listener;
setHasStableIds(true);
}
public void setContacts(SortedSet<Contact> contacts) {
if (contacts != null) {
mContacts = new ArrayList<Contact>();
mContacts.addAll(contacts);
} else
mContacts = null;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if (mContacts == null || mContacts.isEmpty())
return R.layout.listitem_empty;
return R.layout.listitem_contact;
}
// Create new views (invoked by the layout manager)
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
switch (viewType) {
case R.layout.listitem_contact:
return new ContactViewHolder(v);
default:
return new SimpleViewHolder(v);
}
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case R.layout.listitem_empty:
((TextView) holder.itemView).setText(
mCtx.getResources().getString(R.string.address_book_empty));
break;
case R.layout.listitem_contact:
final ContactViewHolder cvh = (ContactViewHolder) holder;
Contact contact = mContacts.get(position);
String pic = contact.getPictureBase64();
if (pic != null && !pic.isEmpty())
cvh.mPicture.setImageBitmap(BoteHelper.decodePicture(pic));
else {
ViewGroup.LayoutParams lp = cvh.mPicture.getLayoutParams();
cvh.mPicture.setImageBitmap(BoteHelper.getIdenticonForAddress(contact.getBase64Dest(), lp.width, lp.height));
}
cvh.mName.setText(contact.getName());
cvh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mListener.onContactSelected(mContacts.get(cvh.getAdapterPosition()));
}
});
break;
default:
break;
}
}
// Return the size of the dataset (invoked by the layout manager)
@Override
public int getItemCount() {
if (mContacts == null || mContacts.isEmpty())
return 1;
return mContacts.size();
}
public long getItemId(int position) {
if (mContacts == null || mContacts.isEmpty())
return 0;
Contact contact = mContacts.get(position);
return contact.getDestination().getHash().hashCode();
}
}

View File

@ -0,0 +1,93 @@
package i2p.bote.android.addressbook;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.Toolbar;
import java.util.Arrays;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
public class EditContactActivity extends BoteActivityBase {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
EditContactFragment f;
Bundle args = getIntent().getExtras();
if (args != null) {
String destination = args.getString(EditContactFragment.NEW_DESTINATION);
if (destination != null) {
String name = args.getString(EditContactFragment.NEW_NAME);
f = EditContactFragment.newInstance(name, destination);
} else {
destination = args.getString(EditContactFragment.CONTACT_DESTINATION);
f = EditContactFragment.newInstance(destination);
}
if (destination != null)
getSupportActionBar().setDisplayShowTitleEnabled(false);
} else
f = EditContactFragment.newInstance(null);
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f).commit();
}
}
@Override
public void onResume() {
super.onResume();
// Check to see that the Activity started due to an Android Beam
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction()) ||
NfcAdapter.ACTION_TAG_DISCOVERED.equals(getIntent().getAction())) {
processIntent(getIntent());
}
}
@Override
public void onNewIntent(Intent intent) {
// onResume gets called after this to handle the intent
setIntent(intent);
}
/**
* Parses the NDEF Message from the intent
*/
private void processIntent(Intent intent) {
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMsgs == null || rawMsgs.length < 1)
return; // TODO notify user?
NdefMessage msg = (NdefMessage) rawMsgs[0];
NdefRecord[] records = msg.getRecords();
if (records.length != 2 ||
records[0].getTnf() != NdefRecord.TNF_EXTERNAL_TYPE ||
!Arrays.equals(records[0].getType(), Constants.NDEF_LEGACY_TYPE_CONTACT.getBytes()) ||
records[1].getTnf() != NdefRecord.TNF_EXTERNAL_TYPE ||
!Arrays.equals(records[1].getType(), Constants.NDEF_LEGACY_TYPE_CONTACT_DESTINATION.getBytes()))
return; // TODO notify user?
String name = new String(records[0].getPayload());
String destination = new String(records[1].getPayload());
EditContactFragment f = EditContactFragment.newInstance(
name, destination);
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, f).commit();
}
}

View File

@ -0,0 +1,237 @@
package i2p.bote.android.addressbook;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.EditPictureFragment;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class EditContactFragment extends EditPictureFragment {
public static final String CONTACT_DESTINATION = "contact_destination";
public static final String NEW_NAME = "new_name";
public static final String NEW_DESTINATION = "new_destination";
static final int REQUEST_DESTINATION_FILE = 3;
EditText mNameField;
EditText mDestinationField;
EditText mTextField;
TextView mError;
private String mDestination;
public static EditContactFragment newInstance(String destination) {
EditContactFragment f = new EditContactFragment();
Bundle args = new Bundle();
args.putString(CONTACT_DESTINATION, destination);
f.setArguments(args);
return f;
}
public static EditContactFragment newInstance(String name, String destination) {
EditContactFragment f = new EditContactFragment();
Bundle args = new Bundle();
args.putString(NEW_NAME, name);
args.putString(NEW_DESTINATION, destination);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mDestination = getArguments().getString(CONTACT_DESTINATION);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_edit_contact, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mNameField = (EditText) view.findViewById(R.id.contact_name);
mDestinationField = (EditText) view.findViewById(R.id.destination);
mTextField = (EditText) view.findViewById(R.id.text);
mError = (TextView) view.findViewById(R.id.error);
Button b = (Button) view.findViewById(R.id.import_destination_from_file);
if (mDestination != null) {
mDestinationField.setVisibility(View.GONE);
b.setVisibility(View.GONE);
} else
b.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.setType("text/plain");
i.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(
Intent.createChooser(i,
getResources().getString(R.string.select_email_destination_file)),
REQUEST_DESTINATION_FILE);
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(getActivity(), R.string.please_install_a_file_manager,
Toast.LENGTH_SHORT).show();
}
}
});
if (I2PBote.getInstance().isPasswordRequired()) {
// Request a password from the user.
BoteHelper.requestPassword(getActivity(), new BoteHelper.RequestPasswordListener() {
@Override
public void onPasswordVerified() {
initializeContact();
}
@Override
public void onPasswordCanceled() {
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
});
} else {
// Password is cached, or not set.
initializeContact();
}
}
private void initializeContact() {
String newDest = getArguments().getString(NEW_DESTINATION);
if (mDestination != null) {
try {
Contact contact = BoteHelper.getContact(mDestination);
String pic = contact.getPictureBase64();
if (pic != null && !pic.isEmpty()) {
setPictureB64(pic);
}
mNameField.setText(contact.getName());
mTextField.setText(contact.getText());
} catch (PasswordException e) {
// TODO Handle
e.printStackTrace();
}
} else if (newDest != null) {
mNameField.setText(getArguments().getString(NEW_NAME));
mDestinationField.setText(newDest);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.edit_contact, menu);
menu.findItem(R.id.action_save_contact).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_save));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_save_contact:
String picture = getPictureB64();
String name = mNameField.getText().toString();
String destination = mDestination == null ?
mDestinationField.getText().toString() : mDestination;
String text = mTextField.getText().toString();
// Check fields
if (destination.isEmpty()) {
mDestinationField.setError(getActivity().getString(R.string.this_field_is_required));
mDestinationField.requestFocus();
return true;
} else {
mDestinationField.setError(null);
}
mError.setText("");
try {
String err = BoteHelper.saveContact(destination, name, picture, text);
if (err == null) {
if (mDestination == null) // Only set if adding new contact
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
} else {
if (err.startsWith("No Email Destination found in string:") ||
err.startsWith("Not a valid Email Destination:")) {
mDestinationField.setError(getActivity().getString(R.string.not_a_valid_bote_address));
mDestinationField.requestFocus();
} else {
mError.setText(err);
}
}
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
mError.setText(e.getLocalizedMessage());
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
mError.setText(e.getLocalizedMessage());
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_DESTINATION_FILE) {
if (resultCode == Activity.RESULT_OK) {
Uri result = data.getData();
BufferedReader br;
try {
ParcelFileDescriptor pfd = getActivity().getContentResolver().openFileDescriptor(result, "r");
br = new BufferedReader(
new InputStreamReader(
new FileInputStream(pfd.getFileDescriptor()))
);
try {
mDestinationField.setText(br.readLine());
} catch (IOException ioe) {
Toast.makeText(getActivity(), R.string.failed_to_read_email_destination_file,
Toast.LENGTH_SHORT).show();
}
} catch (FileNotFoundException fnfe) {
Toast.makeText(getActivity(), R.string.could_not_find_email_destination_file,
Toast.LENGTH_SHORT).show();
}
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@ -0,0 +1,78 @@
package i2p.bote.android.addressbook;
import android.annotation.SuppressLint;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import i2p.bote.android.BoteActivityBase;
public class ViewContactActivity extends BoteActivityBase {
NfcAdapter mNfcAdapter;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
String destination = null;
Bundle args = getIntent().getExtras();
if (args != null)
destination = args.getString(ViewContactFragment.ADDRESS);
if (destination == null) {
setResult(RESULT_CANCELED);
finish();
return;
}
ViewContactFragment f = ViewContactFragment.newInstance(destination);
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, f).commit();
}
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
@Override
public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
return getNdefMessage();
}
}, this);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public void onResume() {
super.onResume();
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.enableForegroundNdefPush(this, getNdefMessage());
}
}
private NdefMessage getNdefMessage() {
ViewContactFragment f = (ViewContactFragment) getSupportFragmentManager()
.findFragmentById(android.R.id.content);
return f.createNdefMessage();
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public void onPause() {
super.onPause();
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.disableForegroundNdefPush(this);
}
}
}

View File

@ -0,0 +1,101 @@
package i2p.bote.android.addressbook;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import java.security.GeneralSecurityException;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ViewAddressFragment;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class ViewContactFragment extends ViewAddressFragment {
private Contact mContact;
public static ViewContactFragment newInstance(String destination) {
ViewContactFragment f = new ViewContactFragment();
Bundle args = new Bundle();
args.putString(ADDRESS, destination);
f.setArguments(args);
return f;
}
@Override
protected void loadAddress() {
try {
mContact = BoteHelper.getContact(mAddress);
if (mContact == null) {
// No contact found, finish
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
} catch (PasswordException e) {
// TODO Handle
e.printStackTrace();
}
}
@Override
protected String getPublicName() {
return mContact.getName();
}
@Override
protected int getDeleteAddressMessage() {
return R.string.delete_contact;
}
@Override
public void onResume() {
super.onResume();
Bitmap picture = BoteHelper.decodePicture(mContact.getPictureBase64());
if (picture != null)
mPicture.setImageBitmap(picture);
else {
ViewGroup.LayoutParams lp = mPicture.getLayoutParams();
mPicture.setImageBitmap(BoteHelper.getIdenticonForAddress(mAddress, lp.width, lp.height));
}
mPublicName.setText(mContact.getName());
if (mContact.getText().isEmpty())
mDescription.setVisibility(View.GONE);
else {
mDescription.setText(mContact.getText());
mDescription.setVisibility(View.VISIBLE);
}
mCryptoImplName.setText(mContact.getDestination().getCryptoImpl().getName());
}
@Override
protected void onEditAddress() {
Intent ei = new Intent(getActivity(), EditContactActivity.class);
ei.putExtra(EditContactFragment.CONTACT_DESTINATION, mAddress);
startActivity(ei);
}
@Override
public void onDeleteAddress() {
try {
String err = BoteHelper.deleteContact(mAddress);
if (err == null) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
} else
Toast.makeText(getActivity(), err, Toast.LENGTH_SHORT).show();
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,128 @@
package i2p.bote.android.config;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.EditTextPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceFragmentCompat;
import i2p.bote.android.R;
import i2p.bote.android.config.util.SummaryEditTextPreference;
public class AdvancedPreferenceFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings_advanced);
setupAdvancedSettings();
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label_advanced);
}
private void setupAdvancedSettings() {
final Context ctx = getPreferenceManager().getContext();
final PreferenceCategory i2pCat = (PreferenceCategory) findPreference("i2pCategory");
CheckBoxPreference routerAuto = (CheckBoxPreference) findPreference("i2pbote.router.auto");
if (!routerAuto.isChecked()) {
setupI2PCategory(ctx, i2pCat);
}
routerAuto.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Boolean checked = (Boolean) newValue;
if (!checked) {
setupI2PCategory(ctx, i2pCat);
} else {
Preference p1 = i2pCat.findPreference("i2pbote.router.use");
Preference p2 = i2pCat.findPreference("i2pbote.i2cp.tcp.host");
Preference p3 = i2pCat.findPreference("i2pbote.i2cp.tcp.port");
if (p1 != null)
i2pCat.removePreference(p1);
if (p2 != null)
i2pCat.removePreference(p2);
if (p3 != null)
i2pCat.removePreference(p3);
}
return true;
}
});
}
private static void setupI2PCategory(Context context, PreferenceCategory i2pCat) {
final ListPreference routerChoice = createRouterChoice(context);
final EditTextPreference hostField = createHostField(context);
final EditTextPreference portField = createPortField(context);
i2pCat.addPreference(routerChoice);
i2pCat.addPreference(hostField);
i2pCat.addPreference(portField);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
routerChoice.setSummary(routerChoice.getEntry());
if ("remote".equals(routerChoice.getValue())) {
hostField.setEnabled(true);
portField.setEnabled(true);
}
routerChoice.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
final String val = newValue.toString();
int index = routerChoice.findIndexOfValue(val);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
routerChoice.setSummary(routerChoice.getEntries()[index]);
if (index == 2) {
hostField.setEnabled(true);
hostField.setText("127.0.0.1");
portField.setEnabled(true);
portField.setText("7654");
} else {
hostField.setEnabled(false);
hostField.setText("internal");
portField.setEnabled(false);
portField.setText("internal");
}
return true;
}
});
}
private static ListPreference createRouterChoice(Context context) {
ListPreference routerChoice = new ListPreference(context);
routerChoice.setKey("i2pbote.router.use");
routerChoice.setEntries(R.array.routerOptionNames);
routerChoice.setEntryValues(R.array.routerOptions);
routerChoice.setTitle(R.string.pref_title_router);
routerChoice.setSummary("%s");
routerChoice.setDialogTitle(R.string.pref_dialog_title_router);
routerChoice.setDefaultValue("internal");
return routerChoice;
}
private static EditTextPreference createHostField(Context context) {
EditTextPreference p = new SummaryEditTextPreference(context);
p.setKey("i2pbote.i2cp.tcp.host");
p.setTitle(R.string.pref_title_i2cp_host);
p.setSummary("%s");
p.setDefaultValue("internal");
p.setEnabled(false);
return p;
}
private static EditTextPreference createPortField(Context context) {
EditTextPreference p = new SummaryEditTextPreference(context);
p.setKey("i2pbote.i2cp.tcp.port");
p.setTitle(R.string.pref_title_i2cp_port);
p.setSummary("%s");
p.setDefaultValue("internal");
p.setEnabled(false);
return p;
}
}

View File

@ -0,0 +1,40 @@
package i2p.bote.android.config;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import i2p.bote.android.R;
public class AppProtectionPreferenceFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings_app_protection);
setupAppProtectionSettings();
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label_app_protection);
// Screen security only works from API 14
Preference screenSecurityPreference = findPreference("pref_screen_security");
if (screenSecurityPreference != null &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
getPreferenceScreen().removePreference(screenSecurityPreference);
}
private void setupAppProtectionSettings() {
findPreference("pref_change_password").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(new Intent(getActivity(), SetPasswordActivity.class));
return true;
}
});
}
}

View File

@ -0,0 +1,35 @@
package i2p.bote.android.config;
import android.os.Bundle;
import android.support.v7.preference.PreferenceFragmentCompat;
import i2p.bote.android.R;
public class AppearancePreferenceFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings_appearance);
}
@Override
public void onStart() {
super.onStart();
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(
(SettingsActivity) getActivity()
);
}
@Override
public void onResume() {
super.onResume();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label_appearance);
}
@Override
public void onStop() {
super.onStop();
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
(SettingsActivity) getActivity()
);
}
}

View File

@ -0,0 +1,47 @@
package i2p.bote.android.config;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import java.util.Map;
import i2p.bote.Configuration;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.config.util.CustomPreferenceFragment;
public class NetworkPreferenceFragment extends CustomPreferenceFragment {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings_network);
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label_network);
}
@Override
public void onPause() {
Configuration config = I2PBote.getInstance().getConfiguration();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
Map<String, ?> all = prefs.getAll();
for (String x : all.keySet()) {
if ("autoMailCheckEnabled".equals(x))
config.setAutoMailCheckEnabled(prefs.getBoolean(x, true));
else if ("mailCheckInterval".equals(x))
config.setMailCheckInterval(prefs.getInt(x, 30));
else if ("deliveryCheckEnabled".equals(x))
config.setDeliveryCheckEnabled(prefs.getBoolean(x, true));
}
config.save();
// Store the settings in Android
super.onPause();
}
}

View File

@ -0,0 +1,70 @@
package i2p.bote.android.config;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import java.util.Map;
import i2p.bote.Configuration;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.config.util.CustomPreferenceFragment;
public class PrivacyPreferenceFragment extends CustomPreferenceFragment {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings_privacy);
setupPrivacySettings();
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.pref_title_privacy);
}
@Override
public void onPause() {
Configuration config = I2PBote.getInstance().getConfiguration();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
Map<String, ?> all = prefs.getAll();
for (String x : all.keySet()) {
if ("hideLocale".equals(x))
config.setHideLocale(prefs.getBoolean(x, true));
else if ("includeSentTime".equals(x))
config.setIncludeSentTime(prefs.getBoolean(x, true));
else if ("numSendHops".equals(x))
config.setNumStoreHops(Integer.parseInt(prefs.getString(x, "0")));
else if ("relayMinDelay".equals(x))
config.setRelayMinDelay(prefs.getInt(x, 5));
else if ("relayMaxDelay".equals(x))
config.setRelayMaxDelay(prefs.getInt(x, 40));
}
config.save();
// Store the settings in Android
super.onPause();
}
private void setupPrivacySettings() {
ListPreference numSendHops = (ListPreference) findPreference("numSendHops");
int value = Integer.valueOf(numSendHops.getValue());
numSendHops.setSummary(getResources().getQuantityString(R.plurals.pref_summ_numHops,
value, value));
numSendHops.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int value = Integer.valueOf((String) newValue);
preference.setSummary(getResources().getQuantityString(R.plurals.pref_summ_numHops,
value, value));
return true;
}
});
}
}

View File

@ -0,0 +1,34 @@
package i2p.bote.android.config;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.widget.Toast;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
public class SetPasswordActivity extends BoteActivityBase implements
SetPasswordFragment.Callbacks {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_set_password);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
// SetPasswordFragment.Callbacks
public void onTaskFinished() {
Toast.makeText(this, R.string.password_changed,
Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
}

View File

@ -0,0 +1,266 @@
package i2p.bote.android.config;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import i2p.bote.I2PBote;
import i2p.bote.fileencryption.PasswordIncorrectException;
import i2p.bote.fileencryption.PasswordMismatchException;
import i2p.bote.status.StatusListener;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.android.util.TaskFragment;
import i2p.bote.status.ChangePasswordStatus;
public class SetPasswordFragment extends Fragment {
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks {
public void onTaskFinished();
}
private static Callbacks sDummyCallbacks = new Callbacks() {
public void onTaskFinished() {}
};
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof Callbacks))
throw new IllegalStateException("Activity must implement fragment's callbacks.");
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
mCallbacks = sDummyCallbacks;
}
// Code to identify the fragment that is calling onActivityResult().
static final int PASSWORD_WAITER = 0;
// Tag so we can find the task fragment again, in another
// instance of this fragment after rotation.
static final String PASSWORD_WAITER_TAG = "passwordWaiterTask";
MenuItem mSave;
EditText mOldField;
EditText mNewField;
EditText mConfirmField;
TextView mError;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PasswordWaiterFrag f = (PasswordWaiterFrag) getFragmentManager().findFragmentByTag(PASSWORD_WAITER_TAG);
if (f != null)
f.setTargetFragment(this, PASSWORD_WAITER);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_set_password, container);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mOldField = (EditText) view.findViewById(R.id.password_old);
mNewField = (EditText) view.findViewById(R.id.password_new);
mConfirmField = (EditText) view.findViewById(R.id.password_confirm);
mError = (TextView) view.findViewById(R.id.error);
if (!I2PBote.getInstance().getConfiguration().getPasswordFile().exists()) {
mOldField.setVisibility(View.GONE);
view.findViewById(R.id.msg_remove_password).setVisibility(View.GONE);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.set_password, menu);
mSave = menu.findItem(R.id.action_set_password);
mSave.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_save));
// If task is running, disable the save button.
PasswordWaiterFrag f = (PasswordWaiterFrag) getFragmentManager().findFragmentByTag(PASSWORD_WAITER_TAG);
if (f != null)
setInterfaceEnabled(false);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_set_password:
String oldPassword = mOldField.getText().toString();
String newPassword = mNewField.getText().toString();
String confirmNewPassword = mConfirmField.getText().toString();
InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mNewField.getWindowToken(), 0);
setInterfaceEnabled(false);
mError.setText("");
PasswordWaiterFrag f = PasswordWaiterFrag.newInstance(oldPassword, newPassword, confirmNewPassword);
f.setTask(new PasswordWaiter());
f.setTargetFragment(SetPasswordFragment.this, PASSWORD_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.password_waiter_frag, f, PASSWORD_WAITER_TAG)
.commit();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PASSWORD_WAITER) {
if (resultCode == Activity.RESULT_OK) {
mCallbacks.onTaskFinished();
} else if (resultCode == Activity.RESULT_CANCELED) {
setInterfaceEnabled(true);
mError.setText(data.getStringExtra("error"));
}
}
}
private void setInterfaceEnabled(boolean enabled) {
mSave.setVisible(enabled);
mOldField.setEnabled(enabled);
mNewField.setEnabled(enabled);
mConfirmField.setEnabled(enabled);
}
public static class PasswordWaiterFrag extends TaskFragment<String, String, Throwable> {
String currentStatus;
TextView mStatus;
public static PasswordWaiterFrag newInstance(String... params) {
PasswordWaiterFrag f = new PasswordWaiterFrag();
Bundle args = new Bundle();
args.putStringArray("params", params);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.dialog_status, container, false);
mStatus = (TextView) v.findViewById(R.id.status);
if (currentStatus != null && !currentStatus.isEmpty())
mStatus.setText(currentStatus);
return v;
}
@Override
public String[] getParams() {
Bundle args = getArguments();
return args.getStringArray("params");
}
@Override
public void updateProgress(String... values) {
ChangePasswordStatus status = ChangePasswordStatus.valueOf(values[0]);
switch (status) {
case CHECKING_PASSWORD:
currentStatus = getString(R.string.checking_password);
break;
case RE_ENCRYPTING_IDENTITIES:
currentStatus = getString(R.string.re_encrypting_identities);
break;
case RE_ENCRYPTING_ADDRESS_BOOK:
currentStatus = getString(R.string.re_encrypting_address_book);
break;
case RE_ENCRYPTING_FOLDER:
currentStatus = getString(R.string.re_encrypting_folder, values[1]);
break;
case UPDATING_PASSWORD_FILE:
currentStatus = getString(R.string.updating_password_file);
break;
}
mStatus.setText(currentStatus);
}
@Override
public void taskFinished(Throwable ignored) {
super.taskFinished(ignored);
if (getTargetFragment() != null) {
Intent i = new Intent();
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_OK, i);
}
}
@Override
public void taskCancelled(Throwable error) {
super.taskCancelled(error);
if (getTargetFragment() != null) {
Intent i = new Intent();
if (error instanceof PasswordIncorrectException) {
i.putExtra("error", getString(R.string.old_password_incorrect));
} else if (error instanceof PasswordMismatchException) {
i.putExtra("error", getString(R.string.new_password_mismatch));
} else {
i.putExtra("error", error.getLocalizedMessage());
}
System.out.println("Password error: " + error.toString());
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_CANCELED, i);
}
}
}
private class PasswordWaiter extends RobustAsyncTask<String, String, Throwable> {
protected Throwable doInBackground(String... params) {
StatusListener<ChangePasswordStatus> lsnr = new StatusListener<ChangePasswordStatus>() {
public void updateStatus(ChangePasswordStatus status, String... args) {
ArrayList<String> tmp = new ArrayList<>(Arrays.asList(args));
tmp.add(0, status.name());
publishProgress(tmp.toArray(new String[tmp.size()]));
}
};
try {
I2PBote.getInstance().changePassword(
params[0].getBytes(),
params[1].getBytes(),
params[2].getBytes(),
lsnr);
return null;
} catch (Throwable e) {
cancel(false);
return e;
}
}
}
}

View File

@ -0,0 +1,178 @@
package i2p.bote.android.config;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.widget.Toolbar;
import i2p.bote.I2PBote;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.EmailListActivity;
import i2p.bote.android.R;
import i2p.bote.android.identities.IdentityListActivity;
import i2p.bote.android.service.BoteService;
import i2p.bote.android.util.BoteHelper;
public class SettingsActivity extends BoteActivityBase implements
SharedPreferences.OnSharedPreferenceChangeListener {
public static final String PREFERENCE_CATEGORY = "preference_category";
public static final String PREFERENCE_CATEGORY_NETWORK = "preference_category_network";
public static final String PREFERENCE_CATEGORY_IDENTITIES = "preference_category_identities";
public static final String PREFERENCE_CATEGORY_PRIVACY = "preference_category_privacy";
public static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
public static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
public static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
//
// Android lifecycle
//
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Fragment fragment;
String category = getIntent().getStringExtra(PREFERENCE_CATEGORY);
if (category != null)
fragment = getFragmentForCategory(category);
else
fragment = new SettingsFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment)
.commit();
}
@Override
public boolean onSupportNavigateUp() {
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
} else {
Intent intent = new Intent(this, EmailListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
return true;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals("pref_language")) {
notifyLocaleChanged();
Intent intent = new Intent(BoteService.LOCAL_BROADCAST_LOCALE_CHANGED);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
}
//
// Settings pages
//
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle paramBundle, String s) {
addPreferencesFromResource(R.xml.settings);
findPreference(PREFERENCE_CATEGORY_NETWORK)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NETWORK));
findPreference(PREFERENCE_CATEGORY_IDENTITIES)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_IDENTITIES));
findPreference(PREFERENCE_CATEGORY_PRIVACY)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PRIVACY));
findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
findPreference(PREFERENCE_CATEGORY_ADVANCED)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.action_settings);
}
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
private String category;
public CategoryClickListener(String category) {
this.category = category;
}
@Override
public boolean onPreferenceClick(Preference preference) {
switch (category) {
case PREFERENCE_CATEGORY_IDENTITIES:
Intent ili = new Intent(getActivity(), IdentityListActivity.class);
startActivity(ili);
break;
case PREFERENCE_CATEGORY_PRIVACY:
case PREFERENCE_CATEGORY_APP_PROTECTION:
if (I2PBote.getInstance().isPasswordRequired()) {
BoteHelper.requestPassword(getActivity(), new BoteHelper.RequestPasswordListener() {
@Override
public void onPasswordVerified() {
loadCategory();
}
@Override
public void onPasswordCanceled() {
}
});
} else
loadCategory();
break;
default:
loadCategory();
}
return true;
}
private void loadCategory() {
Fragment fragment = getFragmentForCategory(category);
getActivity().getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment)
.addToBackStack(null)
.commit();
}
}
}
private static Fragment getFragmentForCategory(String category) {
switch (category) {
case PREFERENCE_CATEGORY_NETWORK:
return new NetworkPreferenceFragment();
case PREFERENCE_CATEGORY_PRIVACY:
return new PrivacyPreferenceFragment();
case PREFERENCE_CATEGORY_APP_PROTECTION:
return new AppProtectionPreferenceFragment();
case PREFERENCE_CATEGORY_APPEARANCE:
return new AppearancePreferenceFragment();
case PREFERENCE_CATEGORY_ADVANCED:
return new AdvancedPreferenceFragment();
default:
throw new AssertionError();
}
}
}

View File

@ -0,0 +1,32 @@
package i2p.bote.android.config.util;
import android.support.v4.app.DialogFragment;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
/**
* Handles custom Preferences.
*/
public abstract class CustomPreferenceFragment extends PreferenceFragmentCompat {
private static final String DIALOG_FRAGMENT_TAG =
"android.support.v7.preference.PreferenceFragment.DIALOG";
@Override
public void onDisplayPreferenceDialog(Preference preference) {
// check if dialog is already showing
if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
return;
}
DialogFragment f = null;
if (preference instanceof IntEditTextPreference) {
f = IntEditTextPreferenceDialog.newInstance(preference.getKey());
} else {
super.onDisplayPreferenceDialog(preference);
}
if (f != null) {
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
}
}
}

View File

@ -0,0 +1,51 @@
package i2p.bote.android.config.util;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.v7.preference.Preference;
import android.util.AttributeSet;
import com.mikepenz.iconics.IconicsDrawable;
import i2p.bote.android.R;
public class IconicsPreference extends Preference {
public IconicsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public IconicsPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void init(Context context, AttributeSet attrs) {
// Icons only work on API 11+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
return;
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.IconicsPreference, 0, 0);
String iconName = a.getString(R.styleable.IconicsPreference_ip_icon);
if (iconName == null)
return;
IconicsDrawable icon = new IconicsDrawable(context, iconName);
int color = a.getColor(R.styleable.IconicsPreference_ip_color, 0);
if (color != 0)
icon.color(color);
int size = a.getDimensionPixelSize(R.styleable.IconicsPreference_ip_size, 0);
if (size != 0)
icon.sizePx(size);
int padding = a.getDimensionPixelSize(R.styleable.IconicsPreference_ip_padding, 0);
if (padding != 0)
icon.paddingPx(padding);
a.recycle();
setIcon(icon);
}
}

View File

@ -0,0 +1,35 @@
package i2p.bote.android.config.util;
import android.content.Context;
import android.support.v7.preference.EditTextPreference;
import android.util.AttributeSet;
public class IntEditTextPreference extends EditTextPreference {
public IntEditTextPreference(Context context) {
super(context);
}
public IntEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public CharSequence getSummary() {
return String.format((String) super.getSummary(), getText());
}
@Override
protected String getPersistedString(String defaultReturnValue) {
return String.valueOf(getPersistedInt(-1));
}
@Override
protected boolean persistString(String value) {
return persistInt(Integer.valueOf(value));
}
}

View File

@ -0,0 +1,24 @@
package i2p.bote.android.config.util;
import android.os.Bundle;
import android.support.v7.preference.EditTextPreferenceDialogFragmentCompat;
import android.text.InputType;
import android.view.View;
import android.widget.EditText;
public class IntEditTextPreferenceDialog extends EditTextPreferenceDialogFragmentCompat {
public static IntEditTextPreferenceDialog newInstance(String key) {
final IntEditTextPreferenceDialog fragment = new IntEditTextPreferenceDialog();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
((EditText)view.findViewById(android.R.id.edit)).setInputType(
InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
}

View File

@ -0,0 +1,28 @@
package i2p.bote.android.config.util;
import android.content.Context;
import android.support.v7.preference.EditTextPreference;
import android.util.AttributeSet;
public class SummaryEditTextPreference extends EditTextPreference {
public SummaryEditTextPreference(Context context) {
super(context);
}
public SummaryEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SummaryEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public CharSequence getSummary() {
String summary = (String) super.getSummary();
if (summary == null)
summary = "%s";
return String.format(summary, getText());
}
}

View File

@ -0,0 +1,45 @@
package i2p.bote.android.identities;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.widget.Toast;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
public class EditIdentityActivity extends BoteActivityBase implements
EditIdentityFragment.Callbacks {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_identity);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
String key = null;
Bundle args = getIntent().getExtras();
if (args != null)
key = args.getString(EditIdentityFragment.IDENTITY_KEY);
if (key != null)
getSupportActionBar().setDisplayShowTitleEnabled(false);
EditIdentityFragment f = EditIdentityFragment.newInstance(key);
getSupportFragmentManager().beginTransaction()
.add(R.id.edit_identity_frag, f).commit();
}
}
// EditIdentityFragment.Callbacks
public void onTaskFinished() {
Toast.makeText(this, R.string.identity_saved,
Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
}

View File

@ -0,0 +1,428 @@
package i2p.bote.android.identities;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import i2p.bote.I2PBote;
import i2p.bote.email.IllegalDestinationParametersException;
import i2p.bote.status.StatusListener;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.EditPictureFragment;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.android.util.TaskFragment;
import i2p.bote.crypto.CryptoFactory;
import i2p.bote.crypto.CryptoImplementation;
import i2p.bote.email.EmailIdentity;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.status.ChangeIdentityStatus;
public class EditIdentityFragment extends EditPictureFragment {
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks {
public void onTaskFinished();
}
private static Callbacks sDummyCallbacks = new Callbacks() {
public void onTaskFinished() {}
};
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof Callbacks))
throw new IllegalStateException("Activity must implement fragment's callbacks.");
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
mCallbacks = sDummyCallbacks;
}
public static final String IDENTITY_KEY = "identity_key";
// Code to identify the fragment that is calling onActivityResult().
static final int IDENTITY_WAITER = 3;
// Tag so we can find the task fragment again, in another
// instance of this fragment after rotation.
static final String IDENTITY_WAITER_TAG = "identityWaiterTask";
static final int DEFAULT_CRYPTO_IMPL = 2;
private String mKey;
MenuItem mSave;
EditText mNameField;
EditText mDescField;
Spinner mCryptoField;
int mDefaultPos;
CheckBox mDefaultField;
TextView mError;
public static EditIdentityFragment newInstance(String key) {
EditIdentityFragment f = new EditIdentityFragment();
Bundle args = new Bundle();
args.putString(IDENTITY_KEY, key);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
IdentityWaiterFrag f = (IdentityWaiterFrag) getFragmentManager().findFragmentByTag(IDENTITY_WAITER_TAG);
if (f != null)
f.setTargetFragment(this, IDENTITY_WAITER);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_edit_identity, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mKey = getArguments().getString(IDENTITY_KEY);
mNameField = (EditText) view.findViewById(R.id.public_name);
mDescField = (EditText) view.findViewById(R.id.description);
mDefaultField = (CheckBox) view.findViewById(R.id.default_identity);
mError = (TextView) view.findViewById(R.id.error);
mCryptoField = (Spinner) view.findViewById(R.id.crypto_impl);
if (I2PBote.getInstance().isPasswordRequired()) {
// Request a password from the user.
BoteHelper.requestPassword(getActivity(), new BoteHelper.RequestPasswordListener() {
@Override
public void onPasswordVerified() {
initializeIdentity();
}
@Override
public void onPasswordCanceled() {
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
});
} else {
// Password is cached, or not set.
initializeIdentity();
}
}
private void initializeIdentity() {
if (mKey == null) {
// Show the encryption choice field
CryptoAdapter adapter = new CryptoAdapter(getActivity());
mCryptoField.setAdapter(adapter);
mCryptoField.setSelection(mDefaultPos);
mCryptoField.setVisibility(View.VISIBLE);
// If no identities, set this as default by default
try {
mDefaultField.setChecked(I2PBote.getInstance().getIdentities().size() == 0);
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
} else {
// Load the identity to edit
try {
EmailIdentity identity = BoteHelper.getIdentity(mKey);
String pic = identity.getPictureBase64();
if (pic != null && !pic.isEmpty()) {
setPictureB64(pic);
}
mNameField.setText(identity.getPublicName());
mDescField.setText(identity.getDescription());
mDefaultField.setChecked(identity.isDefaultIdentity());
} catch (PasswordException e) {
// TODO Handle
e.printStackTrace();
} catch (IOException e) {
// TODO Handle
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Handle
e.printStackTrace();
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.edit_identity, menu);
mSave = menu.findItem(R.id.action_save_identity);
mSave.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_save));
IdentityWaiterFrag f = (IdentityWaiterFrag) getFragmentManager().findFragmentByTag(IDENTITY_WAITER_TAG);
if (f != null)
setInterfaceEnabled(false);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_save_identity:
String picture = getPictureB64();
String publicName = mNameField.getText().toString();
String description = mDescField.getText().toString();
boolean setDefault = mDefaultField.isChecked();
int cryptoImplId = -1;
if (mKey == null)
cryptoImplId = ((CryptoImplementation) mCryptoField.getSelectedItem()).getId();
InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mNameField.getWindowToken(), 0);
setInterfaceEnabled(false);
mError.setText("");
IdentityWaiterFrag f = IdentityWaiterFrag.newInstance(
(mKey == null),
cryptoImplId,
null,
mKey,
publicName,
description,
picture,
null,
setDefault);
f.setTask(new IdentityWaiter());
f.setTargetFragment(EditIdentityFragment.this, IDENTITY_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.identity_waiter_frag, f, IDENTITY_WAITER_TAG)
.commit();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == IDENTITY_WAITER) {
if (resultCode == Activity.RESULT_OK) {
mCallbacks.onTaskFinished();
} else if (resultCode == Activity.RESULT_CANCELED) {
setInterfaceEnabled(true);
mError.setText(data.getStringExtra("error"));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void setInterfaceEnabled(boolean enabled) {
mSave.setVisible(enabled);
mNameField.setEnabled(enabled);
mDescField.setEnabled(enabled);
mDefaultField.setEnabled(enabled);
}
private class CryptoAdapter extends ArrayAdapter<CryptoImplementation> {
public CryptoAdapter(Context context) {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
List<CryptoImplementation> instances = CryptoFactory.getInstances();
mDefaultPos = 0;
for (CryptoImplementation instance : instances) {
add(instance);
if (instance.getId() == DEFAULT_CRYPTO_IMPL)
mDefaultPos = getPosition(instance);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
setViewText(v, position);
return v;
}
@Override
public View getDropDownView (int position, View convertView, ViewGroup parent) {
View v = super.getDropDownView(position, convertView, parent);
setViewText(v, position);
return v;
}
private void setViewText(View v, int position) {
TextView text = (TextView) v.findViewById(android.R.id.text1);
text.setText(getItem(position).getName());
}
}
public static class IdentityWaiterFrag extends TaskFragment<Object, String, Throwable> {
static final String CREATE_NEW = "create_new";
static final String CRYPTO_IMPL_ID = "crypto_impl_id";
static final String VANITY_PREFIX = "vanity_prefix";
static final String KEY = "key";
static final String PUBLIC_NAME = "public_name";
static final String DESCRIPTION = "description";
static final String PICTURE_BASE64 = "picture_base64";
static final String EMAIL_ADDRESS = "email_address";
static final String SET_DEFAULT = "set_default";
String currentStatus;
TextView mStatus;
public static IdentityWaiterFrag newInstance(
boolean createNew, int cryptoImplId, String vanity_prefix,
String key, String publicName, String description,
String pictureBase64, String emailAddress, boolean setDefault) {
IdentityWaiterFrag f = new IdentityWaiterFrag();
Bundle args = new Bundle();
args.putBoolean(CREATE_NEW, createNew);
args.putInt(CRYPTO_IMPL_ID, cryptoImplId);
args.putString(VANITY_PREFIX, vanity_prefix);
args.putString(KEY, key);
args.putString(PUBLIC_NAME, publicName);
args.putString(DESCRIPTION, description);
args.putString(PICTURE_BASE64, pictureBase64);
args.putString(EMAIL_ADDRESS, emailAddress);
args.putBoolean(SET_DEFAULT, setDefault);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.dialog_status, container, false);
mStatus = (TextView) v.findViewById(R.id.status);
if (currentStatus != null && !currentStatus.isEmpty())
mStatus.setText(currentStatus);
return v;
}
@Override
public Object[] getParams() {
Bundle args = getArguments();
return new Object[] {
args.getBoolean(CREATE_NEW),
args.getInt(CRYPTO_IMPL_ID),
args.getString(VANITY_PREFIX),
args.getString(KEY),
args.getString(PUBLIC_NAME),
args.getString(DESCRIPTION),
args.getString(PICTURE_BASE64),
args.getString(EMAIL_ADDRESS),
args.getBoolean(SET_DEFAULT),
};
}
@Override
public void updateProgress(String... values) {
ChangeIdentityStatus status = ChangeIdentityStatus.valueOf(values[0]);
switch (status) {
case GENERATING_KEYS:
currentStatus = getString(R.string.generating_keys);
break;
case SAVING_IDENTITY:
currentStatus = getString(R.string.saving_identity);
break;
}
mStatus.setText(currentStatus);
}
@Override
public void taskFinished(Throwable ignored) {
super.taskFinished(ignored);
if (getTargetFragment() != null) {
Intent i = new Intent();
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_OK, i);
}
}
@Override
public void taskCancelled(Throwable error) {
super.taskCancelled(error);
if (getTargetFragment() != null) {
Intent i = new Intent();
if (error instanceof IllegalDestinationParametersException) {
IllegalDestinationParametersException e = (IllegalDestinationParametersException) error;
i.putExtra("error", getString(R.string.invalid_vanity_chars, e.getBadChar(), e.getValidChars()));
} else {
i.putExtra("error", error.getLocalizedMessage());
}
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_CANCELED, i);
}
}
}
private class IdentityWaiter extends RobustAsyncTask<Object, String, Throwable> {
protected Throwable doInBackground(Object... params) {
StatusListener<ChangeIdentityStatus> lsnr = new StatusListener<ChangeIdentityStatus>() {
public void updateStatus(ChangeIdentityStatus status, String... args) {
ArrayList<String> tmp = new ArrayList<>(Arrays.asList(args));
tmp.add(0, status.name());
publishProgress(tmp.toArray(new String[tmp.size()]));
}
};
try {
BoteHelper.createOrModifyIdentity(
(Boolean) params[0],
(Integer) params[1],
(String) params[2],
(String) params[3],
(String) params[4],
(String) params[5],
(String) params[6],
(String) params[7],
new Properties(),
(Boolean) params[8],
lsnr);
lsnr.updateStatus(ChangeIdentityStatus.SAVING_IDENTITY);
I2PBote.getInstance().getIdentities().save();
return null;
} catch (Throwable e) {
cancel(false);
return e;
}
}
}
}

View File

@ -0,0 +1,126 @@
package i2p.bote.android.identities;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.email.EmailIdentity;
public class IdentityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mCtx;
private List<EmailIdentity> mIdentities;
private IdentityListFragment.OnIdentitySelectedListener mListener;
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(View itemView) {
super(itemView);
}
}
public static class IdentityViewHolder extends RecyclerView.ViewHolder {
public ImageView mPicture;
public TextView mName;
public IdentityViewHolder(View itemView) {
super(itemView);
mPicture = (ImageView) itemView.findViewById(R.id.identity_picture);
mName = (TextView) itemView.findViewById(R.id.identity_name);
}
}
public IdentityAdapter(Context context, IdentityListFragment.OnIdentitySelectedListener listener) {
mCtx = context;
mListener = listener;
setHasStableIds(true);
}
public void setIdentities(Collection<EmailIdentity> identities) {
if (identities != null) {
mIdentities = new ArrayList<>();
mIdentities.addAll(identities);
} else
mIdentities = null;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if (mIdentities == null || mIdentities.isEmpty())
return R.layout.listitem_empty;
return R.layout.listitem_identity;
}
// Create new views (invoked by the layout manager)
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
switch (viewType) {
case R.layout.listitem_identity:
return new IdentityViewHolder(v);
default:
return new SimpleViewHolder(v);
}
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case R.layout.listitem_empty:
((TextView) holder.itemView).setText(
mCtx.getResources().getString(R.string.no_identities));
break;
case R.layout.listitem_identity:
final IdentityViewHolder cvh = (IdentityViewHolder) holder;
EmailIdentity identity = mIdentities.get(position);
ViewGroup.LayoutParams lp = cvh.mPicture.getLayoutParams();
cvh.mPicture.setImageBitmap(BoteHelper.getIdentityPicture(identity, lp.width, lp.height));
cvh.mName.setText(identity.getPublicName());
cvh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mListener.onIdentitySelected(mIdentities.get(cvh.getAdapterPosition()));
}
});
break;
default:
break;
}
}
// Return the size of the dataset (invoked by the layout manager)
@Override
public int getItemCount() {
if (mIdentities == null || mIdentities.isEmpty())
return 1;
return mIdentities.size();
}
public long getItemId(int position) {
if (mIdentities == null || mIdentities.isEmpty())
return 0;
EmailIdentity identity = mIdentities.get(position);
return identity.getHash().hashCode();
}
}

View File

@ -0,0 +1,53 @@
package i2p.bote.android.identities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
import i2p.bote.email.EmailIdentity;
public class IdentityListActivity extends BoteActivityBase implements
IdentityListFragment.OnIdentitySelectedListener {
static final int ALTER_IDENTITY_LIST = 1;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
IdentityListFragment f = new IdentityListFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, f).commit();
}
}
@Override
public void onIdentitySelected(EmailIdentity identity) {
Intent i = new Intent(this, ViewIdentityActivity.class);
i.putExtra(ViewIdentityFragment.ADDRESS, identity.getKey());
startActivityForResult(i, ALTER_IDENTITY_LIST);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ALTER_IDENTITY_LIST) {
if (resultCode == Activity.RESULT_OK) {
IdentityListFragment f = (IdentityListFragment) getSupportFragmentManager().findFragmentById(R.id.container);
f.updateIdentityList();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}

View File

@ -0,0 +1,229 @@
package i2p.bote.android.identities;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.pnikosis.materialishprogress.ProgressWheel;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.AuthenticatedFragment;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.widget.DividerItemDecoration;
import i2p.bote.android.widget.LoadingRecyclerView;
import i2p.bote.email.EmailIdentity;
import i2p.bote.email.IdentitiesListener;
import i2p.bote.fileencryption.PasswordException;
public class IdentityListFragment extends AuthenticatedFragment implements
LoaderManager.LoaderCallbacks<Collection<EmailIdentity>> {
OnIdentitySelectedListener mCallback;
private LoadingRecyclerView mIdentitiesList;
private IdentityAdapter mAdapter;
private View mNewIdentity;
// Container Activity must implement this interface
public interface OnIdentitySelectedListener {
void onIdentitySelected(EmailIdentity identity);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnIdentitySelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnIdentitySelectedListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Only so we can show/hide the FAM
setHasOptionsMenu(true);
}
@Override
public View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_list_identities, container, false);
mIdentitiesList = (LoadingRecyclerView) v.findViewById(R.id.identities_list);
View empty = v.findViewById(R.id.empty);
ProgressWheel loading = (ProgressWheel) v.findViewById(R.id.loading);
mIdentitiesList.setLoadingView(empty, loading);
mNewIdentity = v.findViewById(R.id.action_new_identity);
mNewIdentity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startNewIdentity();
}
});
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mIdentitiesList.setHasFixedSize(true);
mIdentitiesList.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST));
// Use a linear layout manager
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mIdentitiesList.setLayoutManager(mLayoutManager);
// Set the adapter for the list view
mAdapter = new IdentityAdapter(getActivity(), mCallback);
mIdentitiesList.setAdapter(mAdapter);
}
/**
* Start loading the address book.
* Only called when we have a password cached, or no
* password is required.
*/
protected void onInitializeFragment() {
getLoaderManager().initLoader(0, null, this);
}
protected void onDestroyFragment() {
getLoaderManager().destroyLoader(0);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.identity_list, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean passwordRequired = I2PBote.getInstance().isPasswordRequired();
menu.findItem(R.id.export_identities).setVisible(!passwordRequired);
menu.findItem(R.id.import_identities).setVisible(!passwordRequired);
mNewIdentity.setVisibility(passwordRequired ? View.GONE : View.VISIBLE);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.export_identities:
Intent ei = new Intent(getActivity(), IdentityShipActivity.class);
ei.putExtra(IdentityShipActivity.EXPORTING, true);
startActivity(ei);
return true;
case R.id.import_identities:
Intent ii = new Intent(getActivity(), IdentityShipActivity.class);
ii.putExtra(IdentityShipActivity.EXPORTING, false);
startActivity(ii);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void startNewIdentity() {
Intent nii = new Intent(getActivity(), EditIdentityActivity.class);
getActivity().startActivity(nii);
}
protected void updateIdentityList() {
getLoaderManager().restartLoader(0, null, this);
}
// LoaderManager.LoaderCallbacks<SortedSet<EmailIdentity>>
public Loader<Collection<EmailIdentity>> onCreateLoader(int id, Bundle args) {
return new IdentityLoader(getActivity());
}
private static class IdentityLoader extends BetterAsyncTaskLoader<Collection<EmailIdentity>> implements IdentitiesListener {
public IdentityLoader(Context context) {
super(context);
}
@Override
public Collection<EmailIdentity> loadInBackground() {
Collection<EmailIdentity> identities = null;
try {
identities = I2PBote.getInstance().getIdentities().getAll();
} catch (PasswordException e) {
// TODO handle, but should not get here
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return identities;
}
@Override
protected void onStartMonitoring() {
I2PBote.getInstance().getIdentities().addIdentitiesListener(this);
}
@Override
protected void onStopMonitoring() {
I2PBote.getInstance().getIdentities().removeIdentitiesListener(this);
}
@Override
protected void releaseResources(Collection<EmailIdentity> data) {
}
// IdentitiesListener
@Override
public void identityAdded(String s) {
onContentChanged();
}
@Override
public void identityUpdated(String s) {
onContentChanged();
}
@Override
public void identityRemoved(String s) {
onContentChanged();
}
}
@Override
public void onLoadFinished(Loader<Collection<EmailIdentity>> loader,
Collection<EmailIdentity> data) {
mAdapter.setIdentities(data);
}
@Override
public void onLoaderReset(Loader<Collection<EmailIdentity>> loader) {
mAdapter.setIdentities(null);
}
}

View File

@ -0,0 +1,26 @@
package i2p.bote.android.identities;
import android.widget.Toast;
import i2p.bote.android.R;
import i2p.bote.android.util.DataShipActivity;
import i2p.bote.android.util.DataShipFragment;
public class IdentityShipActivity extends DataShipActivity {
@Override
protected DataShipFragment getDataShipFragment() {
return IdentityShipFragment.newInstance(mExporting);
}
// DataShipFragment.Callbacks
public void onTaskFinished() {
Toast.makeText(this,
mExporting ?
R.string.identities_exported :
R.string.identities_imported,
Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
}

View File

@ -0,0 +1,95 @@
package i2p.bote.android.identities;
import android.os.Bundle;
import android.view.View;
import java.io.File;
import java.io.FileDescriptor;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.DataShipFragment;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.fileencryption.PasswordException;
public abstract class IdentityShipFragment extends DataShipFragment {
public static DataShipFragment newInstance(boolean exporting) {
return exporting ?
new ExportIdentitiesFragment() :
new ImportIdentitiesFragment();
}
public static class ExportIdentitiesFragment extends ExportDataFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mExportFilename.setText("identities");
}
@Override
protected RobustAsyncTask<Object, String, String> getExportWaiter() {
return new ExportWaiter();
}
@Override
protected int getTitle() {
return R.string.export_identities;
}
private class ExportWaiter extends RobustAsyncTask<Object, String, String> {
@Override
protected String doInBackground(Object... params) {
try {
publishProgress(getResources().getString(R.string.exporting_identities));
I2PBote.getInstance().getIdentities().export(
(File) params[0],
(String) params[1]);
return null;
} catch (Throwable e) {
cancel(false);
return e.getMessage();
}
}
}
}
public static class ImportIdentitiesFragment extends ImportDataFragment {
@Override
protected RobustAsyncTask<Object, String, String> getImportWaiter() {
return new ImportWaiter();
}
@Override
protected int getTitle() {
return R.string.import_identities;
}
private class ImportWaiter extends RobustAsyncTask<Object, String, String> {
@Override
protected String doInBackground(Object... params) {
try {
publishProgress(getResources().getString(R.string.importing_identities));
boolean success = I2PBote.getInstance().getIdentities().importFromFileDescriptor(
(FileDescriptor) params[0],
(String) params[1],
(Boolean) params[2],
(Boolean) params[3]);
if (success)
return null;
else {
cancel(false);
return (params[1] == null) ?
getResources().getString(R.string.no_identities_found_maybe_encrypted) :
getResources().getString(R.string.no_identities_found);
}
} catch (Throwable e) {
e.printStackTrace();
cancel(false);
if (e instanceof PasswordException)
return getResources().getString(R.string.password_incorrect);
return e.getLocalizedMessage();
}
}
}
}
}

View File

@ -0,0 +1,78 @@
package i2p.bote.android.identities;
import android.annotation.SuppressLint;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import i2p.bote.android.BoteActivityBase;
public class ViewIdentityActivity extends BoteActivityBase {
NfcAdapter mNfcAdapter;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
String key = null;
Bundle args = getIntent().getExtras();
if (args != null)
key = args.getString(ViewIdentityFragment.ADDRESS);
if (key == null) {
setResult(RESULT_CANCELED);
finish();
return;
}
ViewIdentityFragment f = ViewIdentityFragment.newInstance(key);
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, f).commit();
}
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
@Override
public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
return getNdefMessage();
}
}, this);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public void onResume() {
super.onResume();
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.enableForegroundNdefPush(this, getNdefMessage());
}
}
private NdefMessage getNdefMessage() {
ViewIdentityFragment f = (ViewIdentityFragment) getSupportFragmentManager()
.findFragmentById(android.R.id.content);
return f.createNdefMessage();
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public void onPause() {
super.onPause();
if (mNfcAdapter != null &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mNfcAdapter.disableForegroundNdefPush(this);
}
}
}

View File

@ -0,0 +1,107 @@
package i2p.bote.android.identities;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import java.io.IOException;
import java.security.GeneralSecurityException;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ViewAddressFragment;
import i2p.bote.email.EmailIdentity;
import i2p.bote.fileencryption.PasswordException;
public class ViewIdentityFragment extends ViewAddressFragment {
private EmailIdentity mIdentity;
public static ViewIdentityFragment newInstance(String key) {
ViewIdentityFragment f = new ViewIdentityFragment();
Bundle args = new Bundle();
args.putString(ADDRESS, key);
f.setArguments(args);
return f;
}
@Override
protected void loadAddress() {
try {
mIdentity = BoteHelper.getIdentity(mAddress);
if (mIdentity == null) {
// No identity found, finish
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
} catch (PasswordException e) {
// TODO Handle
e.printStackTrace();
} catch (IOException e) {
// TODO Handle
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Handle
e.printStackTrace();
}
}
@Override
protected String getPublicName() {
return mIdentity.getPublicName();
}
@Override
protected int getDeleteAddressMessage() {
return R.string.delete_identity;
}
@Override
public void onResume() {
super.onResume();
Bitmap picture = BoteHelper.decodePicture(mIdentity.getPictureBase64());
if (picture != null)
mPicture.setImageBitmap(picture);
else {
ViewGroup.LayoutParams lp = mPicture.getLayoutParams();
mPicture.setImageBitmap(BoteHelper.getIdenticonForAddress(mAddress, lp.width, lp.height));
}
mPublicName.setText(mIdentity.getPublicName());
if (mIdentity.getDescription().isEmpty())
mDescription.setVisibility(View.GONE);
else {
mDescription.setText(mIdentity.getDescription());
mDescription.setVisibility(View.VISIBLE);
}
mCryptoImplName.setText(mIdentity.getCryptoImpl().getName());
}
@Override
protected void onEditAddress() {
Intent ei = new Intent(getActivity(), EditIdentityActivity.class);
ei.putExtra(EditIdentityFragment.IDENTITY_KEY, mAddress);
startActivity(ei);
}
@Override
public void onDeleteAddress() {
try {
BoteHelper.deleteIdentity(mAddress);
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,153 @@
package i2p.bote.android.intro;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import com.viewpagerindicator.LinePageIndicator;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
public class IntroActivity extends BoteActivityBase {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
*/
SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
*/
ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intro);
// Create the sections adapter.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
// Bind the page indicator to the pager.
LinePageIndicator pageIndicator = (LinePageIndicator)findViewById(R.id.page_indicator);
pageIndicator.setViewPager(mViewPager);
findViewById(R.id.skip_intro).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
setResult(Activity.RESULT_CANCELED);
finish();
}
});
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the intro sections.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
return PlaceholderFragment.newInstance(position);
}
@Override
public int getCount() {
return 6;
}
}
/**
* A placeholder fragment containing a simple view.
*/
public static class PlaceholderFragment extends Fragment {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private static final String ARG_SECTION_NUMBER = "section_number";
/**
* Returns a new instance of this fragment for the given section
* number.
*/
public static PlaceholderFragment newInstance(int sectionNumber) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return fragment;
}
public PlaceholderFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
switch (getArguments().getInt(ARG_SECTION_NUMBER)) {
case 1:
return inflater.inflate(R.layout.fragment_intro_1, container, false);
case 2:
return inflater.inflate(R.layout.fragment_intro_2, container, false);
case 3:
return inflater.inflate(R.layout.fragment_intro_3, container, false);
case 4:
return inflater.inflate(R.layout.fragment_intro_4, container, false);
case 5:
View v5 = inflater.inflate(R.layout.fragment_intro_5, container, false);
Button b = (Button) v5.findViewById(R.id.start_setup_wizard);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}
});
return v5;
default:
View v0 = inflater.inflate(R.layout.fragment_intro_0, container, false);
TextView tv = (TextView) v0.findViewById(R.id.intro_app_name);
tv.append(".");
TextView swipe = (TextView) v0.findViewById(R.id.intro_swipe_to_start);
swipe.setCompoundDrawablesWithIntrinsicBounds(
new IconicsDrawable(getActivity(), GoogleMaterial.Icon.gmd_arrow_back)
.colorRes(R.color.md_grey_600).sizeDp(24).paddingDp(4),
null, null, null
);
return v0;
}
}
}
}

View File

@ -0,0 +1,164 @@
package i2p.bote.android.intro;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
import i2p.bote.android.config.SetPasswordActivity;
import i2p.bote.android.identities.EditIdentityActivity;
public class SetupActivity extends BoteActivityBase {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// If a user has chosen to enter the setup wizard, don't let them
// accidentally exit it early.
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setHomeButtonEnabled(false);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new SetPasswordFragment())
.commit();
}
}
@Override
public void onBackPressed() {
// If a user has chosen to enter the setup wizard, don't let them
// accidentally exit it early.
Toast.makeText(this, R.string.please_finish_setup, Toast.LENGTH_SHORT).show();
}
/**
* Set password.
*/
public static class SetPasswordFragment extends Fragment {
private static final int SET_PASSWORD = 1;
public SetPasswordFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_setup_set_password, container, false);
rootView.findViewById(R.id.button_set_password).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(getActivity(), SetPasswordActivity.class);
startActivityForResult(i, SET_PASSWORD);
}
});
rootView.findViewById(R.id.button_skip).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
nextPage();
}
});
return rootView;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SET_PASSWORD) {
if (resultCode == RESULT_OK) {
nextPage();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void nextPage() {
getFragmentManager().beginTransaction()
.replace(R.id.container, new CreateIdentityFragment())
.commit();
}
}
/**
* Create identity.
*/
public static class CreateIdentityFragment extends Fragment {
private static final int CREATE_IDENTITY = 1;
public CreateIdentityFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_setup_create_identity, container, false);
rootView.findViewById(R.id.button_set_password).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(getActivity(), EditIdentityActivity.class);
startActivityForResult(i, CREATE_IDENTITY);
}
});
rootView.findViewById(R.id.button_skip).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
nextPage();
}
});
return rootView;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CREATE_IDENTITY) {
if (resultCode == RESULT_OK) {
nextPage();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void nextPage() {
getFragmentManager().beginTransaction()
.replace(R.id.container, new SetupFinishedFragment())
.commit();
}
}
/**
* Setup finished.
*/
public static class SetupFinishedFragment extends Fragment {
public SetupFinishedFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_setup_finished, container, false);
rootView.findViewById(R.id.button_finish).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getActivity().setResult(RESULT_OK);
getActivity().finish();
}
});
return rootView;
}
}
}

View File

@ -0,0 +1,229 @@
package i2p.bote.android.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import javax.activation.MimetypesFileTypeMap;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.Util;
import i2p.bote.android.BuildConfig;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
public class AttachmentProvider extends ContentProvider {
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".attachmentprovider";
private static final int RAW_ATTACHMENT = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, "*/*/#/RAW", RAW_ATTACHMENT);
}
private final static String[] OPENABLE_PROJECTION = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static Uri getUriForAttachment(String folderName, String messageId, int partNum) {
return new Uri.Builder()
.scheme("content")
.authority(AUTHORITY)
.appendPath(folderName)
.appendPath(messageId)
.appendPath(Integer.toString(partNum))
.appendPath("RAW")
.build();
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (sUriMatcher.match(uri) == UriMatcher.NO_MATCH)
throw new IllegalArgumentException("Invalid URI: " + uri);
if (projection == null) {
projection = OPENABLE_PROJECTION;
}
final MatrixCursor cursor = new MatrixCursor(projection, 1);
MatrixCursor.RowBuilder b = cursor.newRow();
try {
Part attachment = getAttachment(uri);
if (attachment == null)
return null;
for (String col : projection) {
switch (col) {
case OpenableColumns.DISPLAY_NAME:
b.add(attachment.getFileName());
break;
case OpenableColumns.SIZE:
b.add(Util.getPartSize(attachment));
break;
}
}
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (MessagingException e) {
e.printStackTrace();
}
return cursor;
}
@Override
public String getType(Uri uri) {
if (sUriMatcher.match(uri) != UriMatcher.NO_MATCH) {
try {
Part attachment = getAttachment(uri);
if (attachment != null) {
String contentType = attachment.getContentType();
// Remove any "; name=fileName" suffix
int delim = contentType.indexOf(';');
if (delim >= 0) {
String params = contentType.substring(delim + 1);
contentType = contentType.substring(0, delim);
// Double-check in case the attachment was created by an old
// I2P-Bote version that didn't detect MIME types correctly.
if ("application/octet-stream".equals(contentType)) {
// Find the filename
String filename = "";
delim = params.indexOf("name=");
if (delim >= 0) {
filename = params.substring(delim + 5);
delim = filename.indexOf(' ');
if (delim >= 0)
filename = params.substring(0, delim);
}
if (!filename.isEmpty()) {
MimetypesFileTypeMap mimeTypeMap = new MimetypesFileTypeMap();
contentType = mimeTypeMap.getContentType(filename);
}
}
}
return contentType;
}
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (MessagingException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (sUriMatcher.match(uri) == UriMatcher.NO_MATCH)
throw new FileNotFoundException("Invalid URI: " + uri);
if (!"r".equals(mode))
throw new FileNotFoundException("Attachments can only be read");
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
throw new FileNotFoundException("Could not open pipe for: "
+ uri.toString());
}
try {
Part attachment = getAttachment(uri);
if (attachment == null)
throw new FileNotFoundException("Unknown email or attachment for URI " + uri);
new TransferThread(attachment.getInputStream(),
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
} catch (Exception e) {
Log.e(getClass().getSimpleName(), "Exception accessing attachment", e);
throw new FileNotFoundException("Exception accessing attachment: " + e.getLocalizedMessage());
}
return pipe[0];
}
static class TransferThread extends Thread {
InputStream in;
OutputStream out;
TransferThread(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[8192];
int len;
try {
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception transferring file", e);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
private Part getAttachment(Uri uri) throws PasswordException, IOException, MessagingException {
List<String> segments = uri.getPathSegments();
String folderName = segments.get(0);
String messageId = segments.get(1);
int partNum = Integer.valueOf(segments.get(2));
Email email = BoteHelper.getEmail(folderName, messageId);
if (email != null) {
if (partNum >= 0 && partNum < email.getParts().size())
return email.getParts().get(partNum);
}
return null;
}
}

View File

@ -0,0 +1,345 @@
package i2p.bote.android.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import net.i2p.I2PAppContext;
import net.i2p.android.router.service.IRouterState;
import net.i2p.android.router.service.IRouterStateCallback;
import net.i2p.android.router.service.State;
import net.i2p.android.ui.I2PAndroidHelper;
import net.i2p.client.DomainSocketFactory;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterLaunch;
import net.i2p.util.Log;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import javax.mail.Address;
import javax.mail.MessagingException;
import i2p.bote.I2PBote;
import i2p.bote.android.EmailListActivity;
import i2p.bote.android.R;
import i2p.bote.android.ViewEmailActivity;
import i2p.bote.android.service.Init.RouterChoice;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.LocaleManager;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.NewEmailListener;
import i2p.bote.network.NetworkStatus;
import i2p.bote.network.NetworkStatusListener;
public class BoteService extends Service implements NetworkStatusListener, NewEmailListener {
/**
* The locale has just changed.
*/
public static final String LOCAL_BROADCAST_LOCALE_CHANGED = "i2p.bote.android.LOCAL_BROADCAST_LOCALE_CHANGED";
public static final String ROUTER_CHOICE = "router_choice";
public static final int NOTIF_ID_SERVICE = 8073;
public static final int NOTIF_ID_NEW_EMAIL = 80739047;
private LocaleManager localeManager = new LocaleManager();
RouterChoice mRouterChoice;
NotificationCompat.Builder mStatusBuilder;
@Override
public void onCreate() {
LocalBroadcastManager.getInstance(this).registerReceiver(onLocaleChanged, new IntentFilter(LOCAL_BROADCAST_LOCALE_CHANGED));
}
private BroadcastReceiver onLocaleChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
localeManager.updateServiceLocale(BoteService.this);
networkStatusChanged();
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mRouterChoice = (RouterChoice) intent.getSerializableExtra(ROUTER_CHOICE);
if (mRouterChoice == RouterChoice.INTERNAL)
new Thread(new RouterStarter()).start();
// Set log level for i2p.bote.* to DEBUG, and let Android filter out what it wants.
Properties limits = new Properties();
limits.setProperty("i2p.bote", Log.STR_DEBUG);
I2PAppContext.getGlobalContext().logManager().setLimits(limits);
I2PBote bote = I2PBote.getInstance();
if (mRouterChoice == RouterChoice.ANDROID) {
bote.getConfiguration().setI2CPDomainSocket(DomainSocketFactory.I2CP_SOCKET_ADDRESS);
}
bote.startUp();
bote.addNewEmailListener(this);
if (mRouterChoice == RouterChoice.ANDROID) {
// Bind to I2P Android
mTriedBindState = (new I2PAndroidHelper(this)).bind(mStateConnection, 0);
} else if (mRouterChoice == RouterChoice.REMOTE)
bote.connectNow();
mStatusBuilder = new NotificationCompat.Builder(this)
.setContentTitle(getResources().getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_notif)
.setOngoing(true)
.setOnlyAlertOnce(true);
Intent ni = new Intent(this, EmailListActivity.class);
ni.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pi = PendingIntent.getActivity(this, 0, ni, PendingIntent.FLAG_UPDATE_CURRENT);
mStatusBuilder.setContentIntent(pi);
updateServiceNotifText();
startForeground(NOTIF_ID_SERVICE, mStatusBuilder.build());
bote.addNetworkStatusListener(this);
return START_REDELIVER_INTENT;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
if (mTriedBindState && mStateConnection != null) {
if (mStateService != null) {
try {
mStateService.unregisterCallback(mStatusListener);
} catch (RemoteException e) {}
}
unbindService(mStateConnection);
}
mTriedBindState = false;
LocalBroadcastManager.getInstance(this).unregisterReceiver(onLocaleChanged);
I2PBote.getInstance().removeNetworkStatusListener(this);
I2PBote.getInstance().removeNewEmailListener(this);
new Thread(new Runnable() {
@Override
public void run() {
I2PBote.getInstance().shutDown();
}
}).start();
if (mRouterChoice == RouterChoice.INTERNAL)
new Thread(new RouterStopper()).start();
}
//
// Internal router helpers
//
private RouterContext mRouterContext;
private class RouterStarter implements Runnable {
public void run() {
RouterLaunch.main(null);
List<RouterContext> contexts = RouterContext.listContexts();
mRouterContext = contexts.get(0);
mRouterContext.router().setKillVMOnEnd(false);
}
}
private class RouterStopper implements Runnable {
public void run() {
RouterContext ctx = mRouterContext;
if (ctx != null)
ctx.router().shutdown(Router.EXIT_HARD);
}
}
//
// I2P Android helpers
//
private IRouterState mStateService = null;
private boolean mTriedBindState;
private ServiceConnection mStateConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className,
IBinder service) {
mStateService = IRouterState.Stub.asInterface(service);
try {
mStateService.registerCallback(mStatusListener);
final State state = mStateService.getState();
if (state == State.ACTIVE &&
I2PBote.getInstance().getNetworkStatus() == NetworkStatus.DELAY)
I2PBote.getInstance().connectNow();
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mStateService = null;
}
};
private final IRouterStateCallback.Stub mStatusListener =
new IRouterStateCallback.Stub() {
public void stateChanged(State newState) throws RemoteException {
if (newState == State.ACTIVE &&
I2PBote.getInstance().getNetworkStatus() == NetworkStatus.DELAY)
I2PBote.getInstance().connectNow();
else if (newState == State.STOPPING ||
newState == State.MANUAL_STOPPING ||
newState == State.MANUAL_QUITTING ||
newState == State.NETWORK_STOPPING)
stopSelf();
}
};
// NetworkStatusListener
@Override
public void networkStatusChanged() {
updateServiceNotifText();
startForeground(NOTIF_ID_SERVICE, mStatusBuilder.build());
}
private void updateServiceNotifText() {
String statusText;
switch (I2PBote.getInstance().getNetworkStatus()) {
case DELAY:
statusText = getResources().getString(R.string.waiting_for_network);
break;
case CONNECTING:
statusText = getResources().getString(R.string.connecting_to_network);
break;
case CONNECTED:
statusText = getResources().getString(R.string.connected_to_network);
break;
case ERROR:
statusText = getResources().getString(R.string.error);
break;
case NOT_STARTED:
default:
statusText = getResources().getString(R.string.not_started);
}
mStatusBuilder.setContentText(statusText);
}
// NewEmailListener
@Override
public void emailReceived(String messageId) {
NotificationManager nm = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder b = new NotificationCompat.Builder(this)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_notif)
.setDefaults(Notification.DEFAULT_ALL);
try {
EmailFolder inbox = I2PBote.getInstance().getInbox();
// Set the new email as \Recent
inbox.setRecent(messageId, true);
// Now display/update notification with all \Recent emails
List<Email> newEmails = BoteHelper.getRecentEmails(inbox);
int numNew = newEmails.size();
switch (numNew) {
case 0:
nm.cancel(NOTIF_ID_NEW_EMAIL);
return;
case 1:
Email email = newEmails.get(0);
String fromAddress = email.getOneFromAddress();
Bitmap picture = BoteHelper.getPictureForAddress(fromAddress);
if (picture != null)
b.setLargeIcon(picture);
else if (!email.isAnonymous()) {
int width = getResources().getDimensionPixelSize(R.dimen.notification_large_icon_width);
int height = getResources().getDimensionPixelSize(R.dimen.notification_large_icon_height);
b.setLargeIcon(BoteHelper.getIdenticonForAddress(fromAddress, width, height));
} else
b.setSmallIcon(R.drawable.ic_contact_picture);
b.setContentTitle(BoteHelper.getNameAndShortDestination(
fromAddress));
b.setContentText(email.getSubject());
Intent vei = new Intent(this, ViewEmailActivity.class);
vei.putExtra(ViewEmailActivity.FOLDER_NAME, inbox.getName());
vei.putExtra(ViewEmailActivity.MESSAGE_ID, email.getMessageID());
vei.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pvei = PendingIntent.getActivity(this, 0, vei, PendingIntent.FLAG_UPDATE_CURRENT);
b.setContentIntent(pvei);
break;
default:
b.setContentTitle(getResources().getQuantityString(
R.plurals.n_new_emails, numNew, numNew));
HashSet<Address> recipients = new HashSet<Address>();
String bigText = "";
for (Email ne : newEmails) {
recipients.add(BoteHelper.getOneLocalRecipient(ne));
bigText += BoteHelper.getNameAndShortDestination(
ne.getOneFromAddress());
bigText += ": " + ne.getSubject() + "\n";
}
b.setContentText(BoteHelper.joinAddressNames(recipients));
b.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText));
Intent eli = new Intent(this, EmailListActivity.class);
eli.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent peli = PendingIntent.getActivity(this, 0, eli, PendingIntent.FLAG_UPDATE_CURRENT);
b.setContentIntent(peli);
}
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
nm.notify(NOTIF_ID_NEW_EMAIL, b.build());
}
}

View File

@ -0,0 +1,194 @@
package i2p.bote.android.service;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.util.Log;
import net.i2p.android.ui.I2PAndroidHelper;
import net.i2p.client.I2PClient;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.OrderedProperties;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
public class Init {
private final Context ctx;
private final String myDir;
public enum RouterChoice {
INTERNAL,
ANDROID,
REMOTE
}
public Init(Context c) {
ctx = c;
// This needs to be changed so that we can have an alternative place
myDir = c.getFilesDir().getAbsolutePath();
}
/**
* Parses settings and prepares the system for starting the Bote service.
* @return the router choice.
*/
public RouterChoice initialize(I2PAndroidHelper helper) {
// Set up the locations so Router and WorkingDir can find them
// We do this again here, in the event settings were changed.
System.setProperty("i2p.dir.base", myDir);
System.setProperty("i2p.dir.config", myDir);
System.setProperty("wrapper.logfile", myDir + "/wrapper.log");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
RouterChoice routerChoice;
String i2cpHost, i2cpPort;
if (prefs.getBoolean("i2pbote.router.auto", true)) {
if (helper.isI2PAndroidInstalled())
routerChoice = RouterChoice.ANDROID;
else
routerChoice = RouterChoice.INTERNAL;
i2cpHost = "internal";
i2cpPort = "internal";
} else {
// Check manual settings
String which = prefs.getString("i2pbote.router.use", "internal");
if ("remote".equals(which)) {
routerChoice = RouterChoice.REMOTE;
i2cpHost = prefs.getString("i2pbote.i2cp.tcp.host", "127.0.0.1");
i2cpPort = prefs.getString("i2pbote.i2cp.tcp.port", "7654");
} else {
if ("android".equals(which))
routerChoice = RouterChoice.ANDROID;
else // Internal router
routerChoice = RouterChoice.INTERNAL;
i2cpHost = "internal";
i2cpPort = "internal";
}
}
// Set the I2CP host/port
System.setProperty(I2PClient.PROP_TCP_HOST, i2cpHost);
System.setProperty(I2PClient.PROP_TCP_PORT, i2cpPort);
if (routerChoice == RouterChoice.INTERNAL) {
mergeResourceToFile(R.raw.router_config, "router.config", null);
File certDir = new File(myDir, "certificates");
certDir.mkdir();
File certificates = new File(myDir, "certificates");
File[] allcertificates = certificates.listFiles();
if ( allcertificates != null) {
for (File f : allcertificates) {
Log.d(Constants.ANDROID_LOG_TAG, "Deleting old certificate file/dir " + f);
FileUtil.rmdir(f, false);
}
}
unzipResourceToDir(R.raw.certificates_zip, "certificates");
}
return routerChoice;
}
/**
* Load defaults from resource,
* then add props from settings,
* and write back
*
* @param f relative to base dir
* @param overrides local overrides or null
*/
public void mergeResourceToFile(int resID, String f, Properties overrides) {
InputStream in = null;
InputStream fin = null;
try {
in = ctx.getResources().openRawResource(resID);
Properties props = new OrderedProperties();
try {
fin = new FileInputStream(new File(myDir, f));
DataHelper.loadProps(props, fin);
} catch (IOException ioe) {
}
// write in default settings
DataHelper.loadProps(props, in);
// override with user settings
if (overrides != null)
props.putAll(overrides);
File path = new File(myDir, f);
DataHelper.storeProps(props, path);
} catch (IOException ioe) {
} catch (Resources.NotFoundException nfe) {
} finally {
if (in != null) try {
in.close();
} catch (IOException ioe) {
}
if (fin != null) try {
fin.close();
} catch (IOException ioe) {
}
}
}
/**
* @param folder relative to base dir
*/
private void unzipResourceToDir(int resID, String folder) {
InputStream in = null;
FileOutputStream out = null;
ZipInputStream zis = null;
Log.d(Constants.ANDROID_LOG_TAG, "Creating files in '" + myDir + "/" + folder + "/' from resource");
try {
// Context methods
in = ctx.getResources().openRawResource(resID);
zis = new ZipInputStream((in));
ZipEntry ze;
while ((ze = zis.getNextEntry()) != null) {
out = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
String name = ze.getName();
File f = new File(myDir + "/" + folder +"/" + name);
if (ze.isDirectory()) {
Log.d(Constants.ANDROID_LOG_TAG, "Creating directory " + myDir + "/" + folder +"/" + name + " from resource");
f.mkdir();
} else {
Log.d(Constants.ANDROID_LOG_TAG, "Creating file " + myDir + "/" + folder +"/" + name + " from resource");
byte[] bytes = baos.toByteArray();
out = new FileOutputStream(f);
out.write(bytes);
}
} catch (IOException ioe) {
} finally {
if (out != null) { try { out.close(); } catch (IOException ioe) {} out = null; }
}
}
} catch (IOException ioe) {
} catch (Resources.NotFoundException nfe) {
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) try { out.close(); } catch (IOException ioe) {}
if (zis != null) try { zis.close(); } catch (IOException ioe) {}
}
}
}

View File

@ -0,0 +1,124 @@
package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
public abstract class AuthenticatedFragment extends Fragment {
private FrameLayout mAuthenticatedView;
private MenuItem mLogIn;
private MenuItem mClearPassword;
private boolean mFragmentInitialized;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_authenticated, container, false);
mAuthenticatedView = (FrameLayout) view.findViewById(R.id.authenticated_view);
mAuthenticatedView.addView(onCreateAuthenticatedView(inflater, container, savedInstanceState));
return view;
}
protected abstract View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
@Override
public void onResume() {
super.onResume();
if (I2PBote.getInstance().isPasswordRequired()) {
// Ensure any existing data is destroyed.
destroyFragment();
} else {
// Password is cached, or not set.
initializeFragment();
}
getActivity().supportInvalidateOptionsMenu();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.authenticated_fragment, menu);
mLogIn = menu.findItem(R.id.action_log_in);
mClearPassword = menu.findItem(R.id.action_log_out);
mLogIn.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_lock));
mClearPassword.setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_lock_open));
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
mLogIn.setVisible(I2PBote.getInstance().isPasswordRequired());
mClearPassword.setVisible(I2PBote.getInstance().isPasswordInCache());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_log_in:
// Request a password from the user.
BoteHelper.requestPassword(getActivity(), new BoteHelper.RequestPasswordListener() {
@Override
public void onPasswordVerified() {
initializeFragment();
getActivity().supportInvalidateOptionsMenu();
}
@Override
public void onPasswordCanceled() {
}
});
return true;
case R.id.action_log_out:
BoteHelper.clearPassword();
destroyFragment();
getActivity().supportInvalidateOptionsMenu();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void initializeFragment() {
if (mFragmentInitialized)
return;
onInitializeFragment();
mAuthenticatedView.setVisibility(View.VISIBLE);
mFragmentInitialized = true;
}
private void destroyFragment() {
onDestroyFragment();
mAuthenticatedView.setVisibility(View.GONE);
mFragmentInitialized = false;
}
protected abstract void onInitializeFragment();
protected abstract void onDestroyFragment();
}

View File

@ -0,0 +1,125 @@
package i2p.bote.android.util;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
public abstract class BetterAsyncTaskLoader<T> extends AsyncTaskLoader<T> {
protected T mData;
public BetterAsyncTaskLoader(Context context) {
super(context);
}
/**
* Called when there is new data to deliver to the client. The
* super class will take care of delivering it; the implementation
* here just adds a little more logic.
*/
@Override
public void deliverResult(T data) {
if (isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if (data != null) {
releaseResources(data);
}
}
// Hold a reference to the old data so it doesn't get garbage collected.
// We must protect it until the new data has been delivered.
T oldData = mData;
mData = data;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(data);
}
// Invalidate the old data as we don't need it any more.
if (oldData != null && oldData != data) {
releaseResources(oldData);
}
}
/**
* Handles a request to start the Loader.
*/
@Override
protected void onStartLoading() {
if (mData != null) {
// Deliver any previously loaded data immediately.
deliverResult(mData);
}
// Start watching for changes
onStartMonitoring();
if (takeContentChanged() || mData == null) {
// When the observer detects a change, it should call onContentChanged()
// on the Loader, which will cause the next call to takeContentChanged()
// to return true. If this is ever the case (or if the current data is
// null), we force a new load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
@Override
protected void onStopLoading() {
// The Loader is in a stopped state, so we should attempt to cancel the
// current load (if there is one).
cancelLoad();
// Note that we leave the observer as is. Loaders in a stopped state
// should still monitor the data source for changes so that the Loader
// will know to force a new load if it is ever started again.
}
/**
* Handles a request to completely reset the Loader.
*/
@Override
protected void onReset() {
super.onReset();
// Ensure the loader has been stopped.
onStopLoading();
// At this point we can release the resources associated with 'mData'.
if (mData != null) {
releaseResources(mData);
mData = null;
}
// Stop monitoring for changes.
onStopMonitoring();
}
/**
* Handles a request to cancel a load.
*/
@Override
public void onCanceled(T data) {
// Attempt to cancel the current asynchronous load.
super.onCanceled(data);
// The load has been canceled, so we should release the resources
// associated with 'data'.
releaseResources(data);
}
protected abstract void onStartMonitoring();
protected abstract void onStopMonitoring();
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
* For a simple List, there is nothing to do. For something like a Cursor, we
* would close it in this method. All resources associated with the Loader
* should be released here.
*/
protected abstract void releaseResources(T data);
}

View File

@ -0,0 +1,618 @@
package i2p.bote.android.util;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import com.lambdaworks.codec.Base64;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.typeface.IIcon;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
import i2p.bote.android.provider.AttachmentProvider;
import i2p.bote.email.Email;
import i2p.bote.email.EmailDestination;
import i2p.bote.email.EmailIdentity;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.Outbox.EmailStatus;
import i2p.bote.packet.dht.Contact;
import i2p.bote.util.GeneralHelper;
import im.delight.android.identicons.Identicon;
public class BoteHelper extends GeneralHelper {
public static int getNumNewEmails(Context ctx, EmailFolder folder) throws PasswordException, GeneralSecurityException, IOException, MessagingException {
String selectedIdentityKey = ctx.getSharedPreferences(Constants.SHARED_PREFS, 0)
.getString(Constants.PREF_SELECTED_IDENTITY, null);
if (selectedIdentityKey == null)
return folder.getNumNewEmails();
int numNew = 0;
for (Email email : BoteHelper.getEmails(folder, null, true)) {
if (email.getMetadata().isUnread()) {
if (BoteHelper.isSentEmail(email)) {
String senderDest = BoteHelper.extractEmailDestination(email.getOneFromAddress());
if (selectedIdentityKey.equals(senderDest))
numNew++;
} else {
for (Address recipient : email.getAllRecipients()) {
String recipientDest = BoteHelper.extractEmailDestination(recipient.toString());
if (selectedIdentityKey.equals(recipientDest)) {
numNew++;
break;
}
}
}
}
}
return numNew;
}
/**
* Get the translated name of the folder.
* Built-in folders are special-cased; other folders are created by the
* user, so their name is already "translated".
*
* @param ctx Android Context to get strings from.
* @param folder The folder.
* @return The name of the folder.
*/
public static String getFolderDisplayName(Context ctx, EmailFolder folder) {
String name = folder.getName();
if ("inbox".equals(name))
return ctx.getResources().getString(R.string.folder_inbox);
else if ("outbox".equals(name))
return ctx.getResources().getString(R.string.folder_outbox);
else if ("sent".equals(name))
return ctx.getResources().getString(R.string.folder_sent);
else if ("trash".equals(name))
return ctx.getResources().getString(R.string.folder_trash);
else
return name;
}
/**
* Get the translated name of the folder with the number of
* new messages it contains appended.
*
* @param ctx Android Context to get strings from.
* @param folder The folder.
* @return The name of the folder.
* @throws PasswordException
*/
public static String getFolderDisplayNameWithNew(Context ctx, EmailFolder folder) throws PasswordException, GeneralSecurityException, IOException, MessagingException {
String displayName = getFolderDisplayName(ctx, folder);
int numNew = getNumNewEmails(ctx, folder);
if (numNew > 0)
displayName = displayName + " (" + numNew + ")";
return displayName;
}
public static Drawable getFolderIcon(Context ctx, EmailFolder folder) {
IIcon icon;
int padding;
switch (folder.getName()) {
case "inbox":
icon = GoogleMaterial.Icon.gmd_inbox;
padding = 3;
break;
case "outbox":
icon = GoogleMaterial.Icon.gmd_cloud_upload;
padding = 0;
break;
case "sent":
icon = GoogleMaterial.Icon.gmd_send;
padding = 1;
break;
case "trash":
icon = GoogleMaterial.Icon.gmd_delete;
padding = 3;
break;
default:
icon = null;
padding = 0;
}
return new IconicsDrawable(ctx, icon).colorRes(R.color.md_grey_600).sizeDp(24).paddingDp(padding);
}
public static Drawable getMenuIcon(Context ctx, GoogleMaterial.Icon icon) {
IconicsDrawable iconic = new IconicsDrawable(ctx, icon).color(Color.WHITE).sizeDp(24);
switch (icon) {
case gmd_attach_file:
case gmd_lock:
case gmd_lock_open:
case gmd_person_add:
case gmd_send:
iconic.paddingDp(1);
break;
case gmd_drafts:
case gmd_folder:
case gmd_markunread:
iconic.paddingDp(2);
break;
case gmd_create:
case gmd_delete:
case gmd_reply:
case gmd_save:
iconic.paddingDp(3);
break;
case gmd_forward:
iconic.paddingDp(4);
break;
case gmd_reply_all:
default:
break;
}
return iconic;
}
public static String getDisplayAddress(String address) throws PasswordException, IOException, GeneralSecurityException, MessagingException {
String fullAdr = getNameAndDestination(address);
String emailDest = extractEmailDestination(fullAdr);
String name = extractName(fullAdr);
return (emailDest == null ? address
: (name.isEmpty() ? emailDest.substring(0, 10)
: name + " <" + emailDest.substring(0, 10) + "...>"));
}
/**
* Get a Bitmap containing the picture for the contact or identity
* corresponding to the given address.
*
* @param address the address to get a picture for.
* @return a Bitmap, or null if no picture was found.
* @throws PasswordException
* @throws IOException
* @throws GeneralSecurityException
*/
public static Bitmap getPictureForAddress(String address) throws PasswordException, IOException, GeneralSecurityException {
String base64dest = extractEmailDestination(address);
if (base64dest != null) {
return getPictureForDestination(base64dest);
}
// Address not found anywhere, or found and has no picture
return null;
}
/**
* Get a Bitmap containing the picture for the contact or identity
* corresponding to the given Destination.
*
* @param base64dest the Destination to get a picture for.
* @return a Bitmap, or null if no picture was found.
* @throws PasswordException
* @throws IOException
* @throws GeneralSecurityException
*/
public static Bitmap getPictureForDestination(String base64dest) throws PasswordException, IOException, GeneralSecurityException {
// Address was found; try address book first
Contact c = getContact(base64dest);
if (c != null) {
// Address is in address book
String pic = c.getPictureBase64();
if (pic != null) {
return decodePicture(pic);
}
} else {
// Address is an identity
EmailIdentity i = getIdentity(base64dest);
if (i != null) {
String pic = i.getPictureBase64();
if (pic != null) {
return decodePicture(pic);
}
}
}
// Address is not known
return null;
}
public static Bitmap decodePicture(String picB64) {
if (picB64 == null)
return null;
byte[] decodedPic = Base64.decode(picB64.toCharArray());
return BitmapFactory.decodeByteArray(decodedPic, 0, decodedPic.length);
}
public static String encodePicture(Bitmap picture) {
if (picture == null)
return null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
picture.compress(CompressFormat.PNG, 0, baos);
return new String(Base64.encode(baos.toByteArray()));
}
public static Bitmap getIdenticonForAddress(String address, int width, int height) {
String identifier = extractEmailDestination(address);
if (identifier == null) {
// Check if the string contains chars in angle brackets
int ltIndex = address.indexOf('<');
int gtIndex = address.indexOf('>', ltIndex);
if (ltIndex >= 0 && gtIndex > 0)
identifier = address.substring(ltIndex + 1, gtIndex);
else
identifier = address;
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Identicon identicon = new Identicon();
identicon.show(identifier);
identicon.updateSize(canvas.getWidth(), canvas.getHeight());
identicon.draw(canvas);
return bitmap;
}
public static Bitmap getIdentityPicture(EmailIdentity identity, int identiconWidth, int identiconHeight) {
String pic = identity.getPictureBase64();
if (pic != null && !pic.isEmpty())
return BoteHelper.decodePicture(pic);
else
return BoteHelper.getIdenticonForAddress(identity.getKey(), identiconWidth, identiconHeight);
}
private static final String PROPERTY_SENT = "sent";
public static void setEmailSent(Email email, boolean isSent) {
email.getMetadata().setProperty(PROPERTY_SENT, isSent ? "true" : "false");
}
/**
* Determines if we sent this email, either anonymously or from a local identity.
*
* @param email The Email to query metadata for
* @return true if we sent this email, false otherwise
* @throws PasswordException
* @throws IOException
* @throws GeneralSecurityException
* @throws MessagingException
*/
public static boolean isSentEmail(Email email) throws PasswordException, IOException, GeneralSecurityException, MessagingException {
boolean isSent;
if (email.getMetadata().containsKey(PROPERTY_SENT)) {
String sentStr = email.getMetadata().getProperty(PROPERTY_SENT);
isSent = "true".equals(sentStr);
} else {
// Figure it out
// Is the sender anonymous?
if (email.isAnonymous()) {
// Assume we sent it unless we are a recipient
isSent = true;
Address[] recipients = email.getAllRecipients();
for (Address recipient : recipients) {
String toDest = EmailDestination.extractBase64Dest(recipient.toString());
if (toDest != null && getIdentity(toDest) != null) {
// We are a recipient
isSent = false;
break;
}
}
} else {
// Are we the sender?
String fromAddress = email.getOneFromAddress();
String fromDest = EmailDestination.extractBase64Dest(fromAddress);
isSent = (fromDest != null && getIdentity(fromDest) != null);
}
// Cache for next time
setEmailSent(email, isSent);
}
return isSent;
}
public static String getEmailStatusText(Context ctx, Email email, boolean full) {
Resources res = ctx.getResources();
EmailStatus emailStatus = getEmailStatus(email);
switch (emailStatus.getStatus()) {
case QUEUED:
return res.getString(R.string.queued);
case SENDING:
return res.getString(R.string.sending);
case SENT_TO:
if (full)
return res.getString(R.string.sent_to,
(Integer) emailStatus.getParam1(), (Integer) emailStatus.getParam2());
else
return res.getString(R.string.sent_to_short,
(Integer) emailStatus.getParam1(), (Integer) emailStatus.getParam2());
case EMAIL_SENT:
return res.getString(R.string.email_sent);
case GATEWAY_DISABLED:
return res.getString(R.string.gateway_disabled);
case NO_IDENTITY_MATCHES:
if (full)
return res.getString(R.string.no_identity_matches,
emailStatus.getParam1());
case INVALID_RECIPIENT:
if (full)
return res.getString(R.string.invalid_recipient,
emailStatus.getParam1());
case ERROR_CREATING_PACKETS:
if (full)
return res.getString(R.string.error_creating_packets,
emailStatus.getParam1());
case ERROR_SENDING:
if (full)
return res.getString(R.string.error_sending,
emailStatus.getParam1());
case ERROR_SAVING_METADATA:
if (full)
return res.getString(R.string.error_saving_metadata,
emailStatus.getParam1());
default:
// Short string for errors and unknown status
return res.getString(R.string.error);
}
}
public static boolean isInbox(EmailFolder folder) {
return isInbox(folder.getName());
}
public static boolean isInbox(String folderName) {
return "Inbox".equalsIgnoreCase(folderName);
}
public static boolean isOutbox(EmailFolder folder) {
return isOutbox(folder.getName());
}
public static boolean isOutbox(String folderName) {
return "Outbox".equalsIgnoreCase(folderName);
}
public static boolean isTrash(EmailFolder folder) {
return isTrash(folder.getName());
}
public static boolean isTrash(String folderName) {
return "Trash".equalsIgnoreCase(folderName);
}
public static List<Email> getRecentEmails(EmailFolder folder) throws PasswordException, MessagingException {
List<Email> emails = folder.getElements();
Iterator<Email> iter = emails.iterator();
while (iter.hasNext()) {
Email email = iter.next();
if (!email.isRecent())
iter.remove();
}
return emails;
}
public interface RequestPasswordListener {
public void onPasswordVerified();
public void onPasswordCanceled();
}
/**
* Request the password from the user, and try it.
*/
public static void requestPassword(final Context context, final RequestPasswordListener listener) {
requestPassword(context, listener, null);
}
/**
* Request the password from the user, and try it.
*
* @param error is pre-filled in the dialog if not null.
*/
public static void requestPassword(final Context context, final RequestPasswordListener listener, String error) {
LayoutInflater li = LayoutInflater.from(context);
View promptView = li.inflate(R.layout.dialog_password, null);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setView(promptView);
final EditText passwordInput = (EditText) promptView.findViewById(R.id.passwordInput);
if (error != null) {
TextView passwordError = (TextView) promptView.findViewById(R.id.passwordError);
passwordError.setText(error);
passwordError.setVisibility(View.VISIBLE);
}
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(passwordInput.getWindowToken(), 0);
dialog.dismiss();
new PasswordWaiter(context, listener).execute(passwordInput.getText().toString());
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
if (listener != null)
listener.onPasswordCanceled();
}
}).setCancelable(false);
AlertDialog passwordDialog = builder.create();
passwordDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
passwordDialog.show();
}
private static class PasswordWaiter extends AsyncTask<String, Void, String> {
private final Context mContext;
private final ProgressDialog mDialog;
private final RequestPasswordListener mListener;
public PasswordWaiter(Context context, RequestPasswordListener listener) {
super();
mContext = context;
mDialog = new ProgressDialog(context);
mListener = listener;
}
protected void onPreExecute() {
mDialog.setMessage(mContext.getResources().getString(
R.string.checking_password));
mDialog.setCancelable(false);
mDialog.show();
}
protected String doInBackground(String... params) {
try {
if (BoteHelper.tryPassword(params[0]))
return null;
else {
cancel(false);
return mContext.getResources().getString(
R.string.password_incorrect);
}
} catch (IOException e) {
cancel(false);
return mContext.getResources().getString(
R.string.password_file_error);
} catch (GeneralSecurityException e) {
cancel(false);
return mContext.getResources().getString(
R.string.password_file_error);
}
}
protected void onCancelled(String result) {
mDialog.dismiss();
requestPassword(mContext, mListener, result);
}
protected void onPostExecute(String result) {
// Password is valid
mDialog.dismiss();
if (mListener != null)
mListener.onPasswordVerified();
}
}
public static String joinAddressNames(Collection<Address> s) throws PasswordException, GeneralSecurityException, IOException {
StringBuilder builder = new StringBuilder();
Iterator<Address> iter = s.iterator();
while (iter.hasNext()) {
String name = getName(iter.next().toString());
builder.append(name);
if (!iter.hasNext()) {
break;
}
builder.append(", ");
}
return builder.toString();
}
/**
* Attempt to revoke any URI permissions that were granted on an Email's attachments.
* This is best-effort; exceptions are silently ignored.
*
* @param context the Context in which permissions were granted
* @param folderName where the Email is
* @param email the Email to revoke permissions for
*/
public static void revokeAttachmentUriPermissions(Context context, String folderName, Email email) {
List<Part> parts;
try {
parts = email.getParts();
} catch (Exception e) {
// Nothing we can do, abort
return;
}
for (Part part : parts) {
try {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
Uri uri = AttachmentProvider.getUriForAttachment(folderName,
email.getMessageID(), parts.indexOf(part));
context.revokeUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} catch (MessagingException e) {
// Ignore and carry on
}
}
}
public static void copyStream(InputStream in, OutputStream out) {
byte[] buf = new byte[8192];
int len;
try {
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
Log.e(Constants.ANDROID_LOG_TAG, "Exception copying streams", e);
}
}
public static String getHumanReadableSize(Context context, long size) {
int unit = (63 - Long.numberOfLeadingZeros(size)) / 10; // 0 if totalBytes<1K, 1 if 1K<=totalBytes<1M, etc.
double value = (double) size / (1 << (10 * unit));
int formatStr;
switch (unit) {
case 0:
formatStr = R.string.n_bytes;
break;
case 1:
formatStr = R.string.n_kilobytes;
break;
default:
formatStr = R.string.n_megabytes;
}
NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault());
if (value < 100)
formatter.setMaximumFractionDigits(1);
else
formatter.setMaximumFractionDigits(0);
return context.getString(formatStr, formatter.format(value));
}
}

View File

@ -0,0 +1,104 @@
package i2p.bote.android.util;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.Util;
import i2p.bote.email.Attachment;
public class ContentAttachment implements Attachment {
private Context mCtx;
private String mFileName;
private long mSize;
private DataHandler mDataHandler;
public ContentAttachment(Context context, final Uri uri) throws FileNotFoundException {
mCtx = context;
// Get the content resolver instance for this context
ContentResolver cr = context.getContentResolver();
Cursor returnCursor = cr.query(
uri,
new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
null, null, null);
if (returnCursor == null) {
throw new IllegalArgumentException("Query for URI " + uri + " returned null");
} else if (!returnCursor.moveToFirst()) {
throw new FileNotFoundException();
}
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
mFileName = returnCursor.getString(nameIndex);
mSize = returnCursor.getLong(sizeIndex);
returnCursor.close();
final String mimeType = cr.getType(uri);
mDataHandler = new DataHandler(new DataSource() {
@Override
public InputStream getInputStream() throws IOException {
return mCtx.getContentResolver().openInputStream(uri);
}
@Override
public OutputStream getOutputStream() throws IOException {
throw new IOException("Cannot write to attachments");
}
@Override
public String getContentType() {
return mimeType;
}
@Override
public String getName() {
return mFileName;
}
});
}
public ContentAttachment(Context context, Part part)
throws IOException, MessagingException {
mCtx = context;
mFileName = part.getFileName();
mSize = Util.getPartSize(part);
mDataHandler = part.getDataHandler();
}
@Override
public String getFileName() {
return mFileName;
}
public long getSize() {
return mSize;
}
public String getHumanReadableSize() {
return BoteHelper.getHumanReadableSize(mCtx, mSize);
}
@Override
public DataHandler getDataHandler() {
return mDataHandler;
}
@Override
public boolean clean() {
return true;
}
}

View File

@ -0,0 +1,37 @@
package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import i2p.bote.android.BoteActivityBase;
import i2p.bote.android.R;
public abstract class DataShipActivity extends BoteActivityBase implements
DataShipFragment.Callbacks {
public static final String EXPORTING = "exporting";
protected boolean mExporting;
protected abstract DataShipFragment getDataShipFragment();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_data_ship);
// Set the action bar
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
// Enable ActionBar app icon to behave as action to go back
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (savedInstanceState == null) {
Bundle args = getIntent().getExtras();
mExporting = args.getBoolean(EXPORTING);
DataShipFragment f = getDataShipFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.data_ship_frag, f).commit();
}
}
}

View File

@ -0,0 +1,391 @@
package i2p.bote.android.util;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.FileNotFoundException;
import i2p.bote.android.R;
public abstract class DataShipFragment extends Fragment {
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks {
void onTaskFinished();
}
private static Callbacks sDummyCallbacks = new Callbacks() {
public void onTaskFinished() {}
};
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof Callbacks))
throw new IllegalStateException("Activity must implement fragment's callbacks.");
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
mCallbacks = sDummyCallbacks;
}
// Code to identify the fragment that is calling onActivityResult().
static final int SHIP_WAITER = 0;
// Tag so we can find the task fragment again, in another
// instance of this fragment after rotation.
static final String SHIP_WAITER_TAG = "shipWaiterTask";
TextView mError;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
ShipWaiterFrag f = (ShipWaiterFrag) getFragmentManager().findFragmentByTag(SHIP_WAITER_TAG);
if (f != null)
f.setTargetFragment(this, SHIP_WAITER);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mError = (TextView) view.findViewById(R.id.error);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getActivity().setTitle(getTitle());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SHIP_WAITER) {
if (resultCode == Activity.RESULT_OK) {
mCallbacks.onTaskFinished();
} else if (resultCode == Activity.RESULT_CANCELED) {
setInterfaceEnabled(true);
mError.setText(data.getStringExtra("error"));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
protected abstract int getTitle();
protected abstract void setInterfaceEnabled(boolean enabled);
public static class ShipWaiterFrag extends TaskFragment<Object, String, String> {
String currentStatus;
TextView mStatus;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.dialog_status, container, false);
mStatus = (TextView) v.findViewById(R.id.status);
if (currentStatus != null && !currentStatus.isEmpty())
mStatus.setText(currentStatus);
return v;
}
@Override
public void updateProgress(String... values) {
currentStatus = values[0];
mStatus.setText(currentStatus);
}
@Override
public void taskFinished(String result) {
super.taskFinished(result);
if (getTargetFragment() != null) {
Intent i = new Intent();
i.putExtra("result", result);
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_OK, i);
}
}
@Override
public void taskCancelled(String error) {
super.taskCancelled(error);
if (getTargetFragment() != null) {
Intent i = new Intent();
i.putExtra("error", error);
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_CANCELED, i);
}
}
}
public static abstract class ExportDataFragment extends DataShipFragment {
protected EditText mExportFilename;
TextView mSuffix;
CheckBox mEncrypt;
View mPasswordEntry;
EditText mPassword;
EditText mConfirmPassword;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_export_data, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mExportFilename = (EditText) view.findViewById(R.id.export_filename);
mSuffix = (TextView) view.findViewById(R.id.suffix);
mEncrypt = (CheckBox) view.findViewById(R.id.encrypt);
mPasswordEntry = view.findViewById(R.id.password_entry);
mPassword = (EditText) view.findViewById(R.id.password);
mConfirmPassword = (EditText) view.findViewById(R.id.password_confirm);
mEncrypt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
mSuffix.setText(b ? ".bote" : ".txt");
mPasswordEntry.setVisibility(b ? View.VISIBLE : View.GONE);
}
});
view.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String exportFilename = mExportFilename.getText().toString();
String suffix = mSuffix.getText().toString();
boolean encrypt = mEncrypt.isChecked();
String password = null;
if (encrypt) {
password = mPassword.getText().toString();
String confirmPassword = mConfirmPassword.getText().toString();
if (password.isEmpty()) {
mPassword.setError(getActivity().getString(R.string.this_field_is_required));
mPassword.requestFocus();
return;
} else
mPassword.setError(null);
if (confirmPassword.isEmpty()) {
mConfirmPassword.setError(getActivity().getString(R.string.this_field_is_required));
mConfirmPassword.requestFocus();
return;
} else if (!password.equals(confirmPassword)) {
mConfirmPassword.setError(getActivity().getString(R.string.passwords_do_not_match));
mConfirmPassword.requestFocus();
return;
} else
mConfirmPassword.setError(null);
}
File exportFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
), exportFilename + suffix);
if (exportFile.exists()) {
// TODO ask to rename or overwrite
mExportFilename.setError(getActivity().getString(R.string.file_exists));
mExportFilename.requestFocus();
return;
} else
mExportFilename.setError(null);
exportIdentities(exportFile, password);
}
});
}
protected abstract RobustAsyncTask<Object, String, String> getExportWaiter();
private void exportIdentities(File exportFile, String password) {
setInterfaceEnabled(false);
mError.setText("");
ExportWaiterFrag f = ExportWaiterFrag.newInstance(exportFile, password);
f.setTask(getExportWaiter());
f.setTargetFragment(ExportDataFragment.this, SHIP_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.waiter_frag, f, SHIP_WAITER_TAG)
.commit();
}
@Override
protected void setInterfaceEnabled(boolean enabled) {
mExportFilename.setEnabled(enabled);
mEncrypt.setEnabled(enabled);
mPassword.setEnabled(enabled);
mConfirmPassword.setEnabled(enabled);
}
public static class ExportWaiterFrag extends ShipWaiterFrag {
static final String SHIP_FILE = "shipFile";
static final String PASSWORD = "password";
public static ExportWaiterFrag newInstance(File shipFile, String password) {
ExportWaiterFrag f = new ExportWaiterFrag();
Bundle args = new Bundle();
args.putSerializable(SHIP_FILE, shipFile);
args.putString(PASSWORD, password);
f.setArguments(args);
return f;
}
@Override
public Object[] getParams() {
Bundle args = getArguments();
return new Object[]{
args.getSerializable(SHIP_FILE),
args.getString(PASSWORD),
};
}
}
}
public static abstract class ImportDataFragment extends DataShipFragment {
static final int SELECT_IMPORT_FILE = 1;
EditText mPassword;
CheckBox mOverwrite;
CheckBox mReplace;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_import_data, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mPassword = (EditText) view.findViewById(R.id.password);
mOverwrite = (CheckBox) view.findViewById(R.id.overwrite);
mReplace = (CheckBox) view.findViewById(R.id.replace);
mOverwrite.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
mReplace.setVisibility(b ? View.GONE : View.VISIBLE);
}
});
view.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
if (mPassword.getText().toString().isEmpty()) {
i.setType("text/plain");
} else {
i.setType("*/*");
}
i.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(i, SELECT_IMPORT_FILE);
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(getActivity(), R.string.please_install_a_file_manager,
Toast.LENGTH_SHORT).show();
}
}
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SELECT_IMPORT_FILE) {
if (resultCode == Activity.RESULT_OK) {
Uri result = data.getData();
try {
ParcelFileDescriptor pfd = getActivity().getContentResolver().openFileDescriptor(result, "r");
String password = mPassword.getText().toString();
if (password.isEmpty())
password = null;
importIdentities(pfd, password, !mOverwrite.isChecked(), mReplace.isChecked());
} catch (FileNotFoundException e) {
e.printStackTrace();
mError.setText(e.getLocalizedMessage());
}
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
protected abstract RobustAsyncTask<Object, String, String> getImportWaiter();
private void importIdentities(ParcelFileDescriptor importFile, String password,
boolean append, boolean replace) {
setInterfaceEnabled(false);
mError.setText("");
ImportWaiterFrag f = ImportWaiterFrag.newInstance(
importFile, password, append, replace);
f.setTask(getImportWaiter());
f.setTargetFragment(ImportDataFragment.this, SHIP_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.waiter_frag, f, SHIP_WAITER_TAG)
.commit();
}
@Override
protected void setInterfaceEnabled(boolean enabled) {
mPassword.setEnabled(enabled);
mOverwrite.setEnabled(enabled);
}
public static class ImportWaiterFrag extends ShipWaiterFrag {
static final String SHIP_FILE_DESCRIPTOR = "shipFile";
static final String PASSWORD = "password";
static final String APPEND = "append";
static final String REPLACE = "replace";
public static ImportWaiterFrag newInstance(ParcelFileDescriptor shipFile, String password,
boolean append, boolean replace) {
ImportWaiterFrag f = new ImportWaiterFrag();
Bundle args = new Bundle();
args.putParcelable(SHIP_FILE_DESCRIPTOR, shipFile);
args.putString(PASSWORD, password);
args.putBoolean(APPEND, append);
args.putBoolean(REPLACE, replace);
f.setArguments(args);
return f;
}
@Override
public Object[] getParams() {
Bundle args = getArguments();
return new Object[]{
((ParcelFileDescriptor) args.getParcelable(SHIP_FILE_DESCRIPTOR))
.getFileDescriptor(),
args.getString(PASSWORD),
args.getBoolean(APPEND),
args.getBoolean(REPLACE),
};
}
}
}
}

View File

@ -0,0 +1,61 @@
package i2p.bote.android.util;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
public class DeleteAddressDialogFragment extends DialogFragment {
public interface DeleteAddressDialogListener {
void onDeleteAddress();
}
DeleteAddressDialogListener mListener;
public static DialogFragment newInstance(int message) {
DialogFragment f = new DeleteAddressDialogFragment();
Bundle args = new Bundle();
args.putInt("message", message);
f.setArguments(args);
return f;
}
public void onAttachToParentFragment(Fragment fragment) {
// Verify that the host fragment implements the callback interface
try {
// Instantiate the DeleteAddressDialogListener so we can send events to the host
mListener = (DeleteAddressDialogListener) fragment;
} catch (ClassCastException e) {
// The fragment doesn't implement the interface, throw exception
throw new ClassCastException(fragment.toString()
+ " must implement DeleteAddressDialogListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onAttachToParentFragment(getParentFragment());
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(getArguments().getInt("message"))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
mListener.onDeleteAddress();
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
return builder.create();
}
}

View File

@ -0,0 +1,108 @@
package i2p.bote.android.util;
import java.io.File;
import java.util.List;
import i2p.bote.android.R;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
public class EditPictureFragment extends Fragment {
static final int REQUEST_PICTURE_FILE = 1;
static final int CROP_PICTURE = 2;
Uri mPictureCaptureUri;
Bitmap mPicture;
ImageView mPictureView;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mPictureView = (ImageView) view.findViewById(R.id.picture);
// Set up listener for picture changing
mPictureView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.setType("image/*");
startActivityForResult(
Intent.createChooser(i, getResources().getString(R.string.select_a_picture)),
REQUEST_PICTURE_FILE);
}
});
}
protected void setPictureB64(String pic) {
mPicture = BoteHelper.decodePicture(pic);
if (mPicture != null)
mPictureView.setImageBitmap(mPicture);
}
protected String getPictureB64() {
return BoteHelper.encodePicture(mPicture);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_CANCELED) {
System.out.println("Cancelled");
if (mPictureCaptureUri != null ) {
getActivity().getContentResolver().delete(mPictureCaptureUri, null, null);
mPictureCaptureUri = null;
}
}
return;
}
switch (requestCode) {
case REQUEST_PICTURE_FILE:
mPictureCaptureUri = data.getData();
cropPicture();
break;
case CROP_PICTURE:
Bundle extras = data.getExtras();
if (extras != null) {
mPicture = extras.getParcelable("data");
mPictureView.setImageBitmap(mPicture);
}
File f = new File(mPictureCaptureUri.getPath());
if (f.exists())
f.delete();
break;
}
}
private void cropPicture() {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setType("image/*");
List<ResolveInfo> list = getActivity().getPackageManager().queryIntentActivities(intent, 0);
if (list.size() == 0) {
Toast.makeText(getActivity(), R.string.no_image_cropping_app_found, Toast.LENGTH_SHORT)
.show();
} else {
intent.setData(mPictureCaptureUri);
intent.putExtra("outputX", 72);
intent.putExtra("outputY", 72);
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("scale", true);
intent.putExtra("return-data", true);
startActivityForResult(
Intent.createChooser(intent,
getResources().getString(R.string.select_a_cropping_app)),
CROP_PICTURE);
}
}
}

View File

@ -0,0 +1,64 @@
package i2p.bote.android.util;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import java.util.Locale;
public class LocaleManager {
private static final String DEFAULT_LANGUAGE = "zz";
private Locale currentLocale;
public void onCreate(Activity activity) {
currentLocale = getSelectedLocale(activity);
setContextLocale(activity, currentLocale);
}
public void onResume(Activity activity) {
// If the activity has the incorrect locale, restart it
if (!currentLocale.equals(getSelectedLocale(activity))) {
Intent intent = activity.getIntent();
activity.finish();
activity.overridePendingTransition(0, 0);
activity.startActivity(intent);
activity.overridePendingTransition(0, 0);
}
}
public void updateServiceLocale(Service service) {
currentLocale = getSelectedLocale(service);
setContextLocale(service, currentLocale);
}
private static Locale getSelectedLocale(Context context) {
String selectedLanguage = PreferenceManager.getDefaultSharedPreferences(context).getString(
"pref_language", DEFAULT_LANGUAGE
);
String language[] = TextUtils.split(selectedLanguage, "_");
if (language[0].equals(DEFAULT_LANGUAGE))
return Resources.getSystem().getConfiguration().locale;
else if (language.length == 2)
return new Locale(language[0], language[1]);
else
return new Locale(language[0]);
}
private static void setContextLocale(Context context, Locale selectedLocale) {
Configuration configuration = context.getResources().getConfiguration();
if (!configuration.locale.equals(selectedLocale)) {
configuration.locale = selectedLocale;
context.getResources().updateConfiguration(
configuration,
context.getResources().getDisplayMetrics()
);
}
}
}

View File

@ -0,0 +1,87 @@
package i2p.bote.android.util;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.folder.EmailFolder;
public class MoveToDialogFragment extends DialogFragment {
public static final String CURRENT_FOLDER = "current_folder";
public static MoveToDialogFragment newInstance(EmailFolder currentFolder) {
MoveToDialogFragment f = new MoveToDialogFragment();
Bundle args = new Bundle();
args.putString(CURRENT_FOLDER, currentFolder.getName());
f.setArguments(args);
return f;
}
public interface MoveToDialogListener {
public void onFolderSelected(EmailFolder newFolder);
}
MoveToDialogListener mListener;
List<EmailFolder> mFolders;
List<String> mFolderDisplayNames;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (MoveToDialogListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement MoveToDialogListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFolders = I2PBote.getInstance().getEmailFolders();
mFolderDisplayNames = new ArrayList<String>();
String curFolder = getArguments().getString(CURRENT_FOLDER);
Iterator<EmailFolder> i = mFolders.iterator();
while (i.hasNext()) {
EmailFolder folder = i.next();
if (folder.getName().equals(curFolder) || BoteHelper.isOutbox(folder.getName()))
i.remove();
else
mFolderDisplayNames.add(
BoteHelper.getFolderDisplayName(getActivity(), folder));
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.action_move_to)
.setItems(mFolderDisplayNames.toArray(new String[mFolderDisplayNames.size()]),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mListener.onFolderSelected(mFolders.get(which));
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
return builder.create();
}
}

View File

@ -0,0 +1,308 @@
/**
* Copyright (C) 2015 str4d
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.AbsListView;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* Utilities for handling multiple selection in list views. Contains functionality similar to {@link
* AbsListView#CHOICE_MODE_MULTIPLE_MODAL} which works with {@link AppCompatActivity} and
* backward-compatible action bars.
*/
public class MultiSelectionUtil {
/**
* Attach a Controller to the given <code>recyclerView</code>, <code>activity</code>
* and <code>listener</code>.
*
* @param recyclerView RecyclerView which displays {@link android.widget.Checkable} items.
* @param activity Activity which contains the ListView.
* @param listener Listener that will manage the selection mode.
* @return the attached Controller instance.
*/
public static Controller attachMultiSelectionController(final RecyclerView recyclerView,
final AppCompatActivity activity, final MultiChoiceModeListener listener) {
if (!(recyclerView.getAdapter() instanceof SelectableAdapter))
throw new IllegalArgumentException("Adapter must extend SelectableAdapter");
return new Controller(recyclerView, activity, listener);
}
public interface Selector {
public boolean inActionMode();
public void selectItem(int position, long id);
}
/**
* Class which provides functionality similar to {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}
* for the {@link RecyclerView} provided to it.
*/
public static class Controller implements Selector {
private final RecyclerView mRecyclerView;
private final SelectableAdapter mAdapter;
private final AppCompatActivity mActivity;
private final MultiChoiceModeListener mListener;
private final Callbacks mCallbacks;
// Current Action Mode (if there is one)
private ActionMode mActionMode;
// Keeps record of any items that should be checked on the next action mode creation
private HashSet<Pair<Integer, Long>> mItemsToCheck;
private Controller(RecyclerView recyclerView, AppCompatActivity activity,
MultiChoiceModeListener listener) {
mRecyclerView = recyclerView;
mAdapter = (SelectableAdapter) recyclerView.getAdapter();
mActivity = activity;
mListener = listener;
mCallbacks = new Callbacks();
mAdapter.setSelector(this);
}
@Override
public boolean inActionMode() {
return mActionMode != null;
}
@Override
public void selectItem(int position, long id) {
if (mActionMode == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
mItemsToCheck.add(new Pair<Integer, Long>(position, id));
mActionMode = mActivity.startSupportActionMode(mCallbacks);
} else {
mAdapter.toggleSelection(position);
// Check to see what the new checked state is, and then notify the listener
final boolean checked = mAdapter.isSelected(position);
mListener.onItemCheckedStateChanged(mActionMode, position, id, checked);
boolean hasCheckedItem = checked;
// Check to see if we have any checked items
if (!hasCheckedItem)
hasCheckedItem = mAdapter.getSelectedItemCount() > 0;
// If we don't have any checked items, finish the action mode
if (!hasCheckedItem)
mActionMode.finish();
}
}
/**
* Finish the current Action Mode (if there is one).
*/
public void finish() {
if (mActionMode != null) {
mActionMode.finish();
}
}
/**
* This method should be called from your {@link AppCompatActivity} or
* {@link android.support.v4.app.Fragment Fragment} to allow the controller to restore any
* instance state.
*
* @param savedInstanceState - The state passed to your Activity or Fragment.
*/
public void restoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
long[] checkedIds = savedInstanceState.getLongArray(getStateKey());
if (checkedIds != null && checkedIds.length > 0) {
HashSet<Long> idsToCheckOnRestore = new HashSet<Long>();
for (long id : checkedIds) {
idsToCheckOnRestore.add(id);
}
tryRestoreInstanceState(idsToCheckOnRestore);
}
}
}
/**
* This method should be called from
* {@link AppCompatActivity#onSaveInstanceState(android.os.Bundle)} or
* {@link android.support.v4.app.Fragment#onSaveInstanceState(android.os.Bundle)
* Fragment.onSaveInstanceState(Bundle)} to allow the controller to save its instance
* state.
*
* @param outState - The state passed to your Activity or Fragment.
*/
public void saveInstanceState(Bundle outState) {
if (mActionMode != null && mAdapter.hasStableIds()) {
List<Integer> selectedItems = mAdapter.getSelectedItems();
long[] selectedItemIds = new long[selectedItems.size()];
for (int i = 0; i < selectedItems.size(); i++) {
selectedItemIds[i] = mAdapter.getItemId(selectedItems.get(i));
}
outState.putLongArray(getStateKey(), selectedItemIds);
}
}
// Internal utility methods
private String getStateKey() {
return MultiSelectionUtil.class.getSimpleName() + "_" + mRecyclerView.getId();
}
private void tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore) {
if (idsToCheckOnRestore == null) {
return;
}
boolean idsFound = false;
for (int pos = mAdapter.getItemCount() - 1; pos >= 0; pos--) {
if (idsToCheckOnRestore.contains(mAdapter.getItemId(pos))) {
idsFound = true;
if (mItemsToCheck == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
}
mItemsToCheck.add(new Pair<Integer, Long>(pos, mAdapter.getItemId(pos)));
}
}
if (idsFound) {
// We found some IDs that were checked. Let's now restore the multi-selection
// state.
mActionMode = mActivity.startSupportActionMode(mCallbacks);
}
}
/**
* This class encapsulates all of the callbacks necessary for the controller class.
*/
final class Callbacks implements ActionMode.Callback {
@Override
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
if (mListener.onCreateActionMode(actionMode, menu)) {
mActionMode = actionMode;
// If there are some items to check, do it now
if (mItemsToCheck != null) {
for (Pair<Integer, Long> posAndId : mItemsToCheck) {
mAdapter.toggleSelection(posAndId.first);
// Notify the listener that the item has been checked
mListener.onItemCheckedStateChanged(mActionMode, posAndId.first,
posAndId.second, true);
}
}
return true;
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
// Proxy listener
return mListener.onPrepareActionMode(actionMode, menu);
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
// Proxy listener
return mListener.onActionItemClicked(actionMode, menuItem);
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mListener.onDestroyActionMode(actionMode);
// Clear all the checked items
mAdapter.clearSelections();
// Clear the Action Mode
mActionMode = null;
}
}
}
/**
* @see android.widget.AbsListView.MultiChoiceModeListener
*/
public static interface MultiChoiceModeListener extends ActionMode.Callback {
/**
* @see android.widget.AbsListView.MultiChoiceModeListener#onItemCheckedStateChanged(
*android.view.ActionMode, int, long, boolean)
*/
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked);
}
public static abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Selector mSelector;
private SparseBooleanArray selectedItems;
public SelectableAdapter() {
selectedItems = new SparseBooleanArray();
}
public void setSelector(Selector selector) {
mSelector = selector;
}
public Selector getSelector() {
return mSelector;
}
public void toggleSelection(int position) {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
} else {
selectedItems.put(position, true);
}
notifyItemChanged(position);
}
public boolean isSelected(int position) {
return selectedItems.get(position, false);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List<Integer> getSelectedItems() {
List<Integer> items =
new ArrayList<Integer>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
}
}

View File

@ -0,0 +1,29 @@
package i2p.bote.android.util;
import android.graphics.Bitmap;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = -2874686247798691378L;
private String name;
private String address;
private Bitmap picture;
private boolean isExternal;
public Person(String n, String a, Bitmap p) { this(n, a, p, false); }
public Person(String n, String a, Bitmap p, boolean e) { name = n; address = a; picture = p; isExternal = e; }
public String getName() { return name; }
public String getAddress() { return address; }
public Bitmap getPicture() { return picture; }
public boolean isExternal() { return isExternal; }
@Override
public boolean equals(Object other) {
return other instanceof Person && address.equals(((Person) other).address);
}
@Override
public String toString() { return name; }
}

View File

@ -0,0 +1,78 @@
/**
* Copyright (C) 2014 str4d
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011 Andreas Schildbach
*
* 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/>.
*/
package i2p.bote.android.util;
import android.graphics.Bitmap;
import android.graphics.Color;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.i2p.I2PAppContext;
import net.i2p.util.Log;
import java.util.Hashtable;
/**
* Copied from OpenKeychain
* Originally copied from Bitcoin Wallet
*/
public class QrCodeUtils {
/**
* Generate Bitmap with QR Code based on input.
*
* @param input The data to render as a QR code.
* @param size The preferred width and height of the QR code in pixels.
* @return QR Code as Bitmap
*/
public static Bitmap getQRCodeBitmap(final String input, final int size) {
try {
final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
final BitMatrix result = new QRCodeWriter().encode(input, BarcodeFormat.QR_CODE, size,
size, hints);
final int width = result.getWidth();
final int height = result.getHeight();
final int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
final int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
}
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (final WriterException e) {
Log log = I2PAppContext.getGlobalContext().logManager().getLog(QrCodeUtils.class);
if (log.shouldLog(Log.ERROR))
log.error("QrCodeUtils", e);
return null;
}
}
}

View File

@ -0,0 +1,30 @@
package i2p.bote.android.util;
import android.os.AsyncTask;
public abstract class RobustAsyncTask<Params, Progress, Result> extends
AsyncTask<Params, Progress, Result> {
TaskFragment<Params, Progress, Result> mDialog;
void setFragment(TaskFragment<Params, Progress, Result> fragment) {
mDialog = fragment;
}
@Override
protected void onProgressUpdate(Progress... values) {
if (mDialog != null)
mDialog.updateProgress(values);
}
@Override
protected void onPostExecute(Result result) {
if (mDialog != null)
mDialog.taskFinished(result);
}
@Override
protected void onCancelled(Result result) {
if (mDialog != null)
mDialog.taskCancelled(result);
}
}

View File

@ -0,0 +1,69 @@
package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
public class TaskFragment<Params, Progress, Result> extends DialogFragment {
RobustAsyncTask<Params, Progress, Result> mTask;
public void setTask(RobustAsyncTask<Params, Progress, Result> task) {
mTask = task;
mTask.setFragment(this);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this instance so it isn't destroyed
setRetainInstance(true);
// Start the task
if (mTask != null)
mTask.execute(getParams());
}
// This is to work around what is apparently a bug. If you don't have it
// here the dialog will be dismissed on rotation, so tell it not to dismiss.
@Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
@Override
public void onResume()
{
super.onResume();
// This is a little hacky, but we will see if the task has finished
// while we weren't in this activity, and then we can dismiss ourselves.
if (mTask == null)
dismiss();
}
public Params[] getParams() {
return null;
}
public void updateProgress(Progress... values) {}
public void taskFinished(Result result) {
finishTask();
}
public void taskCancelled(Result result) {
finishTask();
}
private void finishTask() {
// Make sure we check if it is resumed because we will crash if trying
// to dismiss the dialog after the user has switched to another app.
if (isResumed())
dismiss();
// If we aren't resumed, setting the task to null will allow us to
// dismiss ourselves in onResume().
mTask = null;
}
}

View File

@ -0,0 +1,398 @@
package i2p.bote.android.util;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
public abstract class ViewAddressFragment extends Fragment implements
DeleteAddressDialogFragment.DeleteAddressDialogListener {
public static final String ADDRESS = "address";
protected String mAddress;
Toolbar mToolbar;
protected ImageView mPicture;
protected TextView mPublicName;
protected TextView mDescription;
protected TextView mCryptoImplName;
TextView mAddressField;
ImageView mAddressQrCode;
TextView mFingerprint;
ImageView mExpandedQrCode;
// Hold a reference to the current animator,
// so that it can be canceled mid-way.
private Animator mQrCodeAnimator;
// The system "short" animation time duration, in milliseconds. This
// duration is ideal for subtle animations or animations that occur
// very frequently.
private int mShortAnimationDuration;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mAddress = getArguments().getString(ADDRESS);
// Retrieve and cache the system's default "short" animation time.
mShortAnimationDuration = getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_view_address, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mToolbar = (Toolbar) view.findViewById(R.id.main_toolbar);
mPicture = (ImageView) view.findViewById(R.id.picture);
mPublicName = (TextView) view.findViewById(R.id.public_name);
mDescription = (TextView) view.findViewById(R.id.description);
mFingerprint = (TextView) view.findViewById(R.id.fingerprint);
mCryptoImplName = (TextView) view.findViewById(R.id.crypto_impl_name);
mAddressField = (TextView) view.findViewById(R.id.email_dest);
mAddressQrCode = (ImageView) view.findViewById(R.id.email_dest_qr_code);
mExpandedQrCode = (ImageView) view.findViewById(R.id.expanded_qr_code);
view.findViewById(R.id.copy_key).setOnClickListener(new View.OnClickListener() {
@SuppressWarnings("deprecation")
@Override
public void onClick(View view) {
Object clipboardService = getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) clipboardService;
clipboard.setText(mAddress);
} else {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) clipboardService;
android.content.ClipData clip = android.content.ClipData.newPlainText(
getString(R.string.bote_dest_for, getPublicName()), mAddress);
clipboard.setPrimaryClip(clip);
}
Toast.makeText(getActivity(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
});
if (mAddress != null) {
loadAddress();
} else {
// No address provided, finish
// Should not happen
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
}
protected abstract void loadAddress();
protected abstract String getPublicName();
protected abstract int getDeleteAddressMessage();
protected abstract void onEditAddress();
public abstract void onDeleteAddress();
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
AppCompatActivity activity = ((AppCompatActivity) getActivity());
// Set the action bar
activity.setSupportActionBar(mToolbar);
// Enable ActionBar app icon to behave as action to go back
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public void onResume() {
super.onResume();
mAddressField.setText(mAddress);
mAddressQrCode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
zoomQrCode();
}
});
loadQrCode();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.view_address, menu);
menu.findItem(R.id.action_edit_address).setIcon(BoteHelper.getMenuIcon(getActivity(), GoogleMaterial.Icon.gmd_create));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_edit_address:
onEditAddress();
return true;
case R.id.action_delete_address:
DialogFragment df = DeleteAddressDialogFragment.newInstance(getDeleteAddressMessage());
df.show(getChildFragmentManager(), "deleteaddress");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public NdefMessage createNdefMessage() {
return new NdefMessage(new NdefRecord[]{
createNameRecord(),
createDestinationRecord()
});
}
private NdefRecord createNameRecord() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
return new NdefRecord(
NdefRecord.TNF_EXTERNAL_TYPE,
"i2p.bote:contact".getBytes(),
new byte[0],
getPublicName().getBytes()
);
else
return NdefRecord.createExternal(
"i2p.bote", "contact", getPublicName().getBytes()
);
}
private NdefRecord createDestinationRecord() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
return new NdefRecord(
NdefRecord.TNF_EXTERNAL_TYPE,
"i2p.bote:contactDestination".getBytes(),
new byte[0],
mAddress.getBytes()
);
else
return NdefRecord.createExternal(
"i2p.bote", "contactDestination", mAddress.getBytes()
);
}
/**
* Load QR Code asynchronously and with a fade in animation
*/
private void loadQrCode() {
AsyncTask<Void, Void, Bitmap[]> loadTask =
new AsyncTask<Void, Void, Bitmap[]>() {
protected Bitmap[] doInBackground(Void... unused) {
String qrCodeContent = Constants.EMAILDEST_SCHEME + ":" + mAddress;
// render with minimal size
Bitmap qrCode = QrCodeUtils.getQRCodeBitmap(qrCodeContent, 0);
Bitmap[] scaled = new Bitmap[2];
// scale the image up to our actual size. we do this in code rather
// than let the ImageView do this because we don't require filtering.
int size = getResources().getDimensionPixelSize(R.dimen.qr_code_size);
scaled[0] = Bitmap.createScaledBitmap(qrCode, size, size, false);
// scale for the expanded image
DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
scaled[1] = Bitmap.createScaledBitmap(qrCode,
smallestDimen, smallestDimen,
false);
return scaled;
}
protected void onPostExecute(Bitmap[] scaled) {
// only change view, if fragment is attached to activity
if (ViewAddressFragment.this.isAdded()) {
mAddressQrCode.setImageBitmap(scaled[0]);
mExpandedQrCode.setImageBitmap(scaled[1]);
// simple fade-in animation
AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
anim.setDuration(200);
mAddressQrCode.startAnimation(anim);
}
}
};
loadTask.execute();
}
private void zoomQrCode() {
// If there's an animation in progress, cancel it
// immediately and proceed with this one.
if (mQrCodeAnimator != null) {
mQrCodeAnimator.cancel();
}
// Calculate the starting and ending bounds for the zoomed-in image.
// This step involves lots of math. Yay, math.
final Rect startBounds = new Rect();
final Rect finalBounds = new Rect();
final Point globalOffset = new Point();
// The start bounds are the global visible rectangle of the thumbnail,
// and the final bounds are the global visible rectangle of the container
// view. Also set the container view's offset as the origin for the
// bounds, since that's the origin for the positioning animation
// properties (X, Y).
mAddressQrCode.getGlobalVisibleRect(startBounds);
getActivity().findViewById(R.id.container)
.getGlobalVisibleRect(finalBounds, globalOffset);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScale;
if ((float) finalBounds.width() / finalBounds.height()
> (float) startBounds.width() / startBounds.height()) {
// Extend start bounds horizontally
startScale = (float) startBounds.height() / finalBounds.height();
float startWidth = startScale * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
} else {
// Extend start bounds vertically
startScale = (float) startBounds.width() / finalBounds.width();
float startHeight = startScale * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
}
// Hide the thumbnail and show the zoomed-in view. When the animation
// begins, it will position the zoomed-in view in the place of the
// thumbnail.
ViewHelper.setAlpha(mAddressQrCode, 0f);
mExpandedQrCode.setVisibility(View.VISIBLE);
// Set the pivot point for SCALE_X and SCALE_Y transformations
// to the top-left corner of the zoomed-in view (the default
// is the center of the view).
ViewHelper.setPivotX(mExpandedQrCode, 0f);
ViewHelper.setPivotY(mExpandedQrCode, 0f);
// Construct and run the parallel animation of the four translation and
// scale properties (X, Y, SCALE_X, and SCALE_Y).
AnimatorSet set = new AnimatorSet();
set
.play(ObjectAnimator.ofFloat(mExpandedQrCode, "x",
startBounds.left, finalBounds.left))
.with(ObjectAnimator.ofFloat(mExpandedQrCode, "y",
startBounds.top, finalBounds.top))
.with(ObjectAnimator.ofFloat(mExpandedQrCode, "scaleX",
startScale, 1f))
.with(ObjectAnimator.ofFloat(mExpandedQrCode, "scaleY",
startScale, 1f));
set.setDuration(mShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mQrCodeAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
mQrCodeAnimator = null;
}
});
set.start();
mQrCodeAnimator = set;
// Upon clicking the zoomed-in image, it should zoom back down
// to the original bounds and show the thumbnail instead of
// the expanded image.
final float startScaleFinal = startScale;
mExpandedQrCode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mQrCodeAnimator != null) {
mQrCodeAnimator.cancel();
}
// Animate the four positioning/sizing properties in parallel,
// back to their original values.
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator
.ofFloat(mExpandedQrCode, "x", startBounds.left))
.with(ObjectAnimator
.ofFloat(mExpandedQrCode,
"y", startBounds.top))
.with(ObjectAnimator
.ofFloat(mExpandedQrCode,
"scaleX", startScaleFinal))
.with(ObjectAnimator
.ofFloat(mExpandedQrCode,
"scaleY", startScaleFinal));
set.setDuration(mShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
ViewHelper.setAlpha(mAddressQrCode, 1f);
mExpandedQrCode.setVisibility(View.GONE);
mExpandedQrCode.setClickable(false);
mQrCodeAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
ViewHelper.setAlpha(mAddressQrCode, 1f);
mExpandedQrCode.setVisibility(View.GONE);
mExpandedQrCode.setClickable(false);
mQrCodeAnimator = null;
}
});
set.start();
mQrCodeAnimator = set;
}
});
}
}

View File

@ -0,0 +1,91 @@
package i2p.bote.android.widget;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.tokenautocomplete.TokenCompleteTextView;
import java.security.GeneralSecurityException;
import java.util.SortedSet;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.Person;
import i2p.bote.email.EmailDestination;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class ContactsCompletionView extends TokenCompleteTextView {
public ContactsCompletionView(Context context, AttributeSet attrs) {
super(context, attrs);
allowDuplicates(false);
}
@Override
protected View getViewForObject(Object object) {
Person person = (Person) object;
LayoutInflater l = (LayoutInflater)getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
LinearLayout view = (LinearLayout)l.inflate(R.layout.contact_token, (ViewGroup)ContactsCompletionView.this.getParent(), false);
((TextView)view.findViewById(R.id.contact_name)).setText(person.getName());
ImageView picView = (ImageView) view.findViewById(R.id.contact_picture);
Bitmap picture = person.getPicture();
if (picture == null) {
ViewGroup.LayoutParams lp = picView.getLayoutParams();
picture = BoteHelper.getIdenticonForAddress(person.getAddress(), lp.width, lp.height);
}
picView.setImageBitmap(picture);
return view;
}
@Override
protected Object defaultObject(String completionText) {
// Stupid simple example of guessing if we have an email or not
int index = completionText.indexOf('@');
if (index == -1) {
try {
// Check if it is a known Destination
Contact c = BoteHelper.getContact(completionText);
if (c != null)
return new Person(c.getName(), c.getBase64Dest(),
BoteHelper.decodePicture(c.getPictureBase64()));
// Check if it is a name
SortedSet<Contact> contacts = I2PBote.getInstance().getAddressBook().getAll();
for (Contact contact : contacts) {
if (contact.getName().startsWith(completionText))
return new Person(contact.getName(), contact.getBase64Dest(),
BoteHelper.decodePicture(contact.getPictureBase64()));
}
// Try as a new Destination
try {
new EmailDestination(completionText);
return new Person(completionText.substring(0, 5), completionText, null);
} catch (GeneralSecurityException e) {
// Not a valid Destination
// Assume the user meant an external address
completionText = completionText.replace(" ", "") + "@example.com";
return new Person(completionText, completionText, null, true);
}
} catch (PasswordException e) {
// TODO handle
completionText = completionText.replace(" ", "") + "@example.com";
return new Person(completionText, completionText, null, true);
}
} else {
return new Person(completionText, completionText, null, true);
}
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package i2p.bote.android.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}

View File

@ -0,0 +1,48 @@
package i2p.bote.android.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import com.mikepenz.iconics.IconicsDrawable;
import net.i2p.android.ext.floatingactionbutton.FloatingActionButton;
import i2p.bote.android.R;
public class IconicsFloatingActionButton extends FloatingActionButton {
public IconicsFloatingActionButton(Context context) {
this(context, null);
}
public IconicsFloatingActionButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public IconicsFloatingActionButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IconicsFloatingActionButton, 0, 0);
String iconName = a.getString(R.styleable.IconicsFloatingActionButton_ifab_icon);
if (iconName == null)
return;
IconicsDrawable icon = new IconicsDrawable(context, iconName);
int color = a.getColor(R.styleable.IconicsFloatingActionButton_ifab_color, 0);
if (color != 0)
icon.color(color);
int size = a.getDimensionPixelSize(R.styleable.IconicsFloatingActionButton_ifab_size, 0);
if (size != 0)
icon.sizePx(size);
int padding = a.getDimensionPixelSize(R.styleable.IconicsFloatingActionButton_ifab_padding, 0);
if (padding != 0)
icon.paddingPx(padding);
a.recycle();
setIconDrawable(icon);
}
}

View File

@ -0,0 +1,89 @@
package i2p.bote.android.widget;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import com.pnikosis.materialishprogress.ProgressWheel;
public class LoadingRecyclerView extends RecyclerView {
private View mLoadingView;
private ProgressWheel mLoadingWheel;
private boolean mLoading;
final private AdapterDataObserver observer = new AdapterDataObserver() {
@Override
public void onChanged() {
mLoading = false;
updateLoading();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mLoading = false;
updateLoading();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mLoading = false;
updateLoading();
}
};
public LoadingRecyclerView(Context context) {
super(context);
}
public LoadingRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LoadingRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private void updateLoading() {
if (mLoadingView != null) {
mLoadingView.setVisibility(mLoading ? VISIBLE : GONE);
setVisibility(mLoading ? GONE : VISIBLE);
if (mLoadingWheel != null) {
if (mLoading && !mLoadingWheel.isSpinning())
mLoadingWheel.spin();
else if (!mLoading && mLoadingWheel.isSpinning())
mLoadingWheel.stopSpinning();
}
}
}
@Override
public void setAdapter(Adapter adapter) {
final Adapter oldAdapter = getAdapter();
if (oldAdapter != null) {
oldAdapter.unregisterAdapterDataObserver(observer);
}
super.setAdapter(adapter);
if (adapter != null) {
adapter.registerAdapterDataObserver(observer);
}
}
/**
* Set the views to use for showing state.
* <p/>
* This method also sets the state to "loading".
*
* @param loadingView The view to show in place of the RecyclerView while loading.
* @param progressWheel The indeterminate ProgressWheel to spin while loading, if any.
*/
public void setLoadingView(View loadingView, ProgressWheel progressWheel) {
mLoadingView = loadingView;
mLoadingWheel = progressWheel;
setLoading(true);
}
public void setLoading(boolean loading) {
mLoading = loading;
updateLoading();
}
}

View File

@ -0,0 +1,87 @@
package i2p.bote.android.widget;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
/**
* A descendant of {@link android.support.v4.widget.SwipeRefreshLayout} which supports multiple
* child views triggering a refresh gesture. You set the views which can trigger the gesture via
* {@link #setSwipeableChildren(int...)}, providing it the child ids.
*/
public class MultiSwipeRefreshLayout extends SwipeRefreshLayout {
private View[] mSwipeableChildren;
public MultiSwipeRefreshLayout(Context context) {
super(context);
}
public MultiSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Set the children which can trigger a refresh by swiping down when they are visible. These
* views need to be a descendant of this view.
*/
public void setSwipeableChildren(final int... ids) {
assert ids != null;
// Iterate through the ids and find the Views
mSwipeableChildren = new View[ids.length];
for (int i = 0; i < ids.length; i++) {
mSwipeableChildren[i] = findViewById(ids[i]);
}
}
/**
* This method controls when the swipe-to-refresh gesture is triggered. By returning false here
* we are signifying that the view is in a state where a refresh gesture can start.
*
* <p>As {@link android.support.v4.widget.SwipeRefreshLayout} only supports one direct child by
* default, we need to manually iterate through our swipeable children to see if any are in a
* state to trigger the gesture. If so we return false to start the gesture.
*/
@Override
public boolean canChildScrollUp() {
if (mSwipeableChildren != null && mSwipeableChildren.length > 0) {
// Iterate through the scrollable children and check if any of them can not scroll up
for (View view : mSwipeableChildren) {
if (view != null && view.isShown() && !canViewScrollUp(view)) {
// If the view is shown, and can not scroll upwards, return false and start the
// gesture.
return false;
}
}
}
return true;
}
/**
* Utility method to check whether a {@link View} can scroll up from it's current position.
* Handles platform version differences, providing backwards compatible functionality where
* needed.
*/
private static boolean canViewScrollUp(View view) {
if (android.os.Build.VERSION.SDK_INT >= 14) {
// For ICS and above we can call canScrollVertically() to determine this
return ViewCompat.canScrollVertically(view, -1);
} else {
if (view instanceof AbsListView) {
// Pre-ICS we need to manually check the first visible item and the child view's top
// value
final AbsListView listView = (AbsListView) view;
return listView.getChildCount() > 0 &&
(listView.getFirstVisiblePosition() > 0
|| listView.getChildAt(0).getTop() < listView.getPaddingTop());
} else {
// For all other view types we just check the getScrollY() value
return view.getScrollY() > 0;
}
}
}
}

View File

@ -0,0 +1,45 @@
package im.delight.android.identicons;
import android.graphics.Color;
public class Identicon extends IdenticonBase {
private static final int CENTER_COLUMN_INDEX = 5;
@Override
protected int getRowCount() {
return 9;
}
@Override
protected int getColumnCount() {
return 9;
}
protected int getSymmetricColumnIndex(int col) {
if (col < CENTER_COLUMN_INDEX) {
return col;
} else {
return getColumnCount() - col - 1;
}
}
@Override
protected boolean isCellVisible(int row, int column) {
return getByte(3 + row * CENTER_COLUMN_INDEX + getSymmetricColumnIndex(column)) >= 0;
}
@Override
protected int getIconColor() {
return Color.rgb(getByte(0) + 128, getByte(1) + 128, getByte(2) + 128);
}
@Override
protected int getBackgroundColor() {
float[] hsv = new float[3];
Color.colorToHSV(getIconColor(), hsv);
if (hsv[2] < 0.5)
return Color.parseColor("#ffeeeeee"); // @color/background_material_light
else
return Color.parseColor("#ff303030"); // @color/background_material_dark
}
}

Some files were not shown because too many files have changed in this diff Show More