propagate from branch 'i2p.i2p-bote' (head c1349d682349dd701b9f0b18624293e7acca9ee3)

to branch 'i2p.i2p-bote.gradle' (head ff5f091c01e0dee5a95632846781536d11ff6324)
This commit is contained in:
str4d
2017-01-20 18:13:21 +00:00
669 changed files with 30177 additions and 194 deletions

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

25
android/.tx/config Normal file
View File

@ -0,0 +1,25 @@
[main]
host = https://www.transifex.com
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.android_bote]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
minimum_perc = 50
[I2P.android_bote_help_start]
file_filter = app/src/main/res/raw-<lang>/help_start.html
source_file = app/src/main/res/raw/help_start.html
source_lang = en
type = HTML
minimum_perc = 50
[I2P.android_bote_help_identities]
file_filter = app/src/main/res/raw-<lang>/help_identities.html
source_file = app/src/main/res/raw/help_identities.html
source_lang = en
type = HTML
minimum_perc = 50

53
android/CHANGELOG Normal file
View File

@ -0,0 +1,53 @@
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

85
android/README.md Normal file
View File

@ -0,0 +1,85 @@
# I2P-Bote for Android
Bote is an Android port of I2P-Bote.
## Build process
### Dependencies:
- Java SDK (preferably Oracle/Sun or OpenJDK) 1.6.0 or higher
- Apache Ant 1.8.0 or higher
- [I2P source](https://github.com/i2p/i2p.i2p)
- [I2P-Bote source](https://github.com/i2p/i2p.i2p-bote)
- Android SDK 21
- Android Build Tools 21.1.1
- Android Support Repository
- Gradle 2.2.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
```
### Preparation
1. Install I2P. You need the installed libraries to build against.
2. Download the Android SDK. The simplest method is to download Android Studio.
3. Check out the `i2p.i2p-bote` repository.
4. Create a `local.properties` file in `i2p.i2p-bote/android` containing:
```
i2pbase=/path/to/installed/i2p
```
5. 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 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`

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

158
android/build.gradle Normal file
View File

@ -0,0 +1,158 @@
apply plugin: 'com.android.application'
apply plugin: 'witness'
android {
compileSdkVersion 22
buildToolsVersion '22.0.1'
defaultConfig {
versionCode 13
versionName '0.6'
minSdkVersion 10
targetSdkVersion 22
// 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
}
}
dependencies {
// Local dependencies
compile(project(':core')) {
// Replaced with Android-specific JavaMail
exclude group: 'com.sun.mail'
// BouncyCastle is replaced with SpongyCastle
exclude group: 'org.bouncycastle'
// We use the I2P Android client library instead to get native libs
exclude group: 'net.i2p'
exclude group: 'net.i2p.client'
}
compile project(':crypto')
compile fileTree(dir: 'libs', include: '*.jar')
// Android Support Repository dependencies
compile 'com.android.support:support-annotations:22.2.0'
compile 'com.android.support:support-v4:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:recyclerview-v7:22.2.0'
// Remote dependencies
compile 'com.sun.mail:android-mail:1.5.6'
compile 'com.sun.mail:android-activation:1.5.6'
compile 'net.i2p:router:0.9.27'
compile('net.i2p.android:client:0.9.27@aar') {
transitive = true
}
compile 'net.i2p.android:helper:0.9.1@aar'
compile 'net.i2p.android.ext:floatingactionbutton:1.9.0'
compile 'com.madgag.spongycastle:core:1.52.0.0'
compile 'com.madgag.spongycastle:prov:1.52.0.0'
compile('com.mcxiaoke.viewpagerindicator:library:2.4.1') {
exclude group: 'com.android.support', module: 'support-v4'
}
compile 'com.google.zxing:core:3.2.0'
compile 'com.google.zxing:android-integration:3.2.0'
compile 'com.androidplot:androidplot-core:0.6.1'
compile 'com.pnikosis:materialish-progress:1.5'
compile('com.android.support:support-v4-preferencefragment:1.0.0@aar') {
exclude module: 'support-v4'
}
compile 'com.mikepenz:iconics:1.0.2@aar'
compile('com.mikepenz:materialdrawer:3.0.7@aar') {
transitive = true
}
// Testing-only dependencies
androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile 'com.android.support.test:rules:0.3'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2'
}
dependencyVerification {
verify = [
'com.android.support:support-annotations:beac5cae60bdb597df9af9c916f785c2f71f8c8ae4be9a32d4298dea85496a42',
'com.android.support:support-v4:7bb6e40a18774aa2595e4d8f9fe0ae14e61670f71a1279272fb0b79b8be71180',
'com.android.support:appcompat-v7:2d5867698410b41f75140c91d6c1e58da74ae0f97baf6e0bdd1f7cc1017ceb2c',
'com.android.support:recyclerview-v7:3a8da14585fa1c81f06e7cef4d93a7641f0323d8f984ff9a7bd7a6e416b46888',
'com.sun.mail:android-activation:61c18cfff09374e90d5168daf40b3d50d77bed8bc0b071124c064ecc3b157506',
'com.sun.mail:android-mail:ab177c3119400c661a026febe0ed84b8661ca61dbde8cbaad1a3ab507d4e5fb8',
'net.i2p:router:74fa6ee310f5e089c0ccf7d9e844ccf0e180f23bf33e068778e7288aa9e6509a',
'net.i2p.android:client:88de00330c0f7a47adffb4b6f156e661c819df7073bbafe2d48249e38577df85',
'net.i2p.android:helper:a1087507fa28ef3570e6f6ad0169bc36f6160726a58c4ece39387bbc0249a1a0',
'net.i2p.android.ext:floatingactionbutton:b41eae5fe6be599e3fade00273521b0914f2e199d5f04c50fa34cfe935347f76',
'com.madgag.spongycastle:core:07a401edbe26e1028e2324754557b741cc57306008df7b71a9e12ec32d65be8f',
'com.madgag.spongycastle:prov:becbb70797b0103517693d2a97ce93174cc4d1f732897ed965a24e32dd99503e',
'com.mcxiaoke.viewpagerindicator:library:1e8aad664137f68abdfee94889f6da3dc98be652a235176a403965a07a25de62',
'com.google.zxing:core:7fe5a8ff437635a540e56317649937b768b454795ce999ed5f244f83373dee7b',
'com.google.zxing:android-integration:0de037a73138033c4a03cdbca5d5728ef65a026ffb89afce071105f43a98ee0e',
'com.androidplot:androidplot-core:777b54dd98b8dedc5f3fcc95018eece1188f6c692dcbd5b7744af175e15d70bd',
'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.mikepenz:iconics:c1a02203d8e0d638959463c00af3ab9096e0a7c1ad5928762eb10ef5ce8a63cd',
'com.mikepenz:materialdrawer:3331ec671630fc1b1a54c4ff4da9d2a0cee45a2c129dd7f2adb337fa814b9497',
]
}
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.

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

@ -0,0 +1,19 @@
-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 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,167 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="i2p.bote.android"
xmlns:android="http://schemas.android.com/apk/res/android">
<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"/>
<application
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=".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 @@
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,726 @@
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.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.accountswitcher.AccountHeader;
import com.mikepenz.materialdrawer.accountswitcher.AccountHeaderBuilder;
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 int 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(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem iDrawerItem) {
switch (iDrawerItem.getIdentifier()) {
case ID_ADDRESS_BOOK:
mDrawer.setSelection(mSelected, false);
mDrawer.closeDrawer();
Intent ai = new Intent(EmailListActivity.this, AddressBookActivity.class);
startActivity(ai);
return true;
case ID_NET_STATUS:
mDrawer.setSelection(mSelected, false);
netStatusSelected();
return true;
default:
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:
final int message = boteNotStartedMessage;
DialogFragment df = new DialogFragment() {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
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();
}
};
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();
}
//
// 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.updateFooterItem(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.setHtmlFromRawResource(getActivity(), R.raw.help_about_libraries, true);
// 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.setHtmlFromRawResource(getActivity(), getArguments().getInt(ARG_HTML_FILE), true);
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,243 @@
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.List;
import java.util.Set;
import i2p.bote.I2PBote;
import i2p.bote.network.BannedPeer;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.RelayPeer;
import static i2p.bote.Util._t;
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();
if (dhtStats != null) {
if (dhtStats.getData().size() == 0) {
Segment n = new Segment("", 100);
SegmentFormatter nf = new SegmentFormatter();
setupSegmentFormatter(nf);
nf.getFillPaint().setColor(getResources().getColor(android.R.color.darker_gray));
mKademliaPie.addSeries(n, nf);
} else {
int reachable = 0;
for (List<String> row : dhtStats.getData()) {
if (_t("No").equals(row.get(4)))
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();
setupSegmentFormatter(rf);
rf.getFillPaint().setColor(getResources().getColor(R.color.green));
mKademliaPie.addSeries(r, rf);
}
if (unreachable > 0) {
Segment u = new Segment(getString(R.string.unreachable), dhtStats.getData().size() - reachable);
SegmentFormatter uf = new SegmentFormatter();
setupSegmentFormatter(uf);
uf.getFillPaint().setColor(getResources().getColor(R.color.error_color));
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();
setupSegmentFormatter(nf);
nf.getFillPaint().setColor(getResources().getColor(android.R.color.darker_gray));
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();
setupSegmentFormatter(gf);
gf.getFillPaint().setColor(getResources().getColor(R.color.green));
mRelayPie.addSeries(g, gf);
}
if (bad > 0) {
Segment b = new Segment(getString(R.string.unreliable), bad);
SegmentFormatter bf = new SegmentFormatter();
setupSegmentFormatter(bf);
bf.getFillPaint().setColor(getResources().getColor(R.color.red));
mRelayPie.addSeries(b, bf);
}
if (untested > 0) {
Segment u = new Segment(getString(R.string.untested), untested);
SegmentFormatter uf = new SegmentFormatter();
setupSegmentFormatter(uf);
uf.getFillPaint().setColor(getResources().getColor(R.color.accent));
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,709 @@
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.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 (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 (DataFormatException 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,197 @@
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.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);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean passwordRequired = I2PBote.getInstance().isPasswordRequired();
mPromotedActions.setVisibility(passwordRequired ? View.GONE : View.VISIBLE);
}
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,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,80 @@
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();
}
// NFC send only works on API 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) {
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
protected 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.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.support.v4.preference.PreferenceFragment;
import i2p.bote.android.R;
import i2p.bote.android.widget.SummaryEditTextPreference;
public class AdvancedPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
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 PreferenceCategory i2pCat = (PreferenceCategory) findPreference("i2pCategory");
CheckBoxPreference routerAuto = (CheckBoxPreference) findPreference("i2pbote.router.auto");
if (!routerAuto.isChecked()) {
setupI2PCategory(getActivity(), i2pCat);
}
routerAuto.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Boolean checked = (Boolean) newValue;
if (!checked) {
setupI2PCategory(getActivity(), 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,41 @@
package i2p.bote.android.config;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.preference.Preference;
import android.support.v4.preference.PreferenceFragment;
import i2p.bote.android.R;
public class AppProtectionPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
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,36 @@
package i2p.bote.android.config;
import android.os.Bundle;
import android.support.v4.preference.PreferenceFragment;
import i2p.bote.android.R;
public class AppearancePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
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,48 @@
package i2p.bote.android.config;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.preference.PreferenceFragment;
import java.util.Map;
import i2p.bote.Configuration;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
public class NetworkPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
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,71 @@
package i2p.bote.android.config;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.v4.preference.PreferenceFragment;
import java.util.Map;
import i2p.bote.Configuration;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
public class PrivacyPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
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,234 @@
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 i2p.bote.I2PBote;
import i2p.bote.StatusListener;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.android.util.TaskFragment;
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, String> {
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) {
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);
}
}
}
private class PasswordWaiter extends RobustAsyncTask<String, String, String> {
protected String doInBackground(String... params) {
StatusListener lsnr = new StatusListener() {
public void updateStatus(String status) {
publishProgress(status);
}
};
try {
I2PBote.getInstance().changePassword(
params[0].getBytes(),
params[1].getBytes(),
params[2].getBytes(),
lsnr);
return null;
} catch (Throwable e) {
cancel(false);
return e.getMessage();
}
}
}
}

View File

@ -0,0 +1,179 @@
package i2p.bote.android.config;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.preference.PreferenceFragment;
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 PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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,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,410 @@
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.List;
import java.util.Properties;
import i2p.bote.I2PBote;
import i2p.bote.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;
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, String> {
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) {
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);
}
}
}
private class IdentityWaiter extends RobustAsyncTask<Object, String, String> {
protected String doInBackground(Object... params) {
StatusListener lsnr = new StatusListener() {
public void updateStatus(String status) {
publishProgress(status);
}
};
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("Saving identity");
I2PBote.getInstance().getIdentities().save();
return null;
} catch (Throwable e) {
cancel(false);
return e.getMessage();
}
}
}
}

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.settings, 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,49 @@
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.InitActivities;
import i2p.bote.android.R;
public class IdentityShipActivity extends BoteActivityBase implements
IdentityShipFragment.Callbacks {
public static final String EXPORTING = "exporting";
boolean mExporting;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_identity_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);
IdentityShipFragment f = IdentityShipFragment.newInstance(mExporting);
getSupportFragmentManager().beginTransaction()
.add(R.id.identity_ship_frag, f).commit();
}
}
// IdentityShipFragment.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,448 @@
package i2p.bote.android.identities;
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.FileDescriptor;
import java.io.FileNotFoundException;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.RobustAsyncTask;
import i2p.bote.android.util.TaskFragment;
import i2p.bote.fileencryption.PasswordException;
public abstract class IdentityShipFragment 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 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;
public static IdentityShipFragment newInstance(boolean exporting) {
return exporting ?
new ExportIdentitiesFragment() :
new ImportIdentitiesFragment();
}
@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 class ExportIdentitiesFragment extends IdentityShipFragment {
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_identities, 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);
}
});
}
private void exportIdentities(File exportFile, String password) {
setInterfaceEnabled(false);
mError.setText("");
ExportWaiterFrag f = ExportWaiterFrag.newInstance(exportFile, password);
f.setTask(new ExportWaiter());
f.setTargetFragment(ExportIdentitiesFragment.this, SHIP_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.waiter_frag, f, SHIP_WAITER_TAG)
.commit();
}
@Override
protected int getTitle() {
return R.string.export_identities;
}
@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),
};
}
}
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 IdentityShipFragment {
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_identities, 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);
i.setType("text/plain");
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);
}
}
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(new ImportWaiter());
f.setTargetFragment(ImportIdentitiesFragment.this, SHIP_WAITER);
getFragmentManager().beginTransaction()
.replace(R.id.waiter_frag, f, SHIP_WAITER_TAG)
.commit();
}
@Override
protected int getTitle() {
return R.string.import_identities;
}
@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),
};
}
}
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,80 @@
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();
}
// NFC send only works on API 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) {
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
protected 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,335 @@
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.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 java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashSet;
import java.util.List;
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();
I2PBote bote = I2PBote.getInstance();
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,101 @@
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);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
if (!returnCursor.moveToFirst())
throw new FileNotFoundException();
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,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,51 @@
package i2p.bote.android.util;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.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,62 @@
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.preference.PreferenceManager;
import android.text.TextUtils;
import java.util.Locale;
public class LocaleManager {
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 defaultLanguage = "zz";
String selectedLanguage = PreferenceManager.getDefaultSharedPreferences(context).getString(
"pref_language", defaultLanguage
);
String language[] = TextUtils.split(selectedLanguage, "_");
if (language[0].equals(defaultLanguage))
return Locale.getDefault();
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,419 @@
package i2p.bote.android.util;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
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.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
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 {
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();
protected 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 = new DialogFragment() {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(getDeleteAddressMessage())
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
onDeleteAddress();
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
return builder.create();
}
};
df.show(getActivity().getSupportFragmentManager(), "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,39 @@
package i2p.bote.android.widget;
import android.content.Context;
import android.preference.EditTextPreference;
import android.text.InputType;
import android.util.AttributeSet;
public class IntEditTextPreference extends EditTextPreference {
public IntEditTextPreference(Context context) {
super(context);
getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
public IntEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
@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,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,28 @@
package i2p.bote.android.widget;
import android.content.Context;
import android.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 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
}
}

View File

@ -0,0 +1,122 @@
package im.delight.android.identicons;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import java.security.MessageDigest;
public abstract class IdenticonBase {
private static final String HASH_ALGORITHM = "SHA-256";
private final int mRowCount;
private final int mColumnCount;
private final Paint mPaint;
private volatile int mCellWidth;
private volatile int mCellHeight;
private volatile byte[] mHash;
private volatile int[][] mColors;
private volatile boolean mReady;
public IdenticonBase() {
mRowCount = getRowCount();
mColumnCount = getColumnCount();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
public static byte[] getHash(String input) {
byte[] mHash;
// if the input was null
if (input == null) {
// we can't create a hash value and have nothing to show (draw to the view)
mHash = null;
}
// if the input was a proper string (non-null)
else {
// generate a hash from the string to get unique but deterministic byte values
try {
final MessageDigest digest = java.security.MessageDigest.getInstance(HASH_ALGORITHM);
digest.update(input.getBytes());
mHash = digest.digest();
} catch (Exception e) {
mHash = null;
}
}
return mHash;
}
protected void setupColors() {
mColors = new int[mRowCount][mColumnCount];
int colorVisible = getIconColor();
int colorInvisible = getBackgroundColor();
for (int r = 0; r < mRowCount; r++) {
for (int c = 0; c < mColumnCount; c++) {
if (isCellVisible(r, c)) {
mColors[r][c] = colorVisible;
} else {
mColors[r][c] = colorInvisible;
}
}
}
}
public void show(String input) {
if (input != null) {
mHash = IdenticonBase.getHash(input);
} else {
mHash = null;
}
// set up the cell colors according to the input that was provided via show(...)
setupColors();
// this view may now be drawn (and thus must be re-drawn)
mReady = true;
}
public byte getByte(int index) {
if (mHash == null) {
return -128;
} else {
return mHash[index % mHash.length];
}
}
abstract protected int getRowCount();
abstract protected int getColumnCount();
abstract protected boolean isCellVisible(int row, int column);
abstract protected int getIconColor();
protected int getBackgroundColor() {
return Color.TRANSPARENT;
}
public void updateSize(int w, int h) {
mCellWidth = w / mColumnCount;
mCellHeight = h / mRowCount;
}
public void draw(Canvas canvas) {
if (mReady) {
int x, y;
for (int r = 0; r < mRowCount; r++) {
for (int c = 0; c < mColumnCount; c++) {
x = mCellWidth * c;
y = mCellHeight * r;
mPaint.setColor(mColors[r][c]);
canvas.drawRect(x, y + mCellHeight, x + mCellWidth, y, mPaint);
}
}
}
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2013 Mohammed Lakkadshaw
*
* 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 org.sufficientlysecure.htmltextview;
import android.text.Editable;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.style.AlignmentSpan;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.TypefaceSpan;
import android.util.Log;
import org.xml.sax.XMLReader;
import java.util.Vector;
/**
* Some parts of this code are based on android.text.Html
*/
public class HtmlTagHandler implements Html.TagHandler {
private int mListItemCount = 0;
private Vector<String> mListParents = new Vector<>();
private static class Code {
}
private static class Center {
}
@Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
if (opening) {
// opening tag
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "opening, output: " + output.toString());
}
if (tag.equalsIgnoreCase("ul") || tag.equalsIgnoreCase("ol") || tag.equalsIgnoreCase("dd")) {
mListParents.add(tag);
mListItemCount = 0;
} else if (tag.equalsIgnoreCase("code")) {
start(output, new Code());
} else if (tag.equalsIgnoreCase("center")) {
start(output, new Center());
}
} else {
// closing tag
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "closing, output: " + output.toString());
}
if (tag.equalsIgnoreCase("ul") || tag.equalsIgnoreCase("ol") || tag.equalsIgnoreCase("dd")) {
mListParents.remove(tag);
mListItemCount = 0;
} else if (tag.equalsIgnoreCase("li")) {
handleListTag(output);
} else if (tag.equalsIgnoreCase("code")) {
end(output, Code.class, new TypefaceSpan("monospace"), false);
} else if (tag.equalsIgnoreCase("center")) {
end(output, Center.class, new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), true);
}
}
}
/**
* Mark the opening tag by using private classes
*
* @param output
* @param mark
*/
private void start(Editable output, Object mark) {
int len = output.length();
output.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "len: " + len);
}
}
private void end(Editable output, Class kind, Object repl, boolean paragraphStyle) {
Object obj = getLast(output, kind);
// start of the tag
int where = output.getSpanStart(obj);
// end of the tag
int len = output.length();
output.removeSpan(obj);
if (where != len) {
// paragraph styles like AlignmentSpan need to end with a new line!
if (paragraphStyle) {
output.append("\n");
len++;
}
output.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "where: " + where);
Log.d(HtmlTextView.TAG, "len: " + len);
}
}
/**
* Get last marked position of a specific tag kind (private class)
*
* @param text
* @param kind
* @return
*/
private Object getLast(Editable text, Class kind) {
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
for (int i = objs.length; i > 0; i--) {
if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) {
return objs[i - 1];
}
}
return null;
}
}
private void handleListTag(Editable output) {
if (mListParents.lastElement().equals("ul")) {
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
} else if (mListParents.lastElement().equals("ol")) {
mListItemCount++;
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.insert(start, mListItemCount + ". ");
output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start, output.length(), 0);
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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 org.sufficientlysecure.htmltextview;
import android.content.Context;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import java.io.InputStream;
public class HtmlTextView extends JellyBeanSpanFixTextView {
public static final String TAG = "HtmlTextView";
public static final boolean DEBUG = false;
public HtmlTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public HtmlTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HtmlTextView(Context context) {
super(context);
}
/**
* http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string
*
* @param is
* @return
*/
static private String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
/**
* Loads HTML from a raw resource, i.e., a HTML file in res/raw/.
* This allows translatable resource (e.g., res/raw-de/ for german).
* The containing HTML is parsed to Android's Spannable format and then displayed.
*
* @param context
* @param id for example: R.raw.help
*/
public void setHtmlFromRawResource(Context context, int id, boolean useLocalDrawables) {
// load html from html file from /res/raw
InputStream inputStreamText = context.getResources().openRawResource(id);
setHtmlFromString(convertStreamToString(inputStreamText), useLocalDrawables);
}
/**
* Parses String containing HTML to Android's Spannable format and displays it in this TextView.
*
* @param html String containing HTML, for example: "<b>Hello world!</b>"
*/
public void setHtmlFromString(String html, boolean useLocalDrawables) {
Html.ImageGetter imgGetter;
if (useLocalDrawables) {
imgGetter = new LocalImageGetter(getContext());
} else {
imgGetter = new UrlImageGetter(this, getContext());
}
// this uses Android's Html class for basic parsing, and HtmlTagHandler
setText(Html.fromHtml(html, imgGetter, new HtmlTagHandler()));
// make links work
setMovementMethod(LinkMovementMethod.getInstance());
// no flickering when clicking textview for Android < 4, but overriders color...
// text.setTextColor(getResources().getColor(android.R.color.secondary_text_dark_nodisable));
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2012 Pierre-Yves Ricau <py.ricau@gmail.com>
*
* 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 org.sufficientlysecure.htmltextview;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
/**
* <p/>
* A {@link android.widget.TextView} that insert spaces around its text spans where needed to prevent
* {@link IndexOutOfBoundsException} in {@link #onMeasure(int, int)} on Jelly Bean.
* <p/>
* When {@link #onMeasure(int, int)} throws an exception, we try to fix the text by adding spaces
* around spans, until it works again. We then try removing some of the added spans, to minimize the
* insertions.
* <p/>
* The fix is time consuming (a few ms, it depends on the size of your text), but it should only
* happen once per text change.
* <p/>
* See http://code.google.com/p/android/issues/detail?id=35466
*/
public class JellyBeanSpanFixTextView extends TextView {
private static class FixingResult {
public final boolean fixed;
public final List<Object> spansWithSpacesBefore;
public final List<Object> spansWithSpacesAfter;
public static FixingResult fixed(List<Object> spansWithSpacesBefore,
List<Object> spansWithSpacesAfter) {
return new FixingResult(true, spansWithSpacesBefore, spansWithSpacesAfter);
}
public static FixingResult notFixed() {
return new FixingResult(false, null, null);
}
private FixingResult(boolean fixed, List<Object> spansWithSpacesBefore,
List<Object> spansWithSpacesAfter) {
this.fixed = fixed;
this.spansWithSpacesBefore = spansWithSpacesBefore;
this.spansWithSpacesAfter = spansWithSpacesAfter;
}
}
public JellyBeanSpanFixTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public JellyBeanSpanFixTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public JellyBeanSpanFixTextView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} catch (IndexOutOfBoundsException e) {
fixOnMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* If possible, fixes the Spanned text by adding spaces around spans when needed.
*/
private void fixOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
CharSequence text = getText();
if (text instanceof Spanned) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
fixSpannedWithSpaces(builder, widthMeasureSpec, heightMeasureSpec);
} else {
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "The text isn't a Spanned");
}
fallbackToString(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* Add spaces around spans until the text is fixed, and then removes the unneeded spaces
*/
private void fixSpannedWithSpaces(SpannableStringBuilder builder, int widthMeasureSpec,
int heightMeasureSpec) {
long startFix = System.currentTimeMillis();
FixingResult result = addSpacesAroundSpansUntilFixed(builder, widthMeasureSpec,
heightMeasureSpec);
if (result.fixed) {
removeUnneededSpaces(widthMeasureSpec, heightMeasureSpec, builder, result);
} else {
fallbackToString(widthMeasureSpec, heightMeasureSpec);
}
if (HtmlTextView.DEBUG) {
long fixDuration = System.currentTimeMillis() - startFix;
Log.d(HtmlTextView.TAG, "fixSpannedWithSpaces() duration in ms: " + fixDuration);
}
}
private FixingResult addSpacesAroundSpansUntilFixed(SpannableStringBuilder builder,
int widthMeasureSpec, int heightMeasureSpec) {
Object[] spans = builder.getSpans(0, builder.length(), Object.class);
List<Object> spansWithSpacesBefore = new ArrayList<Object>(spans.length);
List<Object> spansWithSpacesAfter = new ArrayList<Object>(spans.length);
for (Object span : spans) {
int spanStart = builder.getSpanStart(span);
if (isNotSpace(builder, spanStart - 1)) {
builder.insert(spanStart, " ");
spansWithSpacesBefore.add(span);
}
int spanEnd = builder.getSpanEnd(span);
if (isNotSpace(builder, spanEnd)) {
builder.insert(spanEnd, " ");
spansWithSpacesAfter.add(span);
}
try {
setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
return FixingResult.fixed(spansWithSpacesBefore, spansWithSpacesAfter);
} catch (IndexOutOfBoundsException notFixed) {
}
}
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "Could not fix the Spanned by adding spaces around spans");
}
return FixingResult.notFixed();
}
private boolean isNotSpace(CharSequence text, int where) {
if (where < 0) {
return true;
}
return text.charAt(where) != ' ';
}
private void setTextAndMeasure(CharSequence text, int widthMeasureSpec, int heightMeasureSpec) {
setText(text);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void removeUnneededSpaces(int widthMeasureSpec, int heightMeasureSpec,
SpannableStringBuilder builder, FixingResult result) {
for (Object span : result.spansWithSpacesAfter) {
int spanEnd = builder.getSpanEnd(span);
builder.delete(spanEnd, spanEnd + 1);
try {
setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
} catch (IndexOutOfBoundsException ignored) {
builder.insert(spanEnd, " ");
}
}
boolean needReset = true;
for (Object span : result.spansWithSpacesBefore) {
int spanStart = builder.getSpanStart(span);
builder.delete(spanStart - 1, spanStart);
try {
setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
needReset = false;
} catch (IndexOutOfBoundsException ignored) {
needReset = true;
int newSpanStart = spanStart - 1;
builder.insert(newSpanStart, " ");
}
}
if (needReset) {
setText(builder);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private void fallbackToString(int widthMeasureSpec, int heightMeasureSpec) {
if (HtmlTextView.DEBUG) {
Log.d(HtmlTextView.TAG, "Fallback to unspanned text");
}
String fallbackText = getText().toString();
setTextAndMeasure(fallbackText, widthMeasureSpec, heightMeasureSpec);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2014 drawk
*
* 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 org.sufficientlysecure.htmltextview;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.util.Log;
/**
* Copied from http://stackoverflow.com/a/22298833
*/
public class LocalImageGetter implements Html.ImageGetter {
Context c;
public LocalImageGetter(Context c) {
this.c = c;
}
public Drawable getDrawable(String source) {
int id = c.getResources().getIdentifier(source, "drawable", c.getPackageName());
if (id == 0) {
// the drawable resource wasn't found in our package, maybe it is a stock android drawable?
id = c.getResources().getIdentifier(source, "drawable", "android");
}
if (id == 0) {
// prevent a crash if the resource still can't be found
Log.e(HtmlTextView.TAG, "source could not be found: " + source);
return null;
} else {
Drawable d = c.getResources().getDrawable(id);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
return d;
}
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright (C) 2013 Antarix Tandon
*
* 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 org.sufficientlysecure.htmltextview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.text.Html.ImageGetter;
import android.view.View;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
public class UrlImageGetter implements ImageGetter {
Context c;
View container;
/**
* Construct the URLImageParser which will execute AsyncTask and refresh the container
*
* @param t
* @param c
*/
public UrlImageGetter(View t, Context c) {
this.c = c;
this.container = t;
}
public Drawable getDrawable(String source) {
UrlDrawable urlDrawable = new UrlDrawable();
// get the actual source
ImageGetterAsyncTask asyncTask = new ImageGetterAsyncTask(urlDrawable);
asyncTask.execute(source);
// return reference to URLDrawable which will asynchronously load the image specified in the src tag
return urlDrawable;
}
public class ImageGetterAsyncTask extends AsyncTask<String, Void, Drawable> {
UrlDrawable urlDrawable;
public ImageGetterAsyncTask(UrlDrawable d) {
this.urlDrawable = d;
}
@Override
protected Drawable doInBackground(String... params) {
String source = params[0];
return fetchDrawable(source);
}
@Override
protected void onPostExecute(Drawable result) {
// set the correct bound according to the result from HTTP call
urlDrawable.setBounds(0, 0, result.getIntrinsicWidth(), result.getIntrinsicHeight());
// change the reference of the current drawable to the result from the HTTP call
urlDrawable.drawable = result;
// redraw the image by invalidating the container
UrlImageGetter.this.container.invalidate();
}
/**
* Get the Drawable from URL
*
* @param urlString
* @return
*/
public Drawable fetchDrawable(String urlString) {
try {
InputStream is = fetch(urlString);
Drawable drawable = Drawable.createFromStream(is, "src");
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
return drawable;
} catch (Exception e) {
return null;
}
}
private InputStream fetch(String urlString) throws MalformedURLException, IOException {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet(urlString);
HttpResponse response = httpClient.execute(request);
return response.getEntity().getContent();
}
}
@SuppressWarnings("deprecation")
public class UrlDrawable extends BitmapDrawable {
protected Drawable drawable;
@Override
public void draw(Canvas canvas) {
// override the draw to facilitate refresh function later
if (drawable != null) {
drawable.draw(canvas);
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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