propagate from branch 'i2p.i2p-bote' (head c1349d682349dd701b9f0b18624293e7acca9ee3)
to branch 'i2p.i2p-bote.gradle' (head ff5f091c01e0dee5a95632846781536d11ff6324)
23
.mtn-ignore
@ -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
@ -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
@ -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
@ -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
@ -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
|
382
android/art/bote-feature.svg
Normal 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 |
296
android/art/ic_launcher_master.svg
Normal 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 |
239
android/art/ic_notif_master.svg
Normal 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 |
260
android/art/ic_notif_premaster.svg
Normal 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 |
94
android/art/icons/ic_scan_qr_code_24px.svg
Normal 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
After Width: | Height: | Size: 215 KiB |
232
android/art/intro_3.svg
Normal 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
After Width: | Height: | Size: 326 KiB |
158
android/build.gradle
Normal 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"
|
BIN
android/libs/additionnal.jar
Normal file
BIN
android/libs/tokenautocomplete.jar
Normal file
19
android/proguard-rules.pro
vendored
Normal 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[]);
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
4
android/src/debug/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Bote DEBUG</string>
|
||||
</resources>
|
167
android/src/main/AndroidManifest.xml
Normal 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>
|
44
android/src/main/java/i2p/bote/android/BoteActivityBase.java
Normal 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);
|
||||
}
|
||||
}
|
16
android/src/main/java/i2p/bote/android/Constants.java
Normal 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;
|
||||
}
|
726
android/src/main/java/i2p/bote/android/EmailListActivity.java
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
319
android/src/main/java/i2p/bote/android/EmailListAdapter.java
Normal 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();
|
||||
}
|
||||
}
|
577
android/src/main/java/i2p/bote/android/EmailListFragment.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
98
android/src/main/java/i2p/bote/android/HelpActivity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
35
android/src/main/java/i2p/bote/android/HelpHtmlFragment.java
Normal 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;
|
||||
}
|
||||
}
|
34
android/src/main/java/i2p/bote/android/InitActivities.java
Normal 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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
243
android/src/main/java/i2p/bote/android/NetworkInfoFragment.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
57
android/src/main/java/i2p/bote/android/NewEmailActivity.java
Normal 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();
|
||||
}
|
||||
}
|
709
android/src/main/java/i2p/bote/android/NewEmailFragment.java
Normal 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();
|
||||
}
|
||||
}
|
215
android/src/main/java/i2p/bote/android/ViewEmailActivity.java
Normal 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);
|
||||
}
|
||||
}
|
324
android/src/main/java/i2p/bote/android/ViewEmailFragment.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
153
android/src/main/java/i2p/bote/android/intro/IntroActivity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
164
android/src/main/java/i2p/bote/android/intro/SetupActivity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
335
android/src/main/java/i2p/bote/android/service/BoteService.java
Normal 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());
|
||||
}
|
||||
}
|
194
android/src/main/java/i2p/bote/android/service/Init.java
Normal 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) {}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
618
android/src/main/java/i2p/bote/android/util/BoteHelper.java
Normal 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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
29
android/src/main/java/i2p/bote/android/util/Person.java
Normal 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; }
|
||||
}
|
78
android/src/main/java/i2p/bote/android/util/QrCodeUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
android/src/main/jniLibs/armeabi-v7a/libscrypt.so
Executable file
BIN
android/src/main/jniLibs/armeabi/libscrypt.so
Normal file
BIN
android/src/main/jniLibs/mips/libscrypt.so
Executable file
BIN
android/src/main/jniLibs/x86/libscrypt.so
Executable file
BIN
android/src/main/res/drawable-hdpi/ic_contact_picture.png
Normal file
After Width: | Height: | Size: 1.1 KiB |