diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..82239ab
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+- niccokunzmann
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: Fruit-Radar-Development # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..2884129
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,17 @@
+name: Android CI
+
+on: [push]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Build with Gradle
+ run: ./gradlew build
diff --git a/.gitignore b/.gitignore
index 83f7c0a..f0249bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,7 @@
/gen/
/build/
/private/
-/.idea/
\ No newline at end of file
+/.idea/
+/coloring-book.iml
+local.properties
+connect.sh
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..54e852c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# Coloring Book
+[](https://github.com/niccokunzmann/androidsoft-coloring/actions?query=workflow%3A%22Android+CI%22)
+
+
+
+## Download
+
+[
](https://f-droid.org/packages/eu.quelltext.coloring/)
+
+## Contribution
+
+- You can translate the app on [Transifex](https://www.transifex.com/mundraub-android/coloring-book/)
+- You can donate with [Liberapay](https://liberapay.com/Fruit-Radar-Development)
+
+## Development
+
+This app is developed using Android Studio.
+
+You are welcome to contribute!
+- Translate the app - [read the Documentation]
+- Add features, see HowTo.
+- Solve [issues].
+
+If so and you need help, do not hesitate to open an [issue][issues] to ask!
+If you like to maintain this app, please leave a note.
+
+## How To
+
+### Add more pictures to paint
+
+1. Open Android Studio
+2. Click right on `res` > `drawable`
+3. Click right > `New` > `Image Asset`
+4. Make sure to name your image asset starting with `outline` followed by the
+ number, underscore and name.
+ Also, we do need PNG images of size 600x480, so the app stays small.
+
+### Translate the app
+
+- Go to [Transifex] and [read the Documentation].
+- If you do not find your language, you are invited to request it!
+- The updated version of the translations will be pushed automatically to the master branch of the repository.
+
+## License
+
+This software is open-source under the [GPLv3](LICENSE).
+
+## Credits
+
+- see the app's credits/about page
+- [androidsoft-lib-utils](https://github.com/niccokunzmann/androidsoft-lib-utils)
+- [comic clustering](https://github.com/niccokunzmann/comic-cluster)
+
+## Components
+- [coloring-book-gallery](https://github.com/niccokunzmann/coloring-book-gallery)
+- [coloringbook-lib-utils](https://github.com/niccokunzmann/coloringbook-lib-utils)
+- [androidsoft-lib-credits](https://github.com/androidsoft-org/androidsoft-lib-credits/)
+- [Weka-for-Android](https://github.com/rjmarsan/Weka-for-Android)
+
+[issues]: https://github.com/niccokunzmann/androidsoft-coloring/issues
+[Transifex]: https://www.transifex.com/mundraub-android/coloring-book/dashboard/
+[read the Documentation]: documentation/README.md#readme
+
diff --git a/build.gradle b/build.gradle
index 1edd836..4ec4c48 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,41 +1,49 @@
-task wrapper(type: Wrapper) {
- gradleVersion = '2.3'
-}
-
-
buildscript {
repositories {
//mavenCentral()
+ google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.1.0'
+ classpath 'com.android.tools.build:gradle:3.4.2'
}
}
repositories {
//mavenCentral()
jcenter()
+ google()
}
apply plugin: 'com.android.application'
dependencies {
- compile fileTree( dir: 'libs', include:'*.jar' )
+ implementation fileTree(dir: 'libs', include: '*.jar')
+ implementation 'commons-io:commons-io:2.4'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ testImplementation 'junit:junit:4.12'
}
android {
- compileSdkVersion 19
+ compileSdkVersion 29
- buildToolsVersion "19.1.0"
lintOptions {
enable 'UnusedIds', 'EasterEgg' //, 'NewerVersionAvailable'
+ disable 'MissingTranslation' // see https://stackoverflow.com/a/32676398
}
apply from: 'sign.gradle'
-}
-
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 29
+ }
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+}
diff --git a/changelogs b/changelogs
new file mode 120000
index 0000000..ceae224
--- /dev/null
+++ b/changelogs
@@ -0,0 +1 @@
+metadata/en/changelogs/
\ No newline at end of file
diff --git a/copy-resource-to-app.sh b/copy-resource-to-app.sh
new file mode 100755
index 0000000..ae2d8d6
--- /dev/null
+++ b/copy-resource-to-app.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+for i in `find /home/user/Android/Sdk/platforms/android-29/data/res/ | grep $1.png`; do
+ mkdir -p src/main/res/`basename \`dirname $i\``
+ cp $i src/main/res/`basename \`dirname $i\``/$1.png
+done
diff --git a/documentation/.README.md.swp b/documentation/.README.md.swp
new file mode 100644
index 0000000..699bbb4
Binary files /dev/null and b/documentation/.README.md.swp differ
diff --git a/documentation/README.md b/documentation/README.md
new file mode 100644
index 0000000..9396b7b
--- /dev/null
+++ b/documentation/README.md
@@ -0,0 +1,109 @@
+# Documentation
+
+Here resides the documentation on how to go about this repository.
+
+## Adding Images
+
+The easiest way to add images to the app is to fork the [gallery] and
+add some images there.
+You can request to add the gallery to the app by adding the URL to
+the [Settings].
+Galleries can also be added manually to the app in the settings
+of the app.
+
+If you fork the gallery, do not worry: Images will not be displayed twice.
+They have a unique name depending on the folder they are in.
+
+If you request to add new images to the gallery,
+please make sure they are not violent, frightening or in other ways
+controversal as kids should be able to use the app unattended.
+Best if you ask your child which images it would like to paint.
+I am happy to inlcude these, too.
+
+What to keep in mind when creating an image:
+- Added images should be `600px` wide.
+- They consist of black and white pixels.
+ While it is possible to already add colors, they may be
+ transformed to black and white by the app and only be visible as preview.
+- The outlines should be `6` to `10` pixels wide.
+ This allows scaling down images for fast drawing on smaller phones.
+ If they are too small, it might be that areas are joined i.e.
+ coloring the head will also color the background.
+
+
+If an app contains many galleries, the user defined galleries are
+more important than the built-in galleries.
+
+Related:
+- [Issue 90](https://github.com/niccokunzmann/coloring-book/issues/90)
+
+[Settings]: ../src/main/java/org/androidsoft/coloring/util/Settings.java
+[gallery]: https://gallery.quelltext.eu
+
+## Translations
+
+Translations are done on [Transifex]. You must request to join the
+team and help translate. Whenever a file is translated 100%,
+a new version is created as a commmit.
+- [view all commits](https://github.com/niccokunzmann/coloring-book/commits/master)
+- [see example commit](https://github.com/niccokunzmann/coloring-book/commit/1b081c0d905b615f340b48bf90487dabdf09ea24)
+
+If you do not manage to translate 100% but want to have it included
+in the next release, please open an issue.
+We can pull the translations then.
+
+### Create a new release
+
+Changes go to the master branch of the app.
+Follow this process to publish the latest version.
+
+1. Check that the tests are running. [](https://github.com/niccokunzmann/androidsoft-coloring/actions?query=workflow%3A%22Android+CI%22)
+2. Fetch all the tags from this repository.
+ ```
+ git fetch --tags origin
+ ```
+2. List the releases.
+ ```
+ git tag
+ ```
+3. See the changes since the latest release
+ ```
+ git diff v1.1.6 HEAD
+ ```
+ or the commits - you should see the tags in the commit history.
+ ```
+ git log
+ ```
+4. Edit [src/main/AndroidManifest.xml](src/main/AndroidManifest.xml) and increase the `versionCode` and the `versionName`.
+5. Create or edit the file for the changes in the [metadata/en/changelogs/](metadata/en/changelogs) folder with the number of the `versionCode`.
+ Make sure the changelog file includes the relevant changes:
+ - added/removed/improved features
+ - changes in language
+ - changes in permissions
+6. Create a commit with the changes, named `version `, tag it as `v` and push it as branch and tag
+ ```
+ git checkout master
+ git add src/main/AndroidManifest.xml metadata/en/changelogs/
+ git commit -m"version 1.1.5"
+ git tag v1.1.5
+ git push
+ git push origin v1.1.5
+ ```
+
+
+## Screenshots
+
+The screen shots of the app reside in the `metadata//images`
+folder.
+
+Documentation:
+- [Google for resolution](https://support.google.com/googleplay/android-developer/answer/1078870?hl=en)
+- [fastlane for naming](https://docs.fastlane.tools/actions/upload_to_play_store/#images-and-screenshots)
+- [Fdroid for location](https://fdroid.gitlab.io/fdroid-website/docs/All_About_Descriptions_Graphics_and_Screenshots/)
+
+
+
+
+
+
+[Transifex]: https://www.transifex.com/mundraub-android/coloring-book/dashboard/
diff --git a/documentation/transifex-github-integration.yml b/documentation/transifex-github-integration.yml
new file mode 100644
index 0000000..fbecf1d
--- /dev/null
+++ b/documentation/transifex-github-integration.yml
@@ -0,0 +1,35 @@
+filters:
+ - filter_type: file
+ file_format: TXT
+ source_language: en-US
+ source_file: metadata/en/short_description.txt
+ translation_files_expression: 'metadata//short_description.txt'
+ - filter_type: file
+ file_format: TXT
+ source_language: en-US
+ source_file: metadata/en/title.txt
+ translation_files_expression: 'metadata//title.txt'
+ - filter_type: file
+ file_format: GITHUBMARKDOWN
+ source_language: en-US
+ source_file: metadata/en/full_description.txt
+ translation_files_expression: 'metadata//full_description.txt'
+ - filter_type: file
+ file_format: ANDROID
+ source_language: en-US
+ source_file: src/main/res/values/strings.xml
+ translation_files_expression: 'src/main/res/values-/strings.xml'
+ - filter_type: file
+ file_format: ANDROID
+ source_language: en-US
+ source_file: src/main/res/values/credits.xml
+ translation_files_expression: 'src/main/res/values-/credits.xml'
+ - filter_type: dir
+ file_format: GITHUBMARKDOWN
+ source_file_extension: txt
+ source_language: en-US
+ source_file_dir: metadata/en/changelogs/
+ translation_files_expression: 'metadata//changelogs/'
+settings:
+ language_mapping:
+ pt-BR: pt-rBR
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..5465fec
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+android.enableJetifier=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 78e3e68..1f81168 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue May 05 23:10:10 CST 2015
+#Tue Feb 25 11:09:43 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/images/download.svg b/images/download.svg
new file mode 100644
index 0000000..bdf5dc1
--- /dev/null
+++ b/images/download.svg
@@ -0,0 +1,93 @@
+
+
+
+
diff --git a/images/logo-round.png b/images/logo-round.png
new file mode 100644
index 0000000..05be0c1
Binary files /dev/null and b/images/logo-round.png differ
diff --git a/images/logo-round.svg b/images/logo-round.svg
new file mode 100644
index 0000000..d67711e
--- /dev/null
+++ b/images/logo-round.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/images/logo.png b/images/logo.png
new file mode 100644
index 0000000..ca47dc9
Binary files /dev/null and b/images/logo.png differ
diff --git a/images/logo.svg b/images/logo.svg
new file mode 100644
index 0000000..6d0e2be
--- /dev/null
+++ b/images/logo.svg
@@ -0,0 +1,174 @@
+
+
+
+
diff --git a/libs/androidsoft-utils-1.0.0.jar b/libs/androidsoft-utils-1.0.0.jar
deleted file mode 100644
index f932fa2..0000000
Binary files a/libs/androidsoft-utils-1.0.0.jar and /dev/null differ
diff --git a/libs/androidsoft-utils-1.0.1.jar b/libs/androidsoft-utils-1.0.1.jar
new file mode 100644
index 0000000..da63d81
Binary files /dev/null and b/libs/androidsoft-utils-1.0.1.jar differ
diff --git a/libs/wekaSTRIPPED.jar b/libs/wekaSTRIPPED.jar
new file mode 100644
index 0000000..f151cc0
Binary files /dev/null and b/libs/wekaSTRIPPED.jar differ
diff --git a/libs/wekaSTRIPPED.jar.txt b/libs/wekaSTRIPPED.jar.txt
new file mode 100644
index 0000000..3d97a21
--- /dev/null
+++ b/libs/wekaSTRIPPED.jar.txt
@@ -0,0 +1 @@
+This file was downloaded from https://github.com/rjmarsan/Weka-for-Android
diff --git a/local.properties b/local.properties
deleted file mode 100644
index d92d457..0000000
--- a/local.properties
+++ /dev/null
@@ -1,11 +0,0 @@
-## This file is automatically generated by Android Studio.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file must *NOT* be checked into Version Control Systems,
-# as it contains information specific to your local configuration.
-#
-# Location of the SDK. This is only used by Gradle.
-# For customization when using a Version Control System, please read the
-# header note.
-#Tue May 05 20:39:57 CST 2015
-sdk.dir=/opt/android-sdk
diff --git a/metadata/bn/changelogs/2.txt b/metadata/bn/changelogs/2.txt
new file mode 100644
index 0000000..1592128
--- /dev/null
+++ b/metadata/bn/changelogs/2.txt
@@ -0,0 +1 @@
+ছোট ত্রুটিগুলো সংশধিত(আনইন্সটল সিনক্রনাইজেশন)
diff --git a/metadata/bn/changelogs/3.txt b/metadata/bn/changelogs/3.txt
new file mode 100644
index 0000000..14af3aa
--- /dev/null
+++ b/metadata/bn/changelogs/3.txt
@@ -0,0 +1,5 @@
+- পেইন্টিং একটিভিটি থেকে বের হতে দুইবার ক্লিক করুন
+- এপিকে ছোট করতে প্রিভিউ ইমেজ সরিয়ে ফেলুন
+- জার্মান এবং পর্তুগীজ অনুবাদ যুক্ত করুন
+- ফুলস্ক্রিন যুক্ত করুন
+- ছবি অ্যাপ এবং ক্লাসিফায়েড এ শেয়ারের করতে দিন
diff --git a/metadata/bn/short_description.txt b/metadata/bn/short_description.txt
new file mode 100644
index 0000000..515ffe5
--- /dev/null
+++ b/metadata/bn/short_description.txt
@@ -0,0 +1 @@
+কালারিং বুক - উদাহরণ এবং ফটো থেকে ছবি
\ No newline at end of file
diff --git a/metadata/bn/title.txt b/metadata/bn/title.txt
new file mode 100644
index 0000000..d40c3fe
--- /dev/null
+++ b/metadata/bn/title.txt
@@ -0,0 +1 @@
+কালারিং বুক
\ No newline at end of file
diff --git a/metadata/de-DE b/metadata/de-DE
new file mode 120000
index 0000000..c42e816
--- /dev/null
+++ b/metadata/de-DE
@@ -0,0 +1 @@
+de
\ No newline at end of file
diff --git a/metadata/de/changelogs/2.txt b/metadata/de/changelogs/2.txt
new file mode 100644
index 0000000..5296e30
--- /dev/null
+++ b/metadata/de/changelogs/2.txt
@@ -0,0 +1 @@
+Kleinere Fehler wurden behoben (Synchronisation bei der Deinstallation).
diff --git a/metadata/de/changelogs/3.txt b/metadata/de/changelogs/3.txt
new file mode 100644
index 0000000..cb6fc12
--- /dev/null
+++ b/metadata/de/changelogs/3.txt
@@ -0,0 +1,5 @@
+- doppeltes Klicken auf Zurück verlässt die Malaktivität
+- Vorschaubilder entfernt, um die APK-Datei kleiner zu machen
+- Deutsche und Portugisische Übersetzung
+- Vollbildmodus
+- Bilder können mit der App geteilt werden und werden klassifiziert
diff --git a/metadata/de/full_description.txt b/metadata/de/full_description.txt
new file mode 100644
index 0000000..fc3ec30
--- /dev/null
+++ b/metadata/de/full_description.txt
@@ -0,0 +1,7 @@
+Ein Malbuch für das Alter zwei und höher.
+Du kannst eine Farbe wählen und die Tiere, Pflanzen und Dinge damit bemalen.
+
+Es gibt eine Auswahl an Bildern und Farben.
+Du kannst das Bild auf deinem Gerät speichern oder per E-Mail oder in sozialen Netzwerken teilen.
+Wenn Du ein Bild an diese App sendest, wird es zu einem Malbuchbild mit weißen Flächen und schwarzen Rändern.
+So kannst Du handgemalte Bilder hinzufügen und Fotos von Autos, Häusern, Tieren und Leuten einfärben wie in einem Comic.
diff --git a/metadata/de/short_description.txt b/metadata/de/short_description.txt
new file mode 100644
index 0000000..377fce4
--- /dev/null
+++ b/metadata/de/short_description.txt
@@ -0,0 +1 @@
+Malbuch mit Bildern aus Beispielen und Fotos
\ No newline at end of file
diff --git a/metadata/de/title.txt b/metadata/de/title.txt
new file mode 100644
index 0000000..df7d2e8
--- /dev/null
+++ b/metadata/de/title.txt
@@ -0,0 +1 @@
+Malbuch
diff --git a/metadata/en-US b/metadata/en-US
new file mode 120000
index 0000000..2c4c454
--- /dev/null
+++ b/metadata/en-US
@@ -0,0 +1 @@
+en
\ No newline at end of file
diff --git a/metadata/en/changelogs/2.txt b/metadata/en/changelogs/2.txt
new file mode 100644
index 0000000..31f0c2b
--- /dev/null
+++ b/metadata/en/changelogs/2.txt
@@ -0,0 +1 @@
+Minor bugs fixes (uninstall synchronization).
diff --git a/metadata/en/changelogs/3.txt b/metadata/en/changelogs/3.txt
new file mode 100644
index 0000000..4973171
--- /dev/null
+++ b/metadata/en/changelogs/3.txt
@@ -0,0 +1,5 @@
+- add double click to exit painting activity
+- remove preview images to make the apk smaller
+- add Indonese, German and Brazilian Portuguese translations
+- add fullscreen mode
+- allow images to be shared with the app and classified
diff --git a/metadata/en/changelogs/4.txt b/metadata/en/changelogs/4.txt
new file mode 100644
index 0000000..de32469
--- /dev/null
+++ b/metadata/en/changelogs/4.txt
@@ -0,0 +1,16 @@
+- new: request permission to write to save and load images
+- fix bug: open shared image even if image was painted before
+- fix bug: show painted image in list of new images in full size
+- new: speed up removing color noise
+- new: add connected components to remove areas which can not be drawn
+- change: replace old paint view with new one, making development easier
+- fix bug: saved images are now not cut off but in the size they were painted
+- new: use AndroidX
+- new: replace saving image progress bar with toast
+- new: cannot save image twice
+- new: save image before replacing it with image from photo
+- new: show saved images in the list of choosable images
+- new: change color behind the buttons
+- new: rotate images so they best fit the drawing area
+- new: add gallery to get even more images and contribute new ones easily
+
diff --git a/metadata/en/changelogs/5.txt b/metadata/en/changelogs/5.txt
new file mode 100644
index 0000000..4efc588
--- /dev/null
+++ b/metadata/en/changelogs/5.txt
@@ -0,0 +1,12 @@
+- add gallery to browse more images and cache selected images on the device
+- if an error occurs loading an image, this is reported to the user
+- on older versions of Android, the status bar can not be activated when painting or choosing a picture or color.
+- add sections in the image listing and allow to choose them in the settings
+- fix bug: images were scaled incorrectly on Samsung Galaxy Node 8
+- fix bug: pictures have different heights in picture choice
+- fix bug: only pictures which can be painted are shown if no internet connection is available
+- new: make painting small areas easier by searching for them next to the click
+- fix bug: app crashed when folder of images was empty
+- translated app to Italian
+- partly translated app to Bengali
+
diff --git a/metadata/en/changelogs/6.txt b/metadata/en/changelogs/6.txt
new file mode 100644
index 0000000..7eb7c2b
--- /dev/null
+++ b/metadata/en/changelogs/6.txt
@@ -0,0 +1 @@
+- add Polish translations
diff --git a/metadata/en/changelogs/7.txt b/metadata/en/changelogs/7.txt
new file mode 100644
index 0000000..636f2db
--- /dev/null
+++ b/metadata/en/changelogs/7.txt
@@ -0,0 +1,2 @@
+- Add Hungarian language by Balázs Úr
+- Add Ukrainian language
diff --git a/metadata/en/changelogs/8.txt b/metadata/en/changelogs/8.txt
new file mode 100644
index 0000000..685aa36
--- /dev/null
+++ b/metadata/en/changelogs/8.txt
@@ -0,0 +1 @@
+- add French language translation by LoubiTek
diff --git a/metadata/en/full_description.txt b/metadata/en/full_description.txt
new file mode 100644
index 0000000..6d9b514
--- /dev/null
+++ b/metadata/en/full_description.txt
@@ -0,0 +1,12 @@
+A coloring book for kids at age two and above.
+You can choose a color and paint the animals, plants and things with a simple tapping.
+
+There is a variety of colors and pictures.
+You can save the image on your device or share it via email and in social networks.
+If you send an image to this app, it will be transformed to a coloring book image of black lines around white areas.
+This way you can add hand-drawn pictures or color pictures of cars, houses, animals, people and more in a comic style.
+
+You can add new pictures to the app by placing them in the current saving location.
+As you can choose from pictures you saved there, your own added pictures will show up when a new picture can be chosen.
+If the images on the app are not enough, you can choose from the online image gallery.
+This gallery is also open to you to contribute more images to the app.
diff --git a/metadata/en/images/featureGrafic.png b/metadata/en/images/featureGrafic.png
new file mode 100644
index 0000000..95ad960
Binary files /dev/null and b/metadata/en/images/featureGrafic.png differ
diff --git a/metadata/en/images/phoneScreenshots/01paint.png b/metadata/en/images/phoneScreenshots/01paint.png
new file mode 100644
index 0000000..fcb8b6d
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/01paint.png differ
diff --git a/metadata/en/images/phoneScreenshots/02choose.png b/metadata/en/images/phoneScreenshots/02choose.png
new file mode 100644
index 0000000..a780b0b
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/02choose.png differ
diff --git a/metadata/en/images/phoneScreenshots/03colors.png b/metadata/en/images/phoneScreenshots/03colors.png
new file mode 100644
index 0000000..7b32659
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/03colors.png differ
diff --git a/metadata/en/images/phoneScreenshots/04camera.png b/metadata/en/images/phoneScreenshots/04camera.png
new file mode 100644
index 0000000..b38ba6a
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/04camera.png differ
diff --git a/metadata/en/images/phoneScreenshots/05classify.png b/metadata/en/images/phoneScreenshots/05classify.png
new file mode 100644
index 0000000..d1d8a2e
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/05classify.png differ
diff --git a/metadata/en/images/phoneScreenshots/06paint.png b/metadata/en/images/phoneScreenshots/06paint.png
new file mode 100644
index 0000000..5592d93
Binary files /dev/null and b/metadata/en/images/phoneScreenshots/06paint.png differ
diff --git a/metadata/en/images/sevenInchScreenshots/01paint.png b/metadata/en/images/sevenInchScreenshots/01paint.png
new file mode 100644
index 0000000..a627568
Binary files /dev/null and b/metadata/en/images/sevenInchScreenshots/01paint.png differ
diff --git a/metadata/en/short_description.txt b/metadata/en/short_description.txt
new file mode 100644
index 0000000..8dd4fab
--- /dev/null
+++ b/metadata/en/short_description.txt
@@ -0,0 +1 @@
+Coloring book - pictures from examples and photos
diff --git a/metadata/en/title.txt b/metadata/en/title.txt
new file mode 100644
index 0000000..c3a9b4a
--- /dev/null
+++ b/metadata/en/title.txt
@@ -0,0 +1 @@
+Coloring Book
diff --git a/metadata/es-ES b/metadata/es-ES
new file mode 120000
index 0000000..6c43814
--- /dev/null
+++ b/metadata/es-ES
@@ -0,0 +1 @@
+es
\ No newline at end of file
diff --git a/metadata/fa/changelogs/2.txt b/metadata/fa/changelogs/2.txt
new file mode 100644
index 0000000..3bc4edd
--- /dev/null
+++ b/metadata/fa/changelogs/2.txt
@@ -0,0 +1 @@
+رفع باگهای جزئی (همسانسازی حذف نصب)
diff --git a/metadata/fa/changelogs/3.txt b/metadata/fa/changelogs/3.txt
new file mode 100644
index 0000000..93501fa
--- /dev/null
+++ b/metadata/fa/changelogs/3.txt
@@ -0,0 +1,5 @@
+- دوبار کلیک را برای خروج از نقاشی اضافه کنید
+- تصاویر پیشنمایش را حذف کنید تا پکیج apk کوچکتر شود
+- آلمانی و پرتقالی برزیل را اضافه کنید
+- حالت تمام صفحه را اضافه کنید
+- اجازه دهید تصاویر با اپلیکیشن به اشتراک گذاشته و طبقهبندی شوند
diff --git a/metadata/fr-FR b/metadata/fr-FR
new file mode 120000
index 0000000..717280a
--- /dev/null
+++ b/metadata/fr-FR
@@ -0,0 +1 @@
+fr
\ No newline at end of file
diff --git a/metadata/fr/changelogs/2.txt b/metadata/fr/changelogs/2.txt
new file mode 100644
index 0000000..e9b9a6c
--- /dev/null
+++ b/metadata/fr/changelogs/2.txt
@@ -0,0 +1 @@
+Corrections bogues mineur (synchronisation désinstallation)
diff --git a/metadata/fr/changelogs/3.txt b/metadata/fr/changelogs/3.txt
new file mode 100644
index 0000000..75702fa
--- /dev/null
+++ b/metadata/fr/changelogs/3.txt
@@ -0,0 +1,5 @@
+- ajouter un double-clic pour quitter l'activité de peinture
+- supprimer les images d'aperçu pour rendre l'apk plus petit
+- ajouter des traductions en allemand et en portugais brésilien
+- ajouter le mode plein écran
+- permettre aux images d'être partagées avec l'application et classées
diff --git a/metadata/fr/full_description.txt b/metadata/fr/full_description.txt
new file mode 100644
index 0000000..ba76ceb
--- /dev/null
+++ b/metadata/fr/full_description.txt
@@ -0,0 +1,7 @@
+Un livre de coloriage pour les enfants à partir de deux ans.
+Vous pouvez choisir une couleur et peindre les animaux, les plantes et les choses d'un simple tapotement.
+
+Il existe une variété de couleurs et d'images.
+Vous pouvez enregistrer l'image sur votre appareil ou la partager par e-mail et sur les réseaux sociaux.
+Si vous envoyez une image à cette application, elle sera transformée en une image de livre de coloriage de lignes noires autour des zones blanches.
+De cette façon, vous pouvez ajouter des images dessinées à la main ou des images en couleur de voitures, de maisons, d'animaux, de personnes et plus encore dans un style comique.
diff --git a/metadata/fr/short_description.txt b/metadata/fr/short_description.txt
new file mode 100644
index 0000000..a26d4eb
--- /dev/null
+++ b/metadata/fr/short_description.txt
@@ -0,0 +1 @@
+Livre de coloriage - images à partir d'exemples et de photos
\ No newline at end of file
diff --git a/metadata/fr/title.txt b/metadata/fr/title.txt
new file mode 100644
index 0000000..2ea6ae0
--- /dev/null
+++ b/metadata/fr/title.txt
@@ -0,0 +1 @@
+Livre de coloriage
\ No newline at end of file
diff --git a/metadata/hu/changelogs/2.txt b/metadata/hu/changelogs/2.txt
new file mode 100644
index 0000000..fb4c966
--- /dev/null
+++ b/metadata/hu/changelogs/2.txt
@@ -0,0 +1 @@
+Kisebb hibajavítások (eltávolítási szinkronizáció).
diff --git a/metadata/hu/changelogs/3.txt b/metadata/hu/changelogs/3.txt
new file mode 100644
index 0000000..e2c5714
--- /dev/null
+++ b/metadata/hu/changelogs/3.txt
@@ -0,0 +1,5 @@
+- Dupla kattintás hozzáadása a színezési tevékenységből való kilépéshez.
+- Előnézeti képek eltávolítása, hogy az APK kisebb legyen.
+- Német és brazíliai portugál fordítás hozzáadása.
+- Teljes képernyős mód hozzáadása.
+- Annak lehetővé tétele, hogy a képek megoszthatók legyenek az alkalmazással és osztályozva legyenek.
diff --git a/metadata/hu/full_description.txt b/metadata/hu/full_description.txt
new file mode 100644
index 0000000..14036d1
--- /dev/null
+++ b/metadata/hu/full_description.txt
@@ -0,0 +1,7 @@
+Színezőkönyv kétéves és annál idősebb gyermekeknek.
+Kiválaszthatsz egy színt, és kifestheted az állatokat, növényeket és egyéb dolgokat egy egyszerű koppintással.
+
+Sokféle szín és kép érhető el.
+Elmentheted a képet a készülékedre, vagy megoszthatod e-mailben és a közösségi hálózatokon.
+Ha küldesz egy képet ennek az alkalmazásnak, akkor az átalakul fehér területek körüli fekete vonalakból álló színezőkönyvképpé.
+Így kézzel rajzolt képeket vagy autókat, házakat, állatokat, embereket és további dolgokat ábrázoló színes képeket adhatsz hozzá képregénystílusban.
diff --git a/metadata/hu/short_description.txt b/metadata/hu/short_description.txt
new file mode 100644
index 0000000..8726144
--- /dev/null
+++ b/metadata/hu/short_description.txt
@@ -0,0 +1 @@
+Színezőkönyv – képek példákból és fényképekből
\ No newline at end of file
diff --git a/metadata/hu/title.txt b/metadata/hu/title.txt
new file mode 100644
index 0000000..f819a07
--- /dev/null
+++ b/metadata/hu/title.txt
@@ -0,0 +1 @@
+Színezőkönyv
\ No newline at end of file
diff --git a/metadata/id/changelogs/2.txt b/metadata/id/changelogs/2.txt
new file mode 100644
index 0000000..ca531e6
--- /dev/null
+++ b/metadata/id/changelogs/2.txt
@@ -0,0 +1 @@
+Bug minor diperbaiki (hapus instalan sinkronisasi)
diff --git a/metadata/id/changelogs/3.txt b/metadata/id/changelogs/3.txt
new file mode 100644
index 0000000..cd8ebc4
--- /dev/null
+++ b/metadata/id/changelogs/3.txt
@@ -0,0 +1,5 @@
+- tambah klik ganda untuk keluar aktivitas melukis
+- hapus gambar pratinjau untuk membuat aplikasi lebih kecil
+- tambah bahasa Jerman dan Portugis Brazil
+- tambahkan mode fullscreen
+- izinkan gambar dibagikan dengan aplikasi dan diklarifikasi
diff --git a/metadata/id/full_description.txt b/metadata/id/full_description.txt
new file mode 100644
index 0000000..e63e183
--- /dev/null
+++ b/metadata/id/full_description.txt
@@ -0,0 +1,7 @@
+Buku mewarnai untuk anak usia dua tahun keatas.
+Kamu bisa memilih sebuah warna dan melukis binatang, tumbuhan, dan benda- benda dengan ketukan mudah.
+
+Ada berbagai macam warna dan gambar.
+Kamu bisa menyimpan gambar di perangkatmu atau membaginya lewat email dan jejaring sosial.
+Jika kamu mengirim sebuah gambar ke aplikasi ini, gambar akan berubah menjadi sebuah buku berwarna dari garis hitam di sekitar area putih.
+Dengan ini kamu bisa menambahkan gambar buatan tangan atau gambar berwarna dari mobil, rumah, binatang, orang, dan masih banyak lagi berupa gambar komik.
diff --git a/metadata/id/short_description.txt b/metadata/id/short_description.txt
new file mode 100644
index 0000000..22647cb
--- /dev/null
+++ b/metadata/id/short_description.txt
@@ -0,0 +1 @@
+Coloring book- gambar dari contoh dan foto
\ No newline at end of file
diff --git a/metadata/it/changelogs/2.txt b/metadata/it/changelogs/2.txt
new file mode 100644
index 0000000..b68388e
--- /dev/null
+++ b/metadata/it/changelogs/2.txt
@@ -0,0 +1 @@
+Sistemati bug minori (rimossa la sincronizzazione)
diff --git a/metadata/it/changelogs/3.txt b/metadata/it/changelogs/3.txt
new file mode 100644
index 0000000..8036735
--- /dev/null
+++ b/metadata/it/changelogs/3.txt
@@ -0,0 +1,5 @@
+- aggiunto doppio click per uscire dall'attività di pittura
+- rimossa l'anteprima delle immagini per rendere il file APK più leggero
+- aggiunte le traduzioni in tedesco e portoghese brasiliano
+- aggiunta la modalità a schermo intero
+- aggiunta la possibilità di condividere e classificare le immagini
diff --git a/metadata/it/full_description.txt b/metadata/it/full_description.txt
new file mode 100644
index 0000000..2117bf7
--- /dev/null
+++ b/metadata/it/full_description.txt
@@ -0,0 +1,7 @@
+Un libro da colorare per i bambini dai due anni in su
+Puoi scegliere un colore e colorare gli animali, le piante e gli oggetti con un semplice tocco.
+
+C'è una varietà di colori e figure.
+Puoi salvare l'immagine sul tuo dispositivo o condividerla via email o sui social network.
+Se invii un'immagine a quest'app, sarà trasformata in un'immagine da colorare, fatta da linee bianche attorno ad aree bianche.
+In questo modo puoi aggiungere immagini fatte a mano o colorare immagini di auto, case, animali, persone e altro in uno stile comico.
diff --git a/metadata/it/short_description.txt b/metadata/it/short_description.txt
new file mode 100644
index 0000000..ad19ec4
--- /dev/null
+++ b/metadata/it/short_description.txt
@@ -0,0 +1 @@
+Libro da colorare - immagini da esempi e foto
\ No newline at end of file
diff --git a/metadata/it/title.txt b/metadata/it/title.txt
new file mode 100644
index 0000000..52d216b
--- /dev/null
+++ b/metadata/it/title.txt
@@ -0,0 +1 @@
+Libro da colorare
\ No newline at end of file
diff --git a/metadata/jp-JP b/metadata/jp-JP
new file mode 120000
index 0000000..d121a59
--- /dev/null
+++ b/metadata/jp-JP
@@ -0,0 +1 @@
+jp
\ No newline at end of file
diff --git a/metadata/link.sh b/metadata/link.sh
new file mode 100755
index 0000000..047aeb9
--- /dev/null
+++ b/metadata/link.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+#
+# Transifex creates stupid directories. Thus, we link them here.
+#
+
+set -e
+
+transifex=../documentation/transifex-github-integration.yml
+end="all ok, no errors"
+
+function linkToLanguage() {
+ local lang=$1
+ local country=$2
+ mkdir -p $lang
+ if [ -d $lang-$country ] && ! [ -L $lang-$country ]; then
+ echo "ERROR: clean up $lang-$country"
+ end=""
+ return
+ fi
+ rm -f $lang-$country $lang-r$country || true
+ ln -sT $lang $lang-$country
+}
+
+function linkToCountry() {
+ local lang=$1
+ local country=$2
+ mkdir -p $lang-r$country
+ if [ -d $lang-$country ] && ! [ -L $lang-$country ]; then
+ echo "ERROR: clean up $lang-$country"
+ end=""
+ return
+ fi
+ if [ -d $lang ] && ! [ -L $lang ]; then
+ echo "ERROR: clean up $lang"
+ end=""
+ return
+ fi
+ rm -f $lang $lang-$country || true
+ ln -sT $lang-r$country $lang-$country
+ ln -sT $lang-r$country $lang
+ if ! cat $transifex | grep -q "$lang-$country: $lang-r$country"; then
+ echo "ERROR: please update the transifex settings in $transifex to map $lang-$country: $lang-r$country"
+ end=""
+ fi
+}
+
+
+linkToLanguage de DE
+linkToCountry pt BR
+linkToLanguage fr FR
+linkToLanguage pl PL
+linkToLanguage jp JP
+linkToLanguage es ES
+linkToLanguage ru RU
+
+if [ -n "$end" ]; then
+ echo $end
+ ls -l
+ echo $end
+fi
+
diff --git a/metadata/pl-PL b/metadata/pl-PL
new file mode 120000
index 0000000..55239f3
--- /dev/null
+++ b/metadata/pl-PL
@@ -0,0 +1 @@
+pl
\ No newline at end of file
diff --git a/metadata/pl/changelogs/2.txt b/metadata/pl/changelogs/2.txt
new file mode 100644
index 0000000..3465f6e
--- /dev/null
+++ b/metadata/pl/changelogs/2.txt
@@ -0,0 +1 @@
+Drobne bugi naprawione (odinstaluj synchronizacje)
diff --git a/metadata/pl/changelogs/3.txt b/metadata/pl/changelogs/3.txt
new file mode 100644
index 0000000..c9c24fe
--- /dev/null
+++ b/metadata/pl/changelogs/3.txt
@@ -0,0 +1,5 @@
+- dodaj podwójne kliknięcie aby przestać malować
+- usuń poprzednie obrazki aby zrobić aplikację mniejszą
+- dodaj tłumaczenie Niemieckie oraz Brazylijskie, Portugalskie
+- dodaj moduł cały ekran
+- umożliwia udostępnianie zdjęć w aplikacji i ich klasyfikacje
diff --git a/metadata/pl/full_description.txt b/metadata/pl/full_description.txt
new file mode 100644
index 0000000..748ab9e
--- /dev/null
+++ b/metadata/pl/full_description.txt
@@ -0,0 +1,7 @@
+Kolorowanka dla dzieci dwu letnich lub większych.
+Możesz wybrać kolor i malować zwierzęta, rośliny oraz rzeczy za pomocą łatwego tapnięcia
+
+Istnieje tutaj wiele kolorów oraz obrazów.
+Możesz zapisać obrazek na twoim urządzeniu lub opublikować go przez emaila oraz na social mediach.
+Jeżeli wyślesz jakieś zdjęcie do tej aplikacji, zdjęcie zostanie przekształcony w obraz kolorowanki z czarnymi liniami wokół białych obszarów.
+W ten sposób można dodać ręczne rysowanie lub kolorowane obrazy samochodów, domków, zwierząt, ludzi i wiele więcej w stylu komiksowym.
diff --git a/metadata/pl/short_description.txt b/metadata/pl/short_description.txt
new file mode 100644
index 0000000..1c3f694
--- /dev/null
+++ b/metadata/pl/short_description.txt
@@ -0,0 +1 @@
+Kolorowanka - zdjęcia z przykładów oraz fotografii
\ No newline at end of file
diff --git a/metadata/pl/title.txt b/metadata/pl/title.txt
new file mode 100644
index 0000000..c2cb2f7
--- /dev/null
+++ b/metadata/pl/title.txt
@@ -0,0 +1 @@
+Kolorowanka
\ No newline at end of file
diff --git a/metadata/pt b/metadata/pt
new file mode 120000
index 0000000..67eaeaa
--- /dev/null
+++ b/metadata/pt
@@ -0,0 +1 @@
+pt-rBR
\ No newline at end of file
diff --git a/metadata/pt-BR b/metadata/pt-BR
new file mode 120000
index 0000000..67eaeaa
--- /dev/null
+++ b/metadata/pt-BR
@@ -0,0 +1 @@
+pt-rBR
\ No newline at end of file
diff --git a/metadata/pt-rBR/changelogs/2.txt b/metadata/pt-rBR/changelogs/2.txt
new file mode 100644
index 0000000..7d276af
--- /dev/null
+++ b/metadata/pt-rBR/changelogs/2.txt
@@ -0,0 +1 @@
+Pequenas correções de erros (sincronização da desinstalação).
diff --git a/metadata/pt-rBR/changelogs/3.txt b/metadata/pt-rBR/changelogs/3.txt
new file mode 100644
index 0000000..96e6e83
--- /dev/null
+++ b/metadata/pt-rBR/changelogs/3.txt
@@ -0,0 +1,5 @@
+- Duplo clique para sair da atividade de pintura
+- Remove as imagens em miniatura para tornar o apk menor
+- Adiciona as traduções para Alemão e Português do Brasil
+- Adiciona o modo tela inteira
+- Permite que as imagens sejam classificadas e compartilhadas com o aplicativo
diff --git a/metadata/pt-rBR/full_description.txt b/metadata/pt-rBR/full_description.txt
new file mode 100644
index 0000000..bb52068
--- /dev/null
+++ b/metadata/pt-rBR/full_description.txt
@@ -0,0 +1,7 @@
+Um livro de colorir para crianças acima de dois anos.
+Você pode escolher uma cor e pintar animais, plantas e coisas com um simples toque.
+
+Há uma variedade de cores e imagens.
+Você pode salvar a imagem no seu dispositivo ou compartilhá-la por email e redes sociais.
+Se você enviar uma imagem para este aplicativo, ela será transformada em uma imagem de livro para colorir com linhas pretas em torno de áreas brancas.
+Você pode adicionar imagens desenhadas à mão ou coloridas de carros, casas, animais, pessoas e muito mais em estilo de quadrinhos.
diff --git a/metadata/pt-rBR/short_description.txt b/metadata/pt-rBR/short_description.txt
new file mode 100644
index 0000000..06294fe
--- /dev/null
+++ b/metadata/pt-rBR/short_description.txt
@@ -0,0 +1 @@
+Livro para colorir - imagens de exemplos e fotos
\ No newline at end of file
diff --git a/metadata/pt-rBR/title.txt b/metadata/pt-rBR/title.txt
new file mode 100644
index 0000000..253aa70
--- /dev/null
+++ b/metadata/pt-rBR/title.txt
@@ -0,0 +1 @@
+Livro para Colorir
\ No newline at end of file
diff --git a/metadata/ru-RU b/metadata/ru-RU
new file mode 120000
index 0000000..adc719b
--- /dev/null
+++ b/metadata/ru-RU
@@ -0,0 +1 @@
+ru
\ No newline at end of file
diff --git a/metadata/ru/short_description.txt b/metadata/ru/short_description.txt
new file mode 100644
index 0000000..085357d
--- /dev/null
+++ b/metadata/ru/short_description.txt
@@ -0,0 +1 @@
+книжка с картинками и фотками для раскрашивания
\ No newline at end of file
diff --git a/metadata/ru/title.txt b/metadata/ru/title.txt
new file mode 100644
index 0000000..a7b886a
--- /dev/null
+++ b/metadata/ru/title.txt
@@ -0,0 +1 @@
+книжка с картинками для раскрашивания
\ No newline at end of file
diff --git a/metadata/uk/changelogs/2.txt b/metadata/uk/changelogs/2.txt
new file mode 100644
index 0000000..e61e765
--- /dev/null
+++ b/metadata/uk/changelogs/2.txt
@@ -0,0 +1 @@
+Другорядні виправлення вад (видалено синхронізацію).
diff --git a/metadata/uk/changelogs/3.txt b/metadata/uk/changelogs/3.txt
new file mode 100644
index 0000000..6feedb6
--- /dev/null
+++ b/metadata/uk/changelogs/3.txt
@@ -0,0 +1,5 @@
+- додано подвійне клацання, щоб припинити малярську діяльність
+- вилучено зображення попереднього перегляду для зменшення розміру apk
+- додано переклади німецькою і бразильською португальською
+- додано повноекранний режим
+- дозволено ділитися зображеннями з програмою і класами
diff --git a/metadata/uk/short_description.txt b/metadata/uk/short_description.txt
new file mode 100644
index 0000000..cac2fed
--- /dev/null
+++ b/metadata/uk/short_description.txt
@@ -0,0 +1 @@
+Coloring book - картинки з прикладів і фотографій
\ No newline at end of file
diff --git a/metadata/uk/title.txt b/metadata/uk/title.txt
new file mode 100644
index 0000000..abfff29
--- /dev/null
+++ b/metadata/uk/title.txt
@@ -0,0 +1 @@
+Coloring Book
\ No newline at end of file
diff --git a/private/cache/retriever/catalog.xml b/private/cache/retriever/catalog.xml
deleted file mode 100644
index e69de29..0000000
diff --git a/releases/androidsoft-coloring-1.0.0.apk b/releases/androidsoft-coloring-1.0.0.apk
deleted file mode 100644
index d1da3b6..0000000
Binary files a/releases/androidsoft-coloring-1.0.0.apk and /dev/null differ
diff --git a/releases/androidsoft-coloring-1.0.1.apk b/releases/androidsoft-coloring-1.0.1.apk
deleted file mode 100644
index 1c2cc37..0000000
Binary files a/releases/androidsoft-coloring-1.0.1.apk and /dev/null differ
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index bb09760..605b58f 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -1,67 +1,112 @@
-
-
-
-
+ xmlns:tools="http://schemas.android.com/tools"
+ package="eu.quelltext.coloring"
+ android:versionCode="8"
+ android:versionName="1.1.8">
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
+
+
+
+
+
-
-
+
+
+
+
+
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
diff --git a/src/main/assets/changelogs/de b/src/main/assets/changelogs/de
new file mode 120000
index 0000000..27ffc03
--- /dev/null
+++ b/src/main/assets/changelogs/de
@@ -0,0 +1 @@
+../../../../metadata/de/changelogs/
\ No newline at end of file
diff --git a/src/main/assets/changelogs/de-DE b/src/main/assets/changelogs/de-DE
new file mode 120000
index 0000000..883c3ba
--- /dev/null
+++ b/src/main/assets/changelogs/de-DE
@@ -0,0 +1 @@
+../../../../metadata/de-DE/changelogs/
\ No newline at end of file
diff --git a/src/main/assets/changelogs/en b/src/main/assets/changelogs/en
new file mode 120000
index 0000000..7d3ba75
--- /dev/null
+++ b/src/main/assets/changelogs/en
@@ -0,0 +1 @@
+../../../../metadata/en/changelogs/
\ No newline at end of file
diff --git a/src/main/assets/changelogs/en-US b/src/main/assets/changelogs/en-US
new file mode 120000
index 0000000..1715e2e
--- /dev/null
+++ b/src/main/assets/changelogs/en-US
@@ -0,0 +1 @@
+../../../../metadata/en-US/changelogs/
\ No newline at end of file
diff --git a/src/main/assets/changelogs/pt b/src/main/assets/changelogs/pt
new file mode 120000
index 0000000..7c94bdf
--- /dev/null
+++ b/src/main/assets/changelogs/pt
@@ -0,0 +1 @@
+../../../../metadata/pt/changelogs/
\ No newline at end of file
diff --git a/src/main/assets/changelogs/pt-BR b/src/main/assets/changelogs/pt-BR
new file mode 120000
index 0000000..d3738cd
--- /dev/null
+++ b/src/main/assets/changelogs/pt-BR
@@ -0,0 +1 @@
+../../../../metadata/pt-BR/changelogs/
\ No newline at end of file
diff --git a/src/main/java/eu/quelltext/images/ArrayMapper.java b/src/main/java/eu/quelltext/images/ArrayMapper.java
new file mode 100644
index 0000000..43aceeb
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/ArrayMapper.java
@@ -0,0 +1,129 @@
+package eu.quelltext.images;
+
+import org.androidsoft.coloring.ui.activity.PaintActivity;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/* This class maps integer arrays back and forth.
+ * Suppose, you have an {array of colors} where the colors are huge numbers.
+ * You can use ArrayMapper.mapTo({array of colors}) to get an array with lower numbers
+ * from 0 to the the number of colors - 1.
+ * {array of colors} --> mapTo() --> {indices of colors}, {colors} --> mapFrom() --> {array of colors}
+ */
+public class ArrayMapper {
+
+ public interface Result {
+ int[] getArray();
+ int[] getValuesInOrder();
+ }
+
+ public static class MapToResult implements Result {
+
+ private final int[] result;
+ private final Map mapping;
+
+ public MapToResult(int[] result, Map mapping) {
+ this.result = result;
+ this.mapping = mapping;
+ }
+
+ public int[] getArray() {
+ return result;
+ }
+
+ public int[] getValuesInOrder() {
+ int[] keys = new int[mapping.size()];
+ //Arrays.fill(keys, Util.max(mapping.values(), -1) + 1);
+ for (Map.Entry entry: mapping.entrySet()) {
+ keys[entry.getValue()] = entry.getKey();
+ }
+ return keys;
+ }
+ }
+
+ public static Result mapTo(int[] data) {
+ return mapTo(data, new int[0]);
+ }
+
+ public static Result mapTo(int[] data, int[] values) {
+ Map mapping = new ConcurrentSkipListMap<>();
+ for (int i = 0; i < values.length; i++) {
+ mapping.put(values[i], i);
+ }
+ int[] result = new int[data.length];
+ for (int i = 0; i < result.length; i++) {
+ Integer id = mapping.get(data[i]);
+ if (id == null) {
+ id = mapping.size();
+ mapping.put(data[i], id);
+ }
+ result[i] = id;
+ }
+ return new MapToResult(result, mapping);
+ }
+
+ private static class MapFromResult implements Result {
+ private final int[] result;
+ private int[] keys;
+ private Map mapping;
+
+ public MapFromResult(int[] result, int[] keys, Map mapping) {
+ this.result = result;
+ this.keys = keys;
+ this.mapping = mapping;
+ }
+
+ @Override
+ public int[] getArray() {
+ return result;
+ }
+
+ @Override
+ public int[] getValuesInOrder() {
+ if (mapping == null) {
+ return keys;
+ }
+ int[] newKeys = new int[Util.max(mapping.keySet(), -1) + 1];
+ Arrays.fill(newKeys, Util.max(mapping.values(), -1) + 1);
+ System.arraycopy(keys, 0, newKeys, 0, keys.length);
+ for (Map.Entry entry : mapping.entrySet()) {
+ newKeys[entry.getKey()] = entry.getValue();
+ }
+ keys = newKeys;
+ mapping = null; // no need to compute a second time
+ return keys;
+ }
+ }
+
+ public static Result mapFrom(int[] array) {
+ return mapFrom(array, new int[0]);
+ }
+
+ public static Result mapFrom(int[] array, int[] keys) {
+ Map mapping = null;
+ int nextUnknownValue = 0;
+ int[] result = new int[array.length];
+ for (int i = 0; i < array.length; i++) {
+ int key = array[i];
+ if (key >= keys.length) {
+ if (mapping == null) {
+ mapping = new ConcurrentSkipListMap<>();
+ nextUnknownValue = Util.max(keys, -1);
+ }
+ Integer value = mapping.get(key);
+ if (value == null) {
+ nextUnknownValue++;
+ mapping.put(key, nextUnknownValue);
+ result[i] = nextUnknownValue;
+ } else {
+ result[i] = value;
+ }
+ } else {
+ result[i] = keys[key];
+ }
+ }
+ return new MapFromResult(result, keys, mapping);
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/BlackAndWhiteConversion.java b/src/main/java/eu/quelltext/images/BlackAndWhiteConversion.java
new file mode 100644
index 0000000..534aa5c
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/BlackAndWhiteConversion.java
@@ -0,0 +1,23 @@
+package eu.quelltext.images;
+
+public class BlackAndWhiteConversion {
+
+ private final int colorBright;
+ private final int colorDark;
+ private static final int BINARY_COLOR_THRESHOLD = 3 * 0xff / 2;
+
+
+ public BlackAndWhiteConversion(int colorBright, int colorDark) {
+ this.colorBright = colorBright;
+ this.colorDark = colorDark;
+ }
+
+ public void toBlackAndWhite(int[] pixels) {
+ for (int i = 0; i < pixels.length; i++) {
+ int pixel = pixels[i];
+ int brightness = (pixel & 0xff) + ((pixel >> 8) & 0xff) + ((pixel >> 16) & 0xff) +
+ (3 * (0xff - (0xff & (pixel >> 24)))); // transparency, see https://github.com/niccokunzmann/coloring-book/issues/86
+ pixels[i] = brightness > BINARY_COLOR_THRESHOLD ? colorBright : colorDark;
+ }
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/ClusteredColors.java b/src/main/java/eu/quelltext/images/ClusteredColors.java
new file mode 100644
index 0000000..4e25082
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/ClusteredColors.java
@@ -0,0 +1,31 @@
+package eu.quelltext.images;
+
+import java.util.Arrays;
+
+public class ClusteredColors {
+ private final int[] classifiedColors;
+ private final int[] centroidColors;
+
+ public ClusteredColors(int[] classifiedColors, int[] centroidColors) {
+ this.classifiedColors = classifiedColors;
+ this.centroidColors = centroidColors;
+ }
+
+ /* Return a list of colors
+ *
+ */
+ public int[] getClassifiedColors() {
+ ArrayMapper.Result mapping = ArrayMapper.mapFrom(classifiedColors, centroidColors);
+ if (mapping.getValuesInOrder().length != centroidColors.length) {
+ throw new AssertionError("Not all colors were in the centroids.");
+ }
+ return mapping.getArray();
+ }
+
+ /* Return a list of integers from 0 to n where n is the number of clusters - 1
+ *
+ */
+ public int[] getArrayWithClusterIds() {
+ return classifiedColors;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/ColorComparator.java b/src/main/java/eu/quelltext/images/ColorComparator.java
new file mode 100644
index 0000000..e8e59ae
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/ColorComparator.java
@@ -0,0 +1,24 @@
+package eu.quelltext.images;
+
+public abstract class ColorComparator {
+
+ public static ColorComparator unequal(final int color1, final int color2) {
+ return new ColorComparator() {
+ @Override
+ boolean equals(int color) {
+ return color != color1 && color != color2;
+ }
+ };
+ }
+
+ public static ColorComparator unequal(final int color) {
+ return new ColorComparator() {
+ @Override
+ boolean equals(int c) {
+ return color != c;
+ }
+ };
+ }
+
+ abstract boolean equals(int color);
+}
diff --git a/src/main/java/eu/quelltext/images/ColorSearch.java b/src/main/java/eu/quelltext/images/ColorSearch.java
new file mode 100644
index 0000000..c64c3da
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/ColorSearch.java
@@ -0,0 +1,59 @@
+package eu.quelltext.images;
+
+/* Search for a color at a position using a comparator. The closest occurrence is searched for.
+ *
+ */
+public class ColorSearch {
+ private final int[] colors;
+ private final int width;
+ private final int height;
+ private boolean success = false;
+ private int foundX;
+ private int foundY;
+
+ public ColorSearch(int[] colors, int width, int height) {
+ this.colors = colors;
+ this.width = width;
+ this.height = height;
+ }
+
+ public void startSearch(int startX, int startY, ColorComparator comparator, int searchRadius) {
+ success = false;
+ for (int currentRadius = 0; currentRadius <= searchRadius; currentRadius++) {
+ for (int delta = -currentRadius; delta <= currentRadius; delta++) {
+ if (
+ found(startX + delta, startY + currentRadius, comparator) ||
+ found(startX + delta, startY - currentRadius, comparator) ||
+ found(startX + currentRadius, startY + delta, comparator) ||
+ found(startX - currentRadius, startY + delta, comparator)
+ ) {
+ return;
+ }
+ }
+ }
+ }
+
+ private boolean found(int x, int y, ColorComparator comparator) {
+ if (x < 0 || y < 0 || x >= width || y >= height ) {
+ return false;
+ }
+ if (comparator.equals(colors[x + y * width])) {
+ success = true;
+ foundX = x;
+ foundY = y;
+ }
+ return success;
+ }
+
+ public int getX() {
+ return foundX;
+ }
+
+ public int getY() {
+ return foundY;
+ }
+
+ public boolean wasSuccessful() {
+ return success;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/ConnectedComponents.java b/src/main/java/eu/quelltext/images/ConnectedComponents.java
new file mode 100644
index 0000000..0e2ccb0
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/ConnectedComponents.java
@@ -0,0 +1,140 @@
+package eu.quelltext.images;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+public class ConnectedComponents {
+ private final int[] classified;
+ private final int width;
+ private final int height;
+
+ public ConnectedComponents(int[] classified, int width, int height) {
+ this.classified = classified;
+ this.width = width;
+ this.height = height;
+ }
+
+ public ConnectedComponents(int[][] classified) {
+ this.classified = Util.flatten(classified);
+ this.height = classified.length;
+ this.width = height == 0 ? 0 : classified[0].length;
+ }
+
+ public Result compute() {
+ int[] area = new int[width * height];
+ List> labels = new ArrayList<>();
+
+ // first pass: label
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int thisValue = classified[x + y * width];
+ if (x > 0 && classified[x-1 + y * width] == thisValue) {
+ if (y > 0 && classified[x + (y-1) * width] == thisValue) {
+ /* pixel is equal to left and top */
+ int leftLabel = area[x-1 + y * width];
+ int topLabel = area[x + (y-1) * width];
+ area[x + y * width] = leftLabel;
+ if (leftLabel == topLabel) {
+ /* both labels are equivalent */
+ } else {
+ /* record equivalence */
+ Set leftSet = labels.get(leftLabel);
+ leftSet.addAll(labels.get(topLabel));
+ labels.set(topLabel, leftSet);
+ }
+ } else {
+ /* pixel is equal to left only */
+ area[x + y * width] = area[x-1 + y * width];
+ }
+ } else if (y > 0 && classified[x + (y-1) * width] == thisValue) {
+ /* pixel is equal to top only */
+ area[x + y * width] = area[x + (y-1) * width];
+ } else {
+ /* pixel is different from top and left */
+ /* create new label */
+ int newLabel = labels.size();
+ CopyOnWriteArraySet set = new CopyOnWriteArraySet();
+ set.add(newLabel);
+ labels.add(set);
+ area[x + y * width] = newLabel;
+ }
+ }
+ }
+ // second pass components
+ int[] minLabels = new int[labels.size()]; /* this is the lowest recorded equivalent label */
+ for (int i = 0; i < labels.size(); i++) {
+ int minLabel = i;
+ for (int label : labels.get(i)) {
+ if (label < minLabel) {
+ minLabel = label;
+ }
+ }
+ minLabels[i] = minLabel;
+ }
+ return new Result(area, width, height, minLabels);
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public static class Result {
+ private boolean isFinal;
+ private int[] area;
+ private final int width;
+ private final int height;
+ private int[] minLabels;
+
+ public Result(int[] area, int width, int height, int[] minLabels) {
+ this.area = area;
+ this.width = width;
+ this.height = height;
+ this.minLabels = minLabels;
+ isFinal = false;
+ }
+
+ public int[] computeArray() {
+ if (isFinal) {
+ return area;
+ }
+ int[] result = new int[width * height];
+ Map finalLabels = new HashMap<>();
+ for (int i = 0; i < result.length; i++) {
+ int label = minLabels[area[i]];
+ int finalLabel;
+ if (!finalLabels.containsKey(label)) {
+ finalLabel = finalLabels.size();
+ finalLabels.put(label, finalLabel);
+ } else {
+ finalLabel = finalLabels.get(label);
+ }
+ result[i] = finalLabel;
+ }
+ // record result - it will not change
+ minLabels = null;
+ this.area = result;
+ isFinal = true;
+ return result;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public Measurement computeMeasurement() {
+ return new Measurement(computeArray(), width, height);
+ }
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/FastMaximumShiftFilter.java b/src/main/java/eu/quelltext/images/FastMaximumShiftFilter.java
new file mode 100644
index 0000000..3a34543
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/FastMaximumShiftFilter.java
@@ -0,0 +1,148 @@
+package eu.quelltext.images;
+
+/* This counter has a run time complexity O(width * height * radius)
+ *
+ */
+public class FastMaximumShiftFilter implements MaximumShiftFilter {
+
+ private final int[] array;
+ private final int width;
+ private final int height;
+
+ public FastMaximumShiftFilter(int[] array, int width, int height) {
+ this.array = array;
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public int[] compute(int radius) {
+ //System.out.println("-------------------------------");
+ int[] result = new int[array.length];
+ OccurrenceCounter startCounter = new OccurrenceCounter();
+ // initialize counter
+ for (int y = 0; y < radius && y < height; y++) {
+ for (int x = 0; x <= radius && x < width; x++) {
+ startCounter.increase(array[x + y * width]);
+ }
+ }
+ for (int y = 0; y < height; y++) {
+ //System.out.println("new line " + y);
+ int minY = y - radius - 1; // index to remove line, not in range
+ int maxY = y + radius; // index to add line, in range
+ if (minY >= 0) {
+ for (int x = 0; x <= radius; x++) {
+ startCounter.decrease(array[x + minY * width]);
+ }
+ }
+ if (maxY < height) {
+ for (int x = 0; x <= radius; x++) {
+ startCounter.increase(array[x + maxY * width]);
+ }
+ }
+ int yTimesWidth = y * width;
+ result[yTimesWidth] = startCounter.max();
+ // start of the row is initialized
+ OccurrenceCounter counter = startCounter.copy();
+ // this defines the range we operate on
+ minY = Math.max(minY + 1, 0); // out of range
+ maxY = Math.min(maxY, height - 1); // in range
+ for (int x = 1; x < width; x++) {
+ //System.out.println("pixel " + x + "," + y);
+ int minX = x - radius - 1; // index to remove line
+ int maxX = x + radius; // index to add line
+ // remove the line we leave
+ if (minX >= 0) {
+ for (int ky = minY; ky <= maxY; ky++) {
+ counter.decrease(array[minX + ky * width]);
+ }
+ }
+ // add the line that is reachable
+ if (maxX < width) {
+ for (int ky = minY; ky <= maxY; ky++) {
+ counter.increase(array[maxX + ky * width]);
+ }
+ }
+ result[x + yTimesWidth] = counter.max();
+ }
+ }
+ return result;
+ }
+}
+
+/*
+ int width = classifiedPixels.length;
+ int height = classifiedPixels[0].length;
+
+ smoothedColors = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ smoothedPixels = new int[width][height];
+
+ int resultColorsIndex = 0;
+ int[] resultColors = new int[width * height];
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int[] surroundingCls = new int[NUMBER_OF_COLORS];
+ int kx_min = Math.max(x - AREA_RADIUS, 0);
+ int ky_min = Math.max(y - AREA_RADIUS, 0);
+ int kx_max = Math.min(x + AREA_RADIUS + 1, width);
+ int ky_max = Math.min(y + AREA_RADIUS + 1, height);
+
+ // use a kernel like this
+ // +--+--+
+ // | | |
+ // +--+--+
+ // | | |
+ // +--+--+
+ for (int ky = ky_min; ky < ky_max; ky++) {
+ if (ky_min <= ky && ky < ky_max) {
+ for (int kx = kx_min; kx < kx_max; kx += AREA_RADIUS) {
+ int cls = classifiedPixels[kx][ky];
+ surroundingCls[cls]++;
+ }
+ }
+ }
+ for (int kx = kx_min; kx < kx_max; kx++) {
+ if (kx_min <= kx && kx < kx_max) {
+ for (int ky = ky_min; ky < ky_max; ky += AREA_RADIUS) {
+ int cls = classifiedPixels[kx][ky];
+ surroundingCls[cls]++;
+ }
+ }
+ }
+ // use a kernel like this
+ // \|/
+ // -+-
+ // /|\
+ for (int k = 0; k <= AREA_RADIUS; k++) {
+ int kyMin = Math.max(y - k, ky_min);
+ int kyMax = Math.min(y + k, ky_max - 1);
+ int kxMin = Math.max(x - k, kx_min);
+ int kxMax = Math.min(x + k, kx_max - 1);
+ //surroundingCls[classifiedPixels[kxMin][y]]++;
+ //surroundingCls[classifiedPixels[kxMax][y]]++;
+ //surroundingCls[classifiedPixels[ x ][kyMin]]++;
+ surroundingCls[classifiedPixels[kxMin][kyMin]]++;
+ surroundingCls[classifiedPixels[kxMax][kyMin]]++;
+ //surroundingCls[classifiedPixels[ x ][kyMax]]++;
+ surroundingCls[classifiedPixels[kxMin][kyMax]]++;
+ surroundingCls[classifiedPixels[kxMax][kyMax]]++;
+ }
+ int maxValue = surroundingCls[0];
+ int bestClass = 0;
+ for (int i = 1; i < NUMBER_OF_COLORS; i++) {
+ if (maxValue < surroundingCls[i]) {
+ maxValue = surroundingCls[i];
+ bestClass = i;
+ }
+ }
+ smoothedPixels[x][y] = bestClass;
+ int color = centroidColors[bestClass];
+ resultColors[resultColorsIndex] = color;
+ resultColorsIndex++;
+ }
+ }
+ progress.stepShowSmoothedImage();
+ smoothedColors.setPixels(resultColors, 0, width, 0, 0, width, height);
+
+ */
diff --git a/src/main/java/eu/quelltext/images/FloodFill.java b/src/main/java/eu/quelltext/images/FloodFill.java
new file mode 100644
index 0000000..8d0677a
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/FloodFill.java
@@ -0,0 +1,60 @@
+package eu.quelltext.images;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/* Fill areas with a color but do not cross the border color.
+ * This algorithm fills bordered areas regardless of the color in the area.
+ */
+public class FloodFill {
+
+ private final int[] pixels;
+ private final int width;
+ private final int height;
+ private final int borderColor;
+ private final Queue queue = new LinkedList<>();
+
+ public FloodFill(int[] pixels, int width, int height, int borderColor) {
+ this.pixels = pixels;
+ this.width = width;
+ this.height = height;
+ this.borderColor = borderColor;
+ }
+
+ public void fillAt(int x, int y, int color) {
+ fillPixel(x, y, color);
+ while (!queue.isEmpty()) {
+ Pixel p = queue.remove();
+ fillPixel(p.x + 1, p.y, color);
+ fillPixel(p.x - 1, p.y, color);
+ fillPixel(p.x, p.y + 1, color);
+ fillPixel(p.x, p.y - 1, color);
+ }
+ }
+
+ private void fillPixel(int x, int y, int color) {
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return;
+ }
+ int pixelColor = pixels[x + y * width];
+ if (pixelColor == color || pixelColor == borderColor) {
+ return;
+ }
+ queue.add(new Pixel(x, y));
+ pixels[x + y * width] = color;
+ }
+
+ private static class Pixel {
+ public Pixel(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public int x;
+ public int y;
+ }
+
+ public int[] getPixels() {
+ return pixels;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/KMeansOnRGBColors.java b/src/main/java/eu/quelltext/images/KMeansOnRGBColors.java
new file mode 100644
index 0000000..f1bd8b8
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/KMeansOnRGBColors.java
@@ -0,0 +1,173 @@
+package eu.quelltext.images;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+import weka.clusterers.SimpleKMeans;
+import weka.core.Attribute;
+import weka.core.DenseInstance;
+import weka.core.Instance;
+import weka.core.Instances;
+import weka.core.ManhattanDistance;
+
+public class KMeansOnRGBColors {
+
+ private static final int NOT_TRANSPARENT = 0xff000000;
+
+ private final int[] colors;
+ private final int numberOfColors;
+ private final ArrayList attributes;
+ private final int capacity;
+ private final SimpleKMeans kmeans;
+
+ private Attribute red = new Attribute("red");
+ private Attribute green = new Attribute("green");
+ private Attribute blue = new Attribute("blue");
+
+ // step 1 attributes
+ private boolean step01Taken = false;
+ private Instances data;
+
+ // step 2 attributes
+ private boolean step02Taken = false;
+ private int[][] centroids;
+ private int[] centroidColors;
+
+ public KMeansOnRGBColors(int[] colors, int numberOfColors) throws Exception {
+ this.colors = colors;
+ this.numberOfColors = numberOfColors;
+ // for an example run, see
+ // see https://www.programcreek.com/2014/02/k-means-clustering-in-java/
+ // for SimpleKMeans
+ // see https://weka.sourceforge.io/doc.dev/weka/clusterers/SimpleKMeans.html
+ kmeans = new SimpleKMeans();
+ // see https://weka.sourceforge.io/doc.dev/weka/core/DistanceFunction.html
+ kmeans.setDistanceFunction(new ManhattanDistance()); // manhattan should speed things up
+ kmeans.setPreserveInstancesOrder(false);
+ kmeans.setFastDistanceCalc(true);
+ kmeans.setNumClusters(numberOfColors);
+ // computing the number of colors to get from the image
+ // see https://medium.com/@equipintelligence/java-algorithms-the-k-nearest-neighbor-classifier-4faca7ad26b2
+ // in the Section "Determining the value of K"
+ // formula: K = sqrt ( number of samples in dataset ) / 2
+ capacity = 4 * numberOfColors * numberOfColors;
+ // for attributes
+ // see https://weka.sourceforge.io/doc.dev/weka/core/Attribute.html
+ attributes = new ArrayList<>(3);
+ attributes.add(red);
+ attributes.add(green);
+ attributes.add(blue);
+ }
+
+ /* This samples random data points in a reproducible way.
+ *
+ */
+ public void step01RandomSampling() {
+ data = new Instances("colors", attributes, capacity);
+ // build the data set from randomly sampled data
+ Random random = new Random(0xffaa123678234232l);
+ for (int i = 0; i < capacity; i++) {
+ int index = random.nextInt(colors.length);
+ Instance pixel = getPixelInstanceAt(index);
+ data.add(pixel);
+ }
+ step01Taken = true;
+ }
+
+ private Instance getPixelInstanceAt(int index) {
+ // get color as int, see https://stackoverflow.com/a/40498362/1320237
+ int color = colors[index];
+ int red = (color >> 16) & 0xff;
+ int green = (color >> 8) & 0xff;
+ int blue = color & 0xff;
+ // use HSV to remove the brightness of the color
+ // see https://en.wikipedia.org/wiki/HSL_and_HSV
+ /*float[] hsv = new float[3];
+ Color.RGBToHSV(red, green, blue, hsv);
+ hsv[2] = hsv[2] > 0.5f ? 1 : 0; // value
+ color = Color.HSVToColor(hsv);
+ red = (color >> 16) & 0xff;
+ green = (color >> 8) & 0xff;
+ blue = color & 0xff;*/
+ // for instances
+ // see https://weka.sourceforge.io/doc.dev/weka/core/DenseInstance.html
+ Instance pixel = new DenseInstance(3);
+ pixel.setValue(this.red, red);
+ pixel.setValue(this.green, green);
+ pixel.setValue(this.blue, blue);
+ return pixel;
+ }
+
+ public void step02clustering() throws Exception {
+ if (!step01Taken) {
+ throw new AssertionError("Step 1 needs to be taken before step 2.");
+ }
+ // compute k-means
+ kmeans.buildClusterer(data);
+ Instances centroidInstances = kmeans.getClusterCentroids();
+ centroids = new int[numberOfColors][3];
+ centroidColors = new int[numberOfColors];
+ for (int i = 0; i < numberOfColors; i++) {
+ Instance centroid = centroidInstances.size() > i ?
+ centroidInstances.get(i) : centroidInstances.firstInstance();
+ int[] color = getColorOf(centroid);
+ centroids[i] = color;
+ centroidColors[i] = NOT_TRANSPARENT | color[0] << 16 | color[1] << 8 | color[2];
+ }
+ step02Taken = true;
+ }
+
+ private int[] getColorOf(Instance centroid) {
+ int[] color = new int[3];
+ for (int a = 0; a < centroid.numAttributes(); a++) {
+ Attribute attribute = centroid.attribute(a);
+ int value = (int)Math.round(centroid.value(a)) & 0xff;
+ if (attribute.equals(red)) {
+ color[0] = value;
+ } else if (attribute.equals(green)) {
+ color[1] = value;
+ } else if (attribute.equals(blue)) {
+ color[2] = value;
+ } else {
+ throw new AssertionError("expected the attribute to equal a color");
+ }
+ }
+ return color;
+ }
+
+ public ClusteredColors step03ClassifyData() {
+ if (!step02Taken) {
+ throw new AssertionError("Step 2 needs to be taken before step 3.");
+ }
+ int[] classifiedColors = new int[colors.length];
+ for (int i = 0; i < colors.length; i++) {
+ classifiedColors[i] = classifyPixel(i);
+ }
+ return new ClusteredColors(classifiedColors, centroidColors);
+ }
+
+ private int classifyPixel(int index) {
+ int color = colors[index];
+ int r = (color >> 16) & 0xff;
+ int g = (color >> 8) & 0xff;
+ int b = (color ) & 0xff;
+ int minDistance = 0xffff;
+ int minCentroid = -1;
+ for (int i = 0; i < numberOfColors; i++) {
+ int[] centroid = centroids[i];
+ int distanceToCentroid = // manhattan distance
+ Math.abs(r - centroid[0]) +
+ Math.abs(g - centroid[1]) +
+ Math.abs(b - centroid[2]);
+ if (minDistance > distanceToCentroid) {
+ minCentroid = i;
+ minDistance = distanceToCentroid;
+ }
+ }
+ if (minCentroid == -1) {
+ throw new AssertionError("A centroid should have been chosen.");
+ }
+ return minCentroid;
+ }
+
+}
diff --git a/src/main/java/eu/quelltext/images/LineasAroundAreas.java b/src/main/java/eu/quelltext/images/LineasAroundAreas.java
new file mode 100644
index 0000000..0602807
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/LineasAroundAreas.java
@@ -0,0 +1,53 @@
+package eu.quelltext.images;
+
+import android.graphics.Bitmap;
+
+public class LineasAroundAreas {
+ private final int[] areas;
+ private final int width;
+ private final int height;
+
+ public LineasAroundAreas(int[] areas, int width, int height) {
+ this.areas = areas;
+ this.width = width;
+ this.height = height;
+ }
+
+ public int[] draw(int lineWidth, int backgroundColor, int lineColor) {
+ final int lineWidthHalf = lineWidth / 2 + lineWidth % 2;
+ final int lineWidthHalfSquared = lineWidthHalf * lineWidthHalf;
+ int[] result = new int[areas.length];
+ int xMax = width - 1;
+ int yMax = height - 1;
+ for (int x = 0; x < xMax; x++) {
+ for (int y = 0; y < yMax; y++) {
+ int i = x + y * width;
+ int cls = areas[i];
+ if (cls != areas[x+1 + y * width] || cls != areas[x + (y+1) * width]) {
+ // we are at a border - draw a circle!
+ for (int dx = -lineWidthHalf; dx <= lineWidthHalf; dx++) {
+ for (int dy = -lineWidthHalf; dy <= lineWidthHalf; dy++) {
+ // check that we paint a dot
+ if (dx*dx + dy*dy > lineWidthHalfSquared) {
+ continue;
+ }
+ // check bounds
+ int kx = x + dx;
+ if (kx < 0 || kx >= width) {
+ continue;
+ }
+ int ky = y + dy;
+ if (ky < 0 || ky >= height) {
+ continue;
+ }
+ result[kx + ky * width] = lineColor;
+ }
+ }
+ } else if (result[i] != lineColor) {
+ result[i] = backgroundColor;
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/MaximumShiftFilter.java b/src/main/java/eu/quelltext/images/MaximumShiftFilter.java
new file mode 100644
index 0000000..bc5c2bc
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/MaximumShiftFilter.java
@@ -0,0 +1,5 @@
+package eu.quelltext.images;
+
+public interface MaximumShiftFilter {
+ int[] compute(int radius);
+}
diff --git a/src/main/java/eu/quelltext/images/Measurement.java b/src/main/java/eu/quelltext/images/Measurement.java
new file mode 100644
index 0000000..4eb4caf
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/Measurement.java
@@ -0,0 +1,172 @@
+package eu.quelltext.images;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Measurement {
+ private final Map> neighborCount = new HashMap<>();
+ private final Map> positions = new HashMap<>();
+ private final Map equalityLookup = new HashMap<>();
+ private final int width;
+ private final int height;
+
+ public Measurement(int[] classified, int width, int height) {
+ this.width = width;
+ this.height = height;
+ setup(classified);
+ }
+
+ private void setup(int[] classified) {
+ int i = 0;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++, i++) {
+ int label = classified[i];
+ equalityLookup.put(label, label);
+ if (!positions.containsKey(label)) {
+ positions.put(label, new ArrayList());
+ neighborCount.put(label, new HashMap());
+ }
+ positions.get(label).add(new XY(x, y));
+ Map thisCount = null;
+ if (x > 0) {
+ int leftLabel = classified[i - 1];
+ if (leftLabel != label) {
+ thisCount = neighborCount.get(label);
+ Map leftCount = neighborCount.get(leftLabel);
+ increaseByOne(thisCount, leftLabel);
+ increaseByOne(leftCount, label);
+ }
+ }
+ if (y > 0) {
+ int topLabel = classified[i - width];
+ if (topLabel != label) {
+ if (thisCount == null) {
+ thisCount = neighborCount.get(label);
+ }
+ Map topCount = neighborCount.get(topLabel);
+ increaseByOne(thisCount, topLabel);
+ increaseByOne(topCount, label);
+ }
+ }
+ }
+ }
+ }
+
+ private void increaseByOne(Map counters, int label) {
+ if (counters.containsKey(label)) {
+ counters.put(label, counters.get(label) + 1);
+ } else {
+ counters.put(label, 1);
+ }
+ }
+
+
+ public int getNumberOfComponents() {
+ return positions.size();
+ }
+
+ public void mergeSmallestAreaIntoItsBiggestNeighbor() {
+ if (positions.size() <= 1) {
+ return;
+ }
+ int smallestLabel = getSmallestLabel();
+ int biggestNeighborSize = 0;
+ int biggestNeighborLabel = -1;
+ for (Map.Entry neigbor : neighborCount.get(smallestLabel).entrySet()) {
+ if (biggestNeighborSize < neigbor.getValue()) {
+ biggestNeighborLabel = neigbor.getKey();
+ biggestNeighborSize = neigbor.getValue();
+ }
+ }
+ if (smallestLabel == biggestNeighborLabel) {
+ throw new AssertionError("A label can not be neighbor of itself.");
+ }
+ if (biggestNeighborLabel == -1) {
+ throw new AssertionError("The label " + smallestLabel + " has no neighbors.");
+ }
+ ArrayList biggestNeighborPositions = null;
+ while(biggestNeighborPositions == null) {
+ biggestNeighborLabel = equalityLookup.get(biggestNeighborLabel);
+ biggestNeighborPositions = positions.get(biggestNeighborLabel);
+ }
+ biggestNeighborPositions.addAll(positions.get(smallestLabel));
+ positions.remove(smallestLabel);
+ Map biggestNeighborsNeighbors = neighborCount.get(biggestNeighborLabel);
+ for (Map.Entry neighbor : neighborCount.get(smallestLabel).entrySet()) {
+ Integer neighborLabel = neighbor.getKey();
+ if (biggestNeighborsNeighbors.containsKey(neighborLabel)) {
+ biggestNeighborsNeighbors.put(neighborLabel,
+ biggestNeighborsNeighbors.get(neighborLabel) + neighbor.getValue());
+ } else {
+ biggestNeighborsNeighbors.put(neighborLabel, neighbor.getValue());
+ }
+ }
+ neighborCount.remove(smallestLabel);
+ biggestNeighborsNeighbors.remove(smallestLabel);
+ biggestNeighborsNeighbors.remove(biggestNeighborLabel);
+ equalityLookup.put(smallestLabel, biggestNeighborLabel);
+ }
+
+ private int getSmallestLabel() {
+ int smallestLabel = -1;
+ int smallestLabelSize = width * height + 1;
+ for (Map.Entry> label: positions.entrySet()) {
+ if (smallestLabelSize > label.getValue().size()) {
+ smallestLabel = label.getKey();
+ smallestLabelSize = label.getValue().size();
+ }
+ }
+ return smallestLabel;
+ }
+
+ public int[] computeArea() {
+ int[] area = new int[width * height];
+ for (Map.Entry> labelPositions : positions.entrySet()) {
+ int label = labelPositions.getKey();
+ for (XY position : labelPositions.getValue()) {
+ area[position.x + position.y * width] = label;
+ }
+ }
+ return area;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getSmallestComponentSize() {
+ int smallestLabel = getSmallestLabel();
+ return positions.get(smallestLabel).size();
+ }
+
+ private static class XY {
+
+ private final int x;
+ private final int y;
+
+ private XY(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (XY.class.isInstance(obj)) {
+ XY other = (XY)obj;
+ return other.x == x && other.y == y;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return x ^ (y << 16);
+ }
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/OccurrenceCounter.java b/src/main/java/eu/quelltext/images/OccurrenceCounter.java
new file mode 100644
index 0000000..4bd0896
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/OccurrenceCounter.java
@@ -0,0 +1,65 @@
+package eu.quelltext.images;
+
+import android.os.Build;
+import android.util.ArrayMap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OccurrenceCounter {
+ private final Map occurrences;
+
+ public OccurrenceCounter() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ occurrences = new ArrayMap<>();
+ } else {
+ occurrences = new HashMap<>();
+ }
+ }
+
+ public void increase(int label) {
+ increaseBy(label, 1);
+ //System.out.println("\tcounter" + "+" + label);
+ }
+
+ public void decrease(int label) {
+ increaseBy(label, -1);
+ //System.out.println("\tcounter" + "-" + label);
+ }
+
+ private void increaseBy(int label, int difference) {
+ int count;
+ Integer previousCount = occurrences.get(label);
+ if (previousCount == null) {
+ count = difference;
+ } else {
+ count = previousCount + difference;
+ }
+ if (count < 0) {
+ throw new AssertionError("Label " + label + " can not occur " + count + " times.");
+ }
+ occurrences.put(label, count);
+ }
+
+ public int max() {
+ int maxLabel = -1;
+ int maxCount = -1;
+ for(Map.Entry label : occurrences.entrySet()) {
+ if (label.getValue() > maxCount) {
+ maxCount = label.getValue();
+ maxLabel = label.getKey();
+ }
+ }
+ return maxLabel;
+ }
+
+ public OccurrenceCounter copy() {
+ OccurrenceCounter copy = new OccurrenceCounter();
+ copy.initializeFrom(occurrences);
+ return copy;
+ }
+
+ protected void initializeFrom(Map occurrences) {
+ this.occurrences.putAll(occurrences);
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/RandomColorGenerator.java b/src/main/java/eu/quelltext/images/RandomColorGenerator.java
new file mode 100644
index 0000000..65b5afd
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/RandomColorGenerator.java
@@ -0,0 +1,29 @@
+package eu.quelltext.images;
+
+import java.util.Random;
+
+/* This class generates the same sequence of colors.
+ *
+ */
+public class RandomColorGenerator {
+
+ private final Random random;
+
+ public RandomColorGenerator() {
+ random = new Random(1828379928l);
+ }
+
+ public int bright() {
+ int randomColor = random.nextInt(0xffffff);
+ int result = 0xff000000 | (0xff << random.nextInt(3)) | randomColor;
+ return result;
+ }
+
+ public int[] bright(int length) {
+ int[] result = new int[length];
+ for (int i = 0; i < length; i++) {
+ result[i] = bright();
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/SimpleMaximumShiftFilter.java b/src/main/java/eu/quelltext/images/SimpleMaximumShiftFilter.java
new file mode 100644
index 0000000..14c1bdf
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/SimpleMaximumShiftFilter.java
@@ -0,0 +1,37 @@
+package eu.quelltext.images;
+
+/* This class moves through the array and assigns each field the class
+ * which occurs maximum in all the fields covered with a radius.
+ */
+class SimpleMaximumShiftFilter implements MaximumShiftFilter {
+ private final int[] array;
+ private final int width;
+ private final int height;
+
+ public SimpleMaximumShiftFilter(int[] array, int width, int height) {
+ this.array = array;
+ this.width = width;
+ this.height = height;
+ }
+
+ public int[] compute(int radius) {
+ int[] result = new int[array.length];
+ for (int y = 0; y < height; y++) {
+ int minY = Math.max(0, y - radius);
+ int maxY = Math.min(height - 1, y + radius);
+ for (int x = 0; x < width; x++) {
+ OccurrenceCounter counter = new OccurrenceCounter();
+ int minX = Math.max(0, x - radius);
+ int maxX = Math.min(width - 1, x + radius);
+ for (int kx = minX ; kx <= maxX; kx++) {
+ for (int ky = minY ; ky <= maxY; ky++) {
+ int label = array[kx + ky * width];
+ counter.increase(label);
+ }
+ }
+ result[x + y * width] = counter.max();
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/eu/quelltext/images/Util.java b/src/main/java/eu/quelltext/images/Util.java
new file mode 100644
index 0000000..f967a8b
--- /dev/null
+++ b/src/main/java/eu/quelltext/images/Util.java
@@ -0,0 +1,60 @@
+package eu.quelltext.images;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Random;
+import java.util.Set;
+
+public class Util {
+ public static int[] flatten(int[][] array) {
+ // from https://stackoverflow.com/a/2569314
+ int size = 0;
+ for (int i = 0; i < array.length; i++) {
+ size += array[i].length;
+ }
+ int[] newArray = new int[size];
+ int newIndex = 0;
+ for (int i = 0; i < array.length; i++) {
+ System.arraycopy(array[i], 0, newArray, newIndex, array[i].length);
+ newIndex += array[i].length;
+ }
+ return newArray;
+ }
+
+ public static int[][] unflatten(int[] array, int numberOfSubarrays) {
+ int subArrayLength = array.length / numberOfSubarrays;
+ int[][] newArray = new int[numberOfSubarrays][subArrayLength];
+ for (int i = 0; i < numberOfSubarrays; i++) {
+ System.arraycopy(array, i * subArrayLength, newArray[i], 0, subArrayLength);
+ }
+ return newArray;
+ }
+
+ public static int max(int[] array, int defaultValue) {
+ if (array.length == 0) {
+ return defaultValue;
+ }
+ int biggestValue = array[0];
+ for (int j = 1; j < array.length; j++) {
+ if (array[j] > biggestValue) {
+ biggestValue = array[j];
+ }
+ }
+ return biggestValue;
+ }
+
+ public static int max(Collection collection, int defaultValue) {
+ if (collection.size() == 0) {
+ return defaultValue;
+ }
+ Iterator iterator = collection.iterator();
+ int biggestValue = iterator.next();
+ for (Iterator it = iterator; it.hasNext(); ) {
+ int value = it.next();
+ if (value > biggestValue) {
+ biggestValue = value;
+ }
+ }
+ return biggestValue;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/AbstractColoringActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/AbstractColoringActivity.java
index 21412d1..b38acc3 100644
--- a/src/main/java/org/androidsoft/coloring/ui/activity/AbstractColoringActivity.java
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/AbstractColoringActivity.java
@@ -24,12 +24,13 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
+
+import org.androidsoft.coloring.util.ScreenUtils;
import org.androidsoft.utils.ui.NoTitleActivity;
-public abstract class AbstractColoringActivity extends NoTitleActivity
+public abstract class AbstractColoringActivity extends FullScreenActivity
{
- public static final String INTENT_START_NEW = "org.androidsoft.coloring.paint.START_NEW";
public static final String INTENT_PICK_COLOR = "org.androidsoft.coloring.paint.PICK_COLOR";
public static final String INTENT_ABOUT = "org.androidsoft.coloring.paint.ABOUT";
@@ -37,6 +38,7 @@ public abstract class AbstractColoringActivity extends NoTitleActivity
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
+ ScreenUtils.setFullscreen(this);
WindowManager w = getWindowManager();
Display d = w.getDefaultDisplay();
@@ -45,6 +47,14 @@ public void onCreate(Bundle savedInstanceState)
}
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ ScreenUtils.setFullscreen(this);
+ }
+ }
+
public static int getDisplayWitdh()
{
return _displayWidth;
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/ChoosePictureActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/ChoosePictureActivity.java
new file mode 100644
index 0000000..cd8c3d2
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/ChoosePictureActivity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2010 Peter Dornbach.
+ *
+ * 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.androidsoft.coloring.ui.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import org.androidsoft.coloring.ui.widget.PreCachingLayoutManager;
+import org.androidsoft.coloring.util.ScreenUtils;
+import org.androidsoft.coloring.util.Settings;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.images.ImageListener;
+import org.androidsoft.coloring.util.images.SectionsAdapter;
+import org.androidsoft.coloring.util.images.SettingsImageDB;
+import org.androidsoft.utils.ui.NoTitleActivity;
+
+import eu.quelltext.coloring.R;
+
+public class ChoosePictureActivity extends FullScreenActivity
+{
+
+ public static final String RESULT_IMAGE = "image";
+ public static final String ARG_IMAGE = "image";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ ScreenUtils.setFullscreen(this);
+ // Apparently this cannot be set from the style.
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ setContentView(R.layout.choose_picture);
+ RecyclerView imagesView = findViewById(R.id.images);
+ // load images ahead
+ int space = getScreenHeight() / 2;
+ LinearLayoutManager manager = new PreCachingLayoutManager(this, space);
+ imagesView.setLayoutManager(manager);
+
+ // use this setting to improve performance if you know that changes
+ // in content do not change the layout size of the RecyclerView
+ imagesView.setHasFixedSize(true);
+
+ // use a linear layout manager
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ imagesView.setLayoutManager(layoutManager);
+
+ // create a database with all the images
+ SettingsImageDB imageDB = Settings.of(this).getImageDB();
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (extras != null && extras.containsKey(ARG_IMAGE)) {
+ ImageDB.Image image = extras.getParcelable(ARG_IMAGE);
+ if (image.canBePainted()) {
+ imageDB.addPaintedImage(image);
+ }
+ }
+
+ // set adapter with all the images
+ SectionsAdapter adapter = new SectionsAdapter(
+ imageDB, R.layout.choose_picture_line,
+ new int[]{R.id.image1, R.id.image2});
+ adapter.setImageListener(new ImageListener() {
+ @Override
+ public void onImageChosen(ImageDB.Image image) {
+ returnImageToParent(image);
+ }
+
+ });
+ imagesView.setAdapter(adapter);
+ }
+
+ private void returnImageToParent(ImageDB.Image image) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_IMAGE, image);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ private int getScreenHeight() {
+ // from https://stackoverflow.com/a/4744499
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ this.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ return displayMetrics.heightPixels;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/CreditsActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/CreditsActivity.java
index 8c8aed6..35a94b1 100644
--- a/src/main/java/org/androidsoft/coloring/ui/activity/CreditsActivity.java
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/CreditsActivity.java
@@ -17,10 +17,11 @@
import android.graphics.Typeface;
import android.os.Bundle;
import android.view.View;
-import org.androidsoft.coloring.R;
+import org.androidsoft.coloring.util.ScreenUtils;
import org.androidsoft.utils.credits.CreditsParams;
import org.androidsoft.utils.credits.CreditsView;
import org.androidsoft.utils.ui.BasicActivity;
+import eu.quelltext.coloring.R;
/**
* Credits activity
@@ -33,12 +34,22 @@ public class CreditsActivity extends BasicActivity
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
+ ScreenUtils.setFullscreen(this);
View view = new CreditsView(this, getCreditsParams());
setContentView(view);
}
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus)
+ {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ ScreenUtils.setFullscreen(this);
+ }
+ }
+
/**
* {@inheritDoc }
*/
@@ -60,8 +71,8 @@ public int getMenuCloseId()
private CreditsParams getCreditsParams()
{
CreditsParams p = new CreditsParams();
- p.setAppNameRes(R.string.credits_app_name);
- p.setAppVersionRes(R.string.credits_current_version);
+ p.setAppNameRes(R.string.app_name);
+ p.setAppVersionRes(R.string.empty);
p.setBitmapBackgroundRes(R.drawable.background);
p.setBitmapBackgroundLandscapeRes(R.drawable.background_land);
p.setArrayCreditsRes(R.array.credits);
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/FullScreenActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/FullScreenActivity.java
new file mode 100644
index 0000000..ee137a6
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/FullScreenActivity.java
@@ -0,0 +1,42 @@
+package org.androidsoft.coloring.ui.activity;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import androidx.annotation.Nullable;
+
+import org.androidsoft.coloring.util.ScreenUtils;
+import org.androidsoft.utils.ui.NoTitleActivity;
+
+public class FullScreenActivity extends NoTitleActivity {
+
+ private ScreenUtils.StatusBarCollapser statusBar = null;
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ ScreenUtils.setFullscreen(this);
+ getStatusBar().shouldCollapse();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getStatusBar().shouldNotCollapse();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (!hasFocus) {
+ getStatusBar().collapse();
+ }
+ }
+
+ public ScreenUtils.StatusBarCollapser getStatusBar() {
+ if (statusBar == null) {
+ statusBar = new ScreenUtils.StatusBarCollapser(this);
+ }
+ return statusBar;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/ImageImportActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/ImageImportActivity.java
new file mode 100644
index 0000000..33cb674
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/ImageImportActivity.java
@@ -0,0 +1,176 @@
+package org.androidsoft.coloring.ui.activity;
+
+
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.ColoredImageImport;
+import org.androidsoft.coloring.util.imports.BlackAndWhiteImageImport;
+import org.androidsoft.coloring.util.images.BitmapImage;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+import org.androidsoft.utils.ui.NoTitleActivity;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import eu.quelltext.coloring.R;
+
+import static org.androidsoft.coloring.ui.activity.PaintActivity.ARG_IMAGE;
+
+/* Activity to receive shared images and pass them to the paint activity
+ * see https://developer.android.com/training/sharing/receive
+ */
+public class ImageImportActivity extends FullScreenActivity {
+
+ private ImageView imageView;
+ private LoadImageProgress progress;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_image_import);
+
+ imageView = findViewById(R.id.imageView);
+ progress = new LoadImageProgress(
+ (ProgressBar)findViewById(R.id.progressBar),
+ (TextView)findViewById(R.id.progress_text));
+
+ openImageFromIntent(getIntent());
+ }
+
+ private void openImageFromIntent(final Intent intent) {
+ // start the receiving when the layouting is done so we know the size of what we are showing
+ // see https://stackoverflow.com/a/24035591/1320237
+ imageView.post(new Runnable() {
+ @Override
+ public void run() {
+ // Get intent, action and MIME type
+ // see https://developer.android.com/training/sharing/receive
+ String action = intent.getAction();
+ String type = intent.getType();
+ Uri linkData = intent.getData();
+
+ Runnable imageProcessing = new Failure();
+ Uri imageUri = null;
+
+ // list all extras of an intent
+ // see https://stackoverflow.com/a/15074150/1320237
+ Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ for (String key : bundle.keySet()) {
+ Log.e("Image Import", key + " : " + (bundle.get(key) != null ? bundle.get(key) : "NULL"));
+ }
+ }
+
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ if (type.startsWith("image/")) {
+ imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (imageUri == null) {
+ String file = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (file != null) {
+ // Uri from string, see https://stackoverflow.com/a/3487413/1320237
+ imageUri = Uri.parse(file);
+ }
+ }
+
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
+ ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (imageUris != null && imageUris.size() >= 1) {
+ imageUri = imageUris.get(0);
+ }
+ } else if (Intent.ACTION_VIEW.equals(action) && linkData != null) {
+ imageUri = linkData;
+ }
+
+ if (imageUri != null) {
+ if (imageUri.toString().toLowerCase().endsWith(".png")) {
+ // import png images directly
+ imageProcessing = new BlackAndWhiteImageImport(imageUri, progress, new ViewImagePreview());
+ } else {
+ imageProcessing = new ColoredImageImport(imageUri, progress, new ViewImagePreview());
+ }
+ }
+
+ Thread processor = new Thread(imageProcessing);
+ processor.start();
+ }
+ });
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ openImageFromIntent(intent);
+ }
+
+ private class Failure implements Runnable {
+
+ @Override
+ public void run() {
+ progress.stepFail();
+ }
+ }
+
+ /* This class cares for the images being shown to the user */
+ private class ViewImagePreview implements ImagePreview {
+ private final Handler handler;
+ private final int width;
+ private final int height;
+ private final ContentResolver contentResolver;
+
+ public ViewImagePreview() {
+ handler = new Handler();
+ width = imageView.getWidth();
+ height = imageView.getHeight();
+ contentResolver = ImageImportActivity.this.getContentResolver();
+ }
+
+ @Override
+ public void setImage(final Bitmap image) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ imageView.setImageBitmap(image);
+ if ((image.getHeight() < image.getWidth()) != (imageView.getHeight() < imageView.getWidth())) {
+ imageView.setRotation(90);
+ }
+ }
+ });
+ }
+
+ @Override
+ public int getWidth() {
+ return width == 0 ? 640 : width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height == 0 ? 480 :height;
+ }
+
+ @Override
+ public InputStream openInputStream(Uri uri) throws FileNotFoundException {
+ return contentResolver.openInputStream(uri);
+ }
+
+ @Override
+ public void done(Bitmap bitmap) {
+ BitmapImage image = new BitmapImage(bitmap);
+ Intent intent = new Intent(ImageImportActivity.this, PaintActivity.class);
+ intent.putExtra(ARG_IMAGE, image);
+ startActivity(intent);
+ finish();
+ }
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/PaintActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/PaintActivity.java
index d19e06c..2d58180 100644
--- a/src/main/java/org/androidsoft/coloring/ui/activity/PaintActivity.java
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/PaintActivity.java
@@ -15,49 +15,58 @@
*/
package org.androidsoft.coloring.ui.activity;
-import org.androidsoft.coloring.ui.widget.PaintView;
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.ui.widget.PaintArea;
import org.androidsoft.coloring.ui.widget.ColorButton;
-import org.androidsoft.coloring.ui.widget.Progress;
-import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import org.androidsoft.coloring.util.BitmapSaver;
+import org.androidsoft.coloring.util.BitmapSharer;
+import org.androidsoft.coloring.util.errors.UIErrorReporter;
+import org.androidsoft.coloring.util.ScreenUtils;
+import org.androidsoft.coloring.util.images.BitmapHash;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.images.ResourceImageDB;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedList;
-import android.app.Dialog;
import android.app.ProgressDialog;
-import android.content.ContentValues;
-import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.MediaScannerConnection;
-import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
+import android.os.BadParcelableException;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
-import android.os.Message;
-import android.provider.MediaStore.Images;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.widget.ProgressBar;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
import java.util.List;
import java.util.ArrayList;
-import org.androidsoft.coloring.R;
-public class PaintActivity extends AbstractColoringActivity implements
- PaintView.LifecycleListener
+import eu.quelltext.coloring.R;
+
+public class PaintActivity extends AbstractColoringActivity
{
- public static final String FILE_BACKUP = "backup";
+ private static final int REQUEST_CHOOSE_PICTURE = 0;
+ private static final int REQUEST_PICK_COLOR = 1;
+ public static final String ARG_IMAGE = "bitmap";
+ private PaintArea paintArea;
+ private ProgressDialog _progressDialog;
+ // The ColorButtonManager makes sure the state of the ColorButtons visible
+ // on this activity is in sync.
+ private ColorButtonManager colorButtonManager;
+ boolean doubleBackToExitPressedOnce = false;
+ private BitmapSaver bitmapSaver = null;
+ private int lastSavedHash; // the hash value of the last saved bitmap
+ private ImageView paintView;
- public PaintActivity()
- {
- _state = new State();
- }
@Override
public void onCreate(Bundle savedInstanceState)
@@ -65,60 +74,84 @@ public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.paint);
- _paintView = (PaintView) findViewById(R.id.paint_view);
- _paintView.setLifecycleListener(this);
- _progressBar = (ProgressBar) findViewById(R.id.paint_progress);
- _progressBar.setMax(Progress.MAX);
- _colorButtonManager = new ColorButtonManager();
+ paintView = (ImageView) findViewById(R.id.paint_view);
+ paintArea = new PaintArea(paintView);
+ colorButtonManager = new ColorButtonManager();
View pickColorsButton = findViewById(R.id.pick_color_button);
- pickColorsButton.setOnClickListener(new PickColorListener());
+ pickColorsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(PaintActivity.this, PickColorActivity.class);
+ startActivityForResult(intent, REQUEST_PICK_COLOR);
+ }
+ });
- final Object previousState = getLastNonConfigurationInstance();
- if (previousState == null)
- {
- // No previous state, this is truly a new activity.
- // We need to make the paint view INVISIBLE (and not GONE) so that
- // it can measure itself correctly.
- _paintView.setVisibility(View.INVISIBLE);
- _progressBar.setVisibility(View.GONE);
- }
- else
- {
- // We have a previous state, so this is a re-created activity.
- // Restore the state of the activity.
- SavedState state = (SavedState) previousState;
- _state = state._paintActivityState;
- _paintView.setState(state._paintViewState);
- _colorButtonManager.setState(state._colorButtonState);
- _paintView.setVisibility(View.VISIBLE);
- _progressBar.setVisibility(View.GONE);
- if (_state._loadInProgress)
- {
- new InitPaintView(_state._loadedResourceId);
+ // make the background area clickable
+ final LinearLayout bg = findViewById(R.id.paint_view_background);
+ bg.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ bg.setBackgroundColor(paintArea.getPaintColor());
}
- }
+ });
+
+ loadImageFromIntent(getIntent());
+ }
+
+ private void loadImageFromIntent(final Intent intent) {
+ paintView.post(new Runnable() {
+ @Override
+ public void run() {
+ Bundle extras = intent.getExtras();
+ ImageDB.Image image;
+ if (extras != null && extras.containsKey(ARG_IMAGE)) {
+ // we received and image and should thus paint it
+ image = extras.getParcelable(ARG_IMAGE);
+ } else {
+ image = new ResourceImageDB(PaintActivity.this).randomImage();
+ }
+ // TODO: add loading animation for the time the image is loading
+ image.asPaintableImage(new Preview(), new LoadImageProgress(null, null));
+ }
+ });
}
@Override
- public boolean onCreateOptionsMenu(Menu menu)
- {
- getMenuInflater().inflate(R.menu.paint_menu, menu);
- return true;
+ protected void onNewIntent(Intent intent) {
+ // capture the new intent
+ // see https://developer.android.com/guide/components/activities/tasks-and-back-stack
+ // see https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
+ super.onNewIntent(intent);
+ saveBitmap();
+ loadImageFromIntent(intent);
}
- public void onPreparedToLoad()
- {
- // We need to invoke InitPaintView in a callback otherwise
- // the visibility changes do not seem to be effective.
- new Handler()
- {
+ @Override
+ public void onBackPressed() {
+ // code for double-clicking the back button to exit the activity
+ // see https://stackoverflow.com/a/13578600/1320237
+ if (doubleBackToExitPressedOnce) {
+ super.onBackPressed();
+ return;
+ }
+
+ this.doubleBackToExitPressedOnce = true;
+ Toast.makeText(this, R.string.toast_double_click_back_button, Toast.LENGTH_SHORT).show();
+
+ new Handler().postDelayed(new Runnable() {
@Override
- public void handleMessage(Message m)
- {
- new InitPaintView(StartNewActivity.randomOutlineId());
+ public void run() {
+ doubleBackToExitPressedOnce=false;
}
- }.sendEmptyMessage(0);
+ }, 2000);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ getMenuInflater().inflate(R.menu.paint_menu, menu);
+ return true;
}
@Override
@@ -127,30 +160,68 @@ public boolean onOptionsItemSelected(MenuItem item)
switch (item.getItemId())
{
case R.id.open_new:
- startActivityForResult(new Intent(INTENT_START_NEW), REQUEST_START_NEW);
+ openPictureChoice();
return true;
case R.id.save:
- new BitmapSaver();
+ saveBitmap();
return true;
case R.id.about:
startActivity(new Intent(INTENT_ABOUT));
return true;
+ case R.id.settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ return true;
+ case R.id.gallery:
+ openGallery();
+ return true;
case R.id.share:
- new BitmapSharer();
+ saveBitmap(new BitmapSharer(this, paintArea.getBitmap()));
return true;
}
return false;
}
+ private void saveBitmap() {
+ saveBitmap(new BitmapSaver(this, paintArea.getBitmap()));
+ }
- @Override
- public Object onRetainNonConfigurationInstance()
- {
- SavedState state = new SavedState();
- state._paintActivityState = _state;
- state._paintViewState = _paintView.getState();
- state._colorButtonState = _colorButtonManager.getState();
- return state;
+ private void saveBitmap(BitmapSaver newBitmapSaver) {
+ int duration = Toast.LENGTH_SHORT;
+ int message;
+ String path;
+ int newHash = BitmapHash.hash(newBitmapSaver.getBitmap());
+ if (bitmapSaver != null && bitmapSaver.isRunning()) {
+ // pressing save while in progress
+ message = R.string.toast_save_file_running;
+ path = bitmapSaver.getFile().getPath();
+ } else if (lastSavedHash == newHash) {
+ // image is already saved
+ message = R.string.toast_save_file_again;
+ path = bitmapSaver.getFile().getPath();
+ newBitmapSaver.alreadySaved(bitmapSaver);
+ } else {
+ // image is not saved
+ bitmapSaver = newBitmapSaver;
+ bitmapSaver.start();
+ message = R.string.toast_save_file;
+ path = bitmapSaver.getFile().getName();
+ lastSavedHash = BitmapHash.hash(newBitmapSaver.getBitmap());
+ }
+ // create a toast
+ // see https://developer.android.com/guide/topics/ui/notifiers/toasts#java
+ String text = getString(message, path);
+ Toast toast = Toast.makeText(this, text, duration);
+ toast.show();
+
+ }
+
+ private void openPictureChoice() {
+ // how to start a new activity
+ // see https://stackoverflow.com/a/4186097
+ Intent intent = new Intent(this, ChoosePictureActivity.class);
+ ImageDB.Image image = paintArea.getImage();
+ intent.putExtra(ChoosePictureActivity.ARG_IMAGE, image);
+ startActivityForResult(intent, REQUEST_CHOOSE_PICTURE);
}
@Override
@@ -158,64 +229,28 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
- case REQUEST_START_NEW:
- if (resultCode != 0)
+ case REQUEST_CHOOSE_PICTURE:
+ if (resultCode == RESULT_OK)
{
- new InitPaintView(resultCode);
+ ImageDB.Image image;
+ try {
+ image = data.getParcelableExtra(ChoosePictureActivity.RESULT_IMAGE);
+ } catch (BadParcelableException e) {
+ UIErrorReporter.of(this).report(e);
+ return;
+ }
+ image.asPaintableImage(new Preview(), new LoadImageProgress(null, null));
}
break;
case REQUEST_PICK_COLOR:
- if (resultCode != 0)
+ if (resultCode != RESULT_CANCELED)
{
- _colorButtonManager.selectColor(resultCode);
+ colorButtonManager.selectColor(resultCode);
}
break;
}
}
- // @Override
- @Override
- protected Dialog onCreateDialog(int id)
- {
- switch (id)
- {
- case DIALOG_PROGRESS:
- _progressDialog = new ProgressDialog(PaintActivity.this);
- _progressDialog.setCancelable(false);
- _progressDialog.setIcon(android.R.drawable.ic_dialog_info);
- _progressDialog.setTitle(R.string.dialog_saving);
- _progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- _progressDialog.setMax(Progress.MAX);
- if (!_saveInProgress)
- {
- // This means that the view hierarchy was recreated but there
- // is no actual save in progress (in this hierarchy), so let's
- // dismiss the dialog.
- new Handler()
- {
-
- @Override
- public void handleMessage(Message m)
- {
- _progressDialog.dismiss();
- }
- }.sendEmptyMessage(0);
- }
-
- return _progressDialog;
- }
- return null;
- }
-
- private class PickColorListener implements View.OnClickListener
- {
-
- public void onClick(View view)
- {
- startActivityForResult(new Intent(INTENT_PICK_COLOR), REQUEST_PICK_COLOR);
- }
- }
-
private class ColorButtonManager implements View.OnClickListener
{
@@ -257,29 +292,6 @@ public void selectColor(int color)
setPaintViewColor();
}
- public Object getState()
- {
- int[] result = new int[_colorButtons.size() + 1];
- int n = _colorButtons.size();
- for (int i = 0; i < n; i++)
- {
- result[i] = _colorButtons.get(i).getColor();
- }
- result[n] = _selectedColorButton.getColor();
- return result;
- }
-
- public void setState(Object o)
- {
- int[] state = (int[]) o;
- int n = _colorButtons.size();
- for (int i = 0; i < n; i++)
- {
- _colorButtons.get(i).setColor(state[i]);
- }
- selectColor(state[n]);
- }
-
// Select the given button.
private void selectButton(ColorButton button)
{
@@ -291,7 +303,15 @@ private void selectButton(ColorButton button)
// Set the currently selected color in the paint view.
private void setPaintViewColor()
{
- _paintView.setPaintColor(_selectedColorButton.getColor());
+ int selectedColor = _selectedColorButton.getColor();
+ paintArea.setPaintColor(selectedColor);
+ setBackgroundColorOfButtons(selectedColor);
+ }
+
+ private void setBackgroundColorOfButtons(int color) {
+ int backgroundColor = (color & 0xffffff) | 0x70000000;
+ View buttonHolder = findViewById(R.id.color_buttons_container);
+ buttonHolder.setBackgroundColor(backgroundColor);
}
// Finds the button with the color. If found, sets it to selected,
@@ -324,268 +344,42 @@ private ColorButton selectAndRemove(int color)
private ColorButton _selectedColorButton;
}
- private class InitPaintView implements Runnable
- {
-
- public InitPaintView(int outlineResourceId)
- {
- // Make the progress bar visible and hide the view
- _paintView.setVisibility(View.GONE);
- _progressBar.setProgress(0);
- _progressBar.setVisibility(View.VISIBLE);
- _state._savedImageUri = null;
-
- _state._loadInProgress = true;
- _state._loadedResourceId = outlineResourceId;
- _originalOutlineBitmap = BitmapFactory.decodeResource(getResources(),
- outlineResourceId);
- _handler = new Handler()
- {
-
- @Override
- public void handleMessage(Message m)
- {
- switch (m.what)
- {
- case Progress.MESSAGE_INCREMENT_PROGRESS:
- // Update progress bar.
- _progressBar.incrementProgressBy(m.arg1);
- break;
- case Progress.MESSAGE_DONE_OK:
- case Progress.MESSAGE_DONE_ERROR:
- // We are done, hide the progress bar and turn
- // the paint view back on.
- _state._loadInProgress = false;
- _paintView.setVisibility(View.VISIBLE);
- _progressBar.setVisibility(View.GONE);
- break;
- }
- }
- };
-
- new Thread(this).start();
- }
-
- public void run()
- {
- _paintView.loadFromBitmap(_originalOutlineBitmap, _handler);
- }
- private Bitmap _originalOutlineBitmap;
- private Handler _handler;
- }
-
- // Class needed to work-around gallery crash bug. If we do not have this
- // scanner then the save succeeds but the Pictures app will crash when
- // trying to open.
- private class MediaScannerNotifier implements MediaScannerConnectionClient
- {
-
- public MediaScannerNotifier(Context context, String path, String mimeType)
- {
- _path = path;
- _mimeType = mimeType;
- _connection = new MediaScannerConnection(context, this);
- _connection.connect();
- }
-
- public void onMediaScannerConnected()
- {
- _connection.scanFile(_path, _mimeType);
- }
-
- public void onScanCompleted(String path, final Uri uri)
- {
- _connection.disconnect();
- }
- private MediaScannerConnection _connection;
- private String _path;
- private String _mimeType;
- }
-
- public static interface OnSavedListener
- {
-
- void onSaved(String filename);
+ private static final String GALLERY_URL = "https://gallery.quelltext.eu/";
+ private void openGallery() {
+ // open url in browser, see https://stackoverflow.com/a/2201999/1320237
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(GALLERY_URL));
+ startActivity(browserIntent);
}
- private class BitmapSaver implements Runnable
- {
-
- public BitmapSaver()
- {
- class DelayHandler extends Handler
- {
-
- @Override
- public void handleMessage(Message m)
- {
- // We are done, hide the progress bar and turn
- // the paint view back on.
- _saveInProgress = false;
- _progressDialog.dismiss();
- }
- }
-
- class ProgressHandler extends Handler
- {
-
+ private class Preview implements ImagePreview {
+ @Override
+ public void setImage(final Bitmap image) {
+ paintView.post(new Runnable() {
@Override
- public void handleMessage(Message m)
- {
- switch (m.what)
- {
- case Progress.MESSAGE_INCREMENT_PROGRESS:
- // Update progress bar.
- _progressDialog.incrementProgressBy(m.arg1);
- break;
- case Progress.MESSAGE_DONE_OK:
- case Progress.MESSAGE_DONE_ERROR:
- if (m.what == Progress.MESSAGE_DONE_OK)
- {
- finishSaving();
- }
- String title = getString(R.string.dialog_saving);
- if (m.what == Progress.MESSAGE_DONE_OK)
- {
- title += getString(R.string.dialog_saving_ok);
- }
- else
- {
- title += getString(R.string.dialog_saving_error);
- }
- _progressDialog.setTitle(title);
- new DelayHandler().sendEmptyMessageDelayed(0,
- SAVE_DIALOG_WAIT_MILLIS);
- break;
- }
+ public void run() {
+ paintArea.setImageBitmap(image);
}
- }
-
- if (_paintView.isInitialized())
- {
- _saveInProgress = true;
- showDialog(DIALOG_PROGRESS);
- _progressDialog.setTitle(R.string.dialog_saving);
- _progressDialog.setProgress(0);
- _originalOutlineBitmap = BitmapFactory.decodeResource(getResources(),
- _state._loadedResourceId);
- _progressHandler = new ProgressHandler();
- new Thread(this).start();
- }
- }
-
- public void run()
- {
- // Get a filename.
- _fileName = newImageFileName();
- _file = new File(Environment.getExternalStorageDirectory(),
- getString(R.string.saved_image_path_prefix) + _fileName + ".png");
-
- // Save the bitmap to a file.
- _paintView.saveToFile(_file, _originalOutlineBitmap, _progressHandler);
+ });
}
- protected void finishSaving()
- {
- // Save it to the MediaStore.
- ContentValues values = new ContentValues();
- values.put(Images.Media.TITLE, _fileName);
- values.put(Images.Media.DISPLAY_NAME, _fileName);
- values.put(Images.Media.MIME_TYPE, MIME_PNG);
- values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
- values.put(Images.Media.DATA, _file.toString());
- File parentFile = _file.getParentFile();
- values.put(Images.Media.BUCKET_ID,
- parentFile.toString().toLowerCase().hashCode());
- values.put(Images.Media.BUCKET_DISPLAY_NAME,
- parentFile.getName().toLowerCase());
- _newImageUri = getContentResolver().insert(
- Images.Media.EXTERNAL_CONTENT_URI, values);
-
- // Delete the old version, if we have any.
- if (_state._savedImageUri != null)
- {
- getContentResolver().delete(_state._savedImageUri, null, null);
- }
- _state._savedImageUri = _newImageUri;
-
- // Scan the file so that it appears in the system as it should.
- if (_newImageUri != null)
- {
- new MediaScannerNotifier(PaintActivity.this, _file.toString(), MIME_PNG);
- }
+ @Override
+ public int getWidth() {
+ return paintArea.getWidth();
}
- private String newImageFileName()
- {
- final DateFormat fmt = new SimpleDateFormat("yyyyMMdd-HHmmss");
- return fmt.format(new Date());
+ @Override
+ public int getHeight() {
+ return paintArea.getHeight();
}
- private Bitmap _originalOutlineBitmap;
- private String _fileName;
- private File _file;
- private Handler _progressHandler;
- protected Uri _newImageUri;
- }
-
- private class BitmapSharer extends BitmapSaver
- {
- public BitmapSharer()
- {
- super();
+ @Override
+ public InputStream openInputStream(Uri uri) throws FileNotFoundException {
+ return getContentResolver().openInputStream(uri);
}
@Override
- protected void finishSaving()
- {
- super.finishSaving();
-
- if (_newImageUri != null)
- {
- Intent sharingIntent = new Intent(Intent.ACTION_SEND);
- sharingIntent.setType("image/png");
- sharingIntent.putExtra(Intent.EXTRA_STREAM, _newImageUri);
- startActivity(Intent.createChooser(sharingIntent, getString( R.string.dialog_share )));
- }
+ public void done(final Bitmap bitmap) {
+ setImage(bitmap);
}
}
-
- // The state of the whole drawing. This is used to transfer the state if
- // the activity is re-created (e.g. due to orientation change).
- private static class SavedState
- {
-
- public State _paintActivityState;
- public Object _colorButtonState;
- public Object _paintViewState;
- }
-
- private static class State
- {
- // Are we just loading a new outline?
-
- public boolean _loadInProgress;
- // The resource ID of the outline we are coloring.
- public int _loadedResourceId;
- // If we have already saved a copy of the image, we store the URI here
- // so that we can delete the previous version when saved again.
- public Uri _savedImageUri;
- }
- private static final int REQUEST_START_NEW = 0;
- private static final int REQUEST_PICK_COLOR = 1;
- private static final int DIALOG_PROGRESS = 1;
- private static final int SAVE_DIALOG_WAIT_MILLIS = 1500;
- private static final String MIME_PNG = "image/png";
- // The state that we will carry over if the activity is recreated.
- private State _state;
- // Main UI elements.
- private PaintView _paintView;
- private ProgressBar _progressBar;
- private ProgressDialog _progressDialog;
- // The ColorButtonManager makes sure the state of the ColorButtons visible
- // on this activity is in sync.
- private ColorButtonManager _colorButtonManager;
- // Is there a save in progress?
- private boolean _saveInProgress;
}
\ No newline at end of file
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/PickColorActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/PickColorActivity.java
index 7a6ffa5..a1b88c6 100644
--- a/src/main/java/org/androidsoft/coloring/ui/activity/PickColorActivity.java
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/PickColorActivity.java
@@ -22,7 +22,9 @@
import android.view.WindowManager;
import java.util.ArrayList;
import java.util.List;
-import org.androidsoft.coloring.R;
+
+import eu.quelltext.coloring.R;
+
public class PickColorActivity extends AbstractColoringActivity implements
View.OnClickListener
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/SettingsActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/SettingsActivity.java
new file mode 100644
index 0000000..1b20d2e
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/SettingsActivity.java
@@ -0,0 +1,184 @@
+package org.androidsoft.coloring.ui.activity;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.content.FileProvider;
+
+import org.androidsoft.coloring.util.BitmapSaver;
+import org.androidsoft.coloring.util.ScreenUtils;
+import org.androidsoft.coloring.util.Settings;
+import org.androidsoft.coloring.util.images.GalleryImageDB;
+import org.androidsoft.coloring.util.images.SettingsImageDB;
+import org.androidsoft.utils.ui.BasicActivity;
+
+import java.io.File;
+
+import eu.quelltext.coloring.R;
+
+public class SettingsActivity extends BasicActivity {
+
+ private Settings settings;
+ private SettingsImageDB imageDBs;
+ private EditText newUrlInput;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.activity_settings);
+ ScreenUtils.setFullscreen(this);
+
+ settings = Settings.of(this);
+
+ // SAVE LOCATION
+ File saveLocation = BitmapSaver.getSavedImagesDirectory(this);
+ TextView saveLocationText = findViewById(R.id.save_location);
+ saveLocationText.setText(getString(R.string.settings_save_location, saveLocation.toString()));
+ Button openSaveLocation = findViewById(R.id.open_save_location);
+ // open a location with the file exporer
+ // see https://stackoverflow.com/a/26651827/1320237
+ // see https://stackoverflow.com/a/38858040
+ // see https://stackoverflow.com/a/8727354
+ Uri saveLocationUri = FileProvider.getUriForFile(this, this.getPackageName() + ".provider", saveLocation);;
+ final Intent openSaveLocationIntent = new Intent(Intent.ACTION_VIEW);
+ openSaveLocationIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ openSaveLocationIntent.setDataAndType(saveLocationUri, "vnd.android.document/directory");
+ // see http://www.openintents.org/action/android-intent-action-view/file-directory
+ openSaveLocationIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", saveLocation.toString());
+ // from https://github.com/derp-caf/packages_apps_DocumentsUI/blob/ab8e8638c87cd5f7fe1005800520e739b3a48cd5/src/com/android/documentsui/ScopedAccessActivity.java#L114
+ // see https://stackoverflow.com/a/60645628/1320237
+ openSaveLocationIntent.putExtra("com.android.documentsui.FILE", saveLocation.toString());
+ openSaveLocationIntent.putExtra("com.android.documentsui.IS_ROOT", false); // is the root folder?
+ openSaveLocationIntent.putExtra("com.android.documentsui.IS_PRIMARY", true); // is the primary volume? SD-Card should be false
+ openSaveLocationIntent.putExtra("com.android.documentsui.APP_LABEL", getTitle());
+ if (openSaveLocationIntent.resolveActivityInfo(getPackageManager(), 0) != null) {
+ openSaveLocation.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startActivity(openSaveLocationIntent);
+ }
+ });
+ } else {
+ // if you reach this place, it means there is no any file
+ // explorer app installed on your device
+ openSaveLocation.setVisibility(View.GONE);
+ }
+
+ // GALLERIES
+ imageDBs = settings.getImageDB();
+ final View newUrlButton = findViewById(R.id.button_add_url);
+ newUrlInput = findViewById(R.id.input_new_url);
+ newUrlButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String url = newUrlInput.getText().toString();
+ Uri uri;
+ try {
+ uri = Uri.parse(url);
+ } catch (Exception e) {
+ badUrl(newUrlInput);
+ return;
+ }
+ if (!uri.getScheme().startsWith("http")) {
+ badUrl(newUrlInput);
+ return;
+ }
+ if (!imageDBs.addUserDefinedGallery(url)) {
+ badUrl(newUrlInput);
+ return;
+ }
+ loadGalleries();
+ goodUrl(newUrlInput);
+ }
+
+ private void goodUrl(EditText newUrlInput) {
+ newUrlInput.setBackgroundColor(Color.TRANSPARENT);
+ }
+
+ private void badUrl(EditText newUrlInput) {
+ newUrlInput.setBackgroundColor(Color.RED);
+ }
+ });
+ }
+
+ private void loadGalleries() {
+ LinearLayout galleries = findViewById(R.id.galleries);
+ galleries.removeAllViews();
+ for (final SettingsImageDB.Entry db : imageDBs.entries()) {
+ LinearLayout galleryLayout = (LinearLayout) getLayoutInflater().inflate(R.layout.gallery, null);
+ // check box
+ CheckBox checkBox = galleryLayout.findViewById(R.id.in_use);
+ checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean activated) {
+ db.setActivation(activated);
+ }
+ });
+ checkBox.setText(db.getName());
+ checkBox.setChecked(db.isActivated());
+ // description
+ TextView description = galleryLayout.findViewById(R.id.text_description);
+ description.setText(db.getDescription());
+ // delete button
+ ImageButton deleteButton = galleryLayout.findViewById(R.id.button_delete);
+ deleteButton.setVisibility(db.canBeDeleted() ? View.VISIBLE : View.GONE);
+ deleteButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ newUrlInput.setText(db.getId());
+ db.delete();
+ loadGalleries();
+
+ }
+ });
+ // browse button
+ ImageButton browseButton = galleryLayout.findViewById(R.id.button_browse);
+ browseButton.setVisibility(db.canBrowse() ? View.VISIBLE : View.GONE);
+ browseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ db.browse(SettingsActivity.this);
+ }
+ });
+ // add to view
+ galleries.addView(galleryLayout);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ loadGalleries();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ ScreenUtils.setFullscreen(this);
+ }
+ }
+
+ @Override
+ public int getMenuResource() {
+ return R.menu.menu_close;
+ }
+
+ @Override
+ public int getMenuCloseId() {
+ return R.id.menu_close;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/SplashActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/SplashActivity.java
index 9d4c2c2..debddc0 100644
--- a/src/main/java/org/androidsoft/coloring/ui/activity/SplashActivity.java
+++ b/src/main/java/org/androidsoft/coloring/ui/activity/SplashActivity.java
@@ -14,22 +14,45 @@
*/
package org.androidsoft.coloring.ui.activity;
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
import android.os.Bundle;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
-import org.androidsoft.coloring.R;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+
+import org.androidsoft.coloring.util.Settings;
import org.androidsoft.utils.ui.WhatsNewActivity;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+
+import eu.quelltext.coloring.R;
+
/**
* Splash activity
* @author Pierre Levy
*/
-public class SplashActivity extends WhatsNewActivity implements OnClickListener
+public class SplashActivity extends WhatsNewActivity
{
+ private static final String CHANGELOG_FOLDER = "changelogs";
+ private int permissionRequest = 0;
private Button mButtonPlay;
@Override
@@ -39,23 +62,57 @@ public void onCreate(Bundle icicle)
setContentView(R.layout.splash);
- mButtonPlay = (Button) findViewById(R.id.button_go);
- mButtonPlay.setOnClickListener(this);
+ OnClickListener closeThis = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SplashActivity.this, PaintActivity.class);
+ startActivity(intent);
+ }
+ };
+
+ mButtonPlay = findViewById(R.id.button_go);
+ mButtonPlay.setOnClickListener(closeThis);
ImageView image = (ImageView) findViewById(R.id.image_splash);
- image.setImageResource(R.drawable.splash);
+ image.setImageResource(R.drawable.ic_logo);
+
+ LinearLayout splashScreen = findViewById(R.id.splash_screen);
+ splashScreen.setOnClickListener(closeThis);
+ TextView versionText = findViewById(R.id.version_text_view);
+ // set the version
+ // see https://developer.android.com/guide/topics/resources/string-resource#java
+ // credits to https://stackoverflow.com/a/51109685/1320237
+ versionText.setText(getString(R.string.credits_current_version, getVersionName()));
+
+ checkForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, R.string.permission_read_external_storage);
+ checkForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, R.string.permission_write_external_storage);
+ checkForPermission(Manifest.permission.EXPAND_STATUS_BAR, R.string.permission_expand_status_bar);
+ checkForPermission(Manifest.permission.SYSTEM_ALERT_WINDOW, R.string.permission_expand_status_bar);
+ if (Settings.of(this).requireInternetConnection()) {
+ checkForPermission(Manifest.permission.ACCESS_WIFI_STATE, R.string.permission_access_wifi_state);
+ }
}
- /**
- * {@inheritDoc }
- */
- public void onClick(View v)
- {
- if (v == mButtonPlay)
- {
- Intent intent = new Intent(this, PaintActivity.class);
- startActivity(intent);
+ public String getVersionName() {
+ // from WhatsNewActivity
+ try {
+ PackageInfo pInfo = this.getPackageManager().getPackageInfo(
+ this.getPackageName(), PackageManager.GET_META_DATA);
+ return pInfo.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return "?";
+ }
+ }
+
+ public int getVersionCode() {
+ // from WhatsNewActivity
+ try {
+ PackageInfo pInfo = this.getPackageManager().getPackageInfo(
+ this.getPackageName(), PackageManager.GET_META_DATA);
+ return pInfo.versionCode;
+ } catch (PackageManager.NameNotFoundException e) {
+ return -1;
}
}
@@ -82,4 +139,91 @@ public int getWhatsNewDialogMsgRes()
{
return R.string.whats_new_dialog_message;
}
+
+ @Override
+ public String getWhatsNewDialogMsgString() {
+ // move changelog slightly by tab
+ String changelog = getChangelog().replace("\n", "\n\t");
+ return getString(getWhatsNewDialogMsgRes(), getVersionName(), changelog);
+ }
+
+ // return the changelog or null if none exists
+ private String getChangelog() {
+ int versionCode = getVersionCode();
+ // for language, see https://stackoverflow.com/a/23168383/1320237
+ String language = Locale.getDefault().getLanguage(); // "en"
+ String country = Locale.getDefault().getCountry(); // "US"
+ String[] filenames = new String[]{
+ getChangelogFileNameForLanguageAndCountry(language, country),
+ getChangelogFileNameForLanguage(language),
+ getChangelogFileNameForLanguageAndCountry(Locale.ENGLISH.getLanguage(), Locale.US.getCountry()),
+ getChangelogFileNameForLanguage(Locale.ENGLISH.getLanguage()),
+ };
+ // opening an asset
+ // see https://inducesmile.com/android-programming/how-to-read-a-file-from-assets-folder-in-android/
+ InputStream in = null;
+ AssetManager assets = getAssets();
+ for (String filename: filenames) {
+ // check if the asset exists, see https://stackoverflow.com/a/38240347/1320237
+ try {
+ in = assets.open(filename, AssetManager.ACCESS_BUFFER);
+ // read all bytes
+ // see https://stackoverflow.com/a/859076/1320237
+ byte[] content = IOUtils.toByteArray(in);
+ return new String(content, "UTF-8");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ private String getChangelogFileNameForLanguage(String language) {
+ return CHANGELOG_FOLDER + "/" + language + "/" + getVersionCode() + ".txt";
+ }
+
+ private String getChangelogFileNameForLanguageAndCountry(String language, String country) {
+ return CHANGELOG_FOLDER + "/" + language + "-" + country + "/" + getVersionCode() + ".txt";
+ }
+
+ public void checkForPermission(final String permissionName, int explanationResourceId) {
+ // check for permissions
+ // see https://developer.android.com/training/permissions/requesting#java
+ if (ContextCompat.checkSelfPermission(this, permissionName) != PackageManager.PERMISSION_GRANTED) {
+
+ // Permission is not granted
+ // Should we show an explanation?
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissionName)) {
+ // Show an explanation to the user *asynchronously* -- don't block
+ // this thread waiting for the user's response! After the user
+ // sees the explanation, try again to request the permission.
+ // see https://stackoverflow.com/a/2115770
+ new AlertDialog.Builder(this)
+ .setMessage(explanationResourceId)
+
+ // Specifying a listener allows you to take an action before dismissing the dialog.
+ // The dialog is automatically dismissed when a dialog button is clicked.
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // Continue with delete operation
+ ActivityCompat.requestPermissions(SplashActivity.this,
+ new String[]{permissionName},
+ permissionRequest++);
+ }
+ })
+
+ // A null listener allows the button to dismiss the dialog and take no further action.
+ .setNegativeButton(android.R.string.no, null)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .show();
+ } else {
+ // No explanation needed; request the permission
+ ActivityCompat.requestPermissions(this,
+ new String[]{permissionName},
+ permissionRequest++);
+ }
+ } else {
+ // Permission has already been granted
+ }
+ }
}
diff --git a/src/main/java/org/androidsoft/coloring/ui/activity/StartNewActivity.java b/src/main/java/org/androidsoft/coloring/ui/activity/StartNewActivity.java
deleted file mode 100644
index 29cac4d..0000000
--- a/src/main/java/org/androidsoft/coloring/ui/activity/StartNewActivity.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2010 Peter Dornbach.
- *
- * 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.androidsoft.coloring.ui.activity;
-
-import java.lang.reflect.Field;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.TreeMap;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.BaseAdapter;
-import android.widget.GridView;
-import android.widget.ImageView;
-import org.androidsoft.coloring.R;
-import org.androidsoft.utils.ui.NoTitleActivity;
-
-public class StartNewActivity extends NoTitleActivity implements View.OnClickListener
-{
- // This is an expensive operation.
-
- public static int randomOutlineId()
- {
- return new ResourceLoader().randomOutlineId();
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
-
- // Apparently this cannot be set from the style.
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
- WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
-
- setContentView(R.layout.start_new);
-
- GridView gridview = (GridView) findViewById(R.id.start_new_grid);
- gridview.setAdapter(new ImageAdapter(this));
- }
-
- public void onClick(View view)
- {
- setResult(view.getId());
- finish();
- }
-
- private static class ResourceLoader
- {
-
- ResourceLoader()
- {
- // Use reflection to list resource ids of thumbnails and outline
- // images.First, we list all the drawables starting with the proper
- // prefixes into 2 maps.
- Map outlineMap = new TreeMap();
- Map thumbMap = new TreeMap();
- Field[] drawables = R.drawable.class.getDeclaredFields();
- for (int i = 0; i < drawables.length; i++)
- {
- String name = drawables[i].getName();
- try
- {
- if (name.startsWith(PREFIX_OUTLINE))
- {
- outlineMap.put(name.substring(PREFIX_OUTLINE.length()),
- drawables[i].getInt(null));
- }
- if (name.startsWith(PREFIX_THUMB))
- {
- thumbMap.put(name.substring(PREFIX_THUMB.length()),
- drawables[i].getInt(null));
- }
- }
- catch (IllegalAccessException e)
- {
- }
- }
- Set keys = outlineMap.keySet();
- keys.retainAll(thumbMap.keySet());
- _outlineIds = new Integer[keys.size()];
- _thumbIds = new Integer[keys.size()];
- int j = 0;
- Iterator i = keys.iterator();
- while (i.hasNext())
- {
- String key = i.next();
- _outlineIds[j] = outlineMap.get(key);
- _thumbIds[j] = thumbMap.get(key);
- j++;
- }
- }
-
- public Integer[] getThumbIds()
- {
- return _thumbIds;
- }
-
- public Integer[] getOutlineIds()
- {
- return _outlineIds;
- }
-
- public int randomOutlineId()
- {
- return _outlineIds[new Random().nextInt(_outlineIds.length)];
- }
- private static final String PREFIX_OUTLINE = "outline";
- private static final String PREFIX_THUMB = "thumb";
- private Integer[] _thumbIds;
- private Integer[] _outlineIds;
- }
-
- private class ImageAdapter extends BaseAdapter
- {
-
- ImageAdapter(Context c)
- {
- _context = c;
- _resourceLoader = new ResourceLoader();
- }
-
- public int getCount()
- {
- return _resourceLoader.getThumbIds().length;
- }
-
- public Object getItem(int i)
- {
- return null;
- }
-
- public long getItemId(int i)
- {
- return 0;
- }
-
- public View getView(int position, View convertView, ViewGroup parent)
- {
- ImageView imageView;
- if (convertView == null)
- {
- // If it's not recycled, initialize some attributes
- imageView = new ImageView(_context);
- imageView.setLayoutParams(new GridView.LayoutParams(145, 145));
- imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
- imageView.setPadding(8, 8, 8, 8);
- imageView.setOnClickListener(StartNewActivity.this);
- }
- else
- {
- imageView = (ImageView) convertView;
- }
-
- imageView.setImageResource(_resourceLoader.getThumbIds()[position]);
- imageView.setId(_resourceLoader.getOutlineIds()[position]);
- return imageView;
- }
- private Context _context;
- private ResourceLoader _resourceLoader;
- }
-}
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/ColorButton.java b/src/main/java/org/androidsoft/coloring/ui/widget/ColorButton.java
index 740babc..f9eca89 100644
--- a/src/main/java/org/androidsoft/coloring/ui/widget/ColorButton.java
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/ColorButton.java
@@ -22,7 +22,8 @@
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.GradientDrawable.Orientation;
import android.util.AttributeSet;
-import org.androidsoft.coloring.R;
+
+import eu.quelltext.coloring.R;
public class ColorButton extends ColoringButton
{
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/ColoringButton.java b/src/main/java/org/androidsoft/coloring/ui/widget/ColoringButton.java
index df3930a..92ab37c 100644
--- a/src/main/java/org/androidsoft/coloring/ui/widget/ColoringButton.java
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/ColoringButton.java
@@ -19,9 +19,10 @@
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
-import org.androidsoft.coloring.R;
import org.androidsoft.coloring.ui.activity.AbstractColoringActivity;
+import eu.quelltext.coloring.R;
+
// A button that is proportional to the screen size.
public class ColoringButton extends View
{
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/ColoringImageButton.java b/src/main/java/org/androidsoft/coloring/ui/widget/ColoringImageButton.java
index 5d4b7a5..135cf12 100644
--- a/src/main/java/org/androidsoft/coloring/ui/widget/ColoringImageButton.java
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/ColoringImageButton.java
@@ -22,7 +22,8 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
-import org.androidsoft.coloring.R;
+
+import eu.quelltext.coloring.R;
public class ColoringImageButton extends ColoringButton
{
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/LoadImageProgress.java b/src/main/java/org/androidsoft/coloring/ui/widget/LoadImageProgress.java
new file mode 100644
index 0000000..1c3649c
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/LoadImageProgress.java
@@ -0,0 +1,112 @@
+package org.androidsoft.coloring.ui.widget;
+
+import android.os.Handler;
+import android.util.Log;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import eu.quelltext.coloring.R;
+
+public class LoadImageProgress {
+
+ private final ProgressBar progressBar;
+ private long lastStep;
+ private TextView textView;
+ private final Handler handler;
+
+ public LoadImageProgress(ProgressBar progressBar, TextView textView) {
+ this.progressBar = progressBar;
+ this.textView = textView;
+ if (progressBar != null) {
+ progressBar.setIndeterminate(false);
+ progressBar.setMax(STEPS);
+ }
+ handler = new Handler();
+ lastStep = System.nanoTime();
+ stepStart();
+
+ }
+
+ private void step(final int step, final int textId) {
+ // stepping should be thread save
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (progressBar != null) {
+ progressBar.setProgress(step);
+ }
+ if (textView != null) {
+ textView.setText(textId);
+ }
+ }
+ });
+ long newStep = System.nanoTime();
+ Log.d("progress", "step " + step + ": " + (newStep - lastStep) / 1000000 + "ms");
+ lastStep = newStep;
+ }
+
+ public void stepStart() {
+ step(0, R.string.progress_starting);
+ }
+
+ public void stepDone() {
+ step(STEPS, R.string.progress_done);
+ }
+
+ public void stepFail() {
+ step(STEPS, R.string.progress_error);
+ }
+
+ public void stepInputPreview() {
+ step(1, R.string.progress_input_preview);
+ }
+
+ public void stepPreparingClustering() {
+ step(2, R.string.progress_preparing_clustering);
+ }
+
+ public void stepSampleDataForClassification() {
+ step(3, R.string.progress_sample_data);
+ }
+
+ public void stepClusteringData() {
+ step(4, R.string.progress_clustering_data);
+ }
+
+ public void stepCreateClusterImage() {
+ step(5, R.string.progress_create_cluster);
+ }
+
+ public void stepShowClusterImage() {
+ step(6, R.string.progress_show_cluster);
+ }
+
+ public void stepRemovingNoise() {
+ step(7, R.string.progress_removing_noise);
+ }
+
+ public void stepShowSmoothedImage() {
+ step(8, R.string.progress_show_smoothed_image);
+ }
+
+ public void stepConnectingComponents() {
+ step(9, R.string.progress_connecting_components);
+ }
+
+ public void stepMeasuringAreas() {
+ step(10, R.string.progress_measuring_areas);
+ }
+
+ public void stepShowComponents() {
+ step(11, R.string.progress_show_components);
+ }
+
+ public void stepDrawLinesAround() {
+ step(12, R.string.progress_draw_lines);
+ }
+
+ private static final int STEPS = 15; // do not forget to change when you add steps
+
+ public void stepConvertingToBinaryImage() {
+ step(4, R.string.progress_convert_binary);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/PaintArea.java b/src/main/java/org/androidsoft/coloring/ui/widget/PaintArea.java
new file mode 100644
index 0000000..4bfdead
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/PaintArea.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2010 Peter Dornbach.
+ *
+ * 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.androidsoft.coloring.ui.widget;
+
+import org.androidsoft.coloring.util.BitmapColorSearch;
+import org.androidsoft.coloring.util.FloodFill;
+import org.androidsoft.coloring.util.images.BitmapImage;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.images.NullImage;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import eu.quelltext.images.ColorComparator;
+import eu.quelltext.images.ColorSearch;
+
+public class PaintArea {
+
+ private static final int COLOR_SEARCH_RADIUS = 10;
+ private final ViewGroup.LayoutParams layoutParams;
+ private Bitmap bitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888);
+ private int paintColor;
+ private final ImageView view;
+
+ public PaintArea(ImageView view) {
+ this.view = view;
+ view.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return onTouchEvent(motionEvent);
+ }
+ });
+ layoutParams = view.getLayoutParams();
+ }
+
+
+ public boolean canPaint() {
+ return bitmap != null;
+ }
+
+ public ImageDB.Image getImage() {
+ if (canPaint()) {
+ return new BitmapImage(bitmap);
+ }
+ return new NullImage();
+ }
+
+ public void setImageBitmap(final Bitmap bm) {
+ setImageBitmapWithSameSize(bm);
+ view.setLayoutParams(layoutParams);
+ view.setRotation(0);
+ // use post to defer execution until the size of the view is determined
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ int bmHeight = bm.getHeight();
+ int bmWidth = bm.getWidth();
+ int maxWidth = ((View)view.getParent()).getWidth();
+ int maxHeight = ((View)view.getParent()).getHeight();
+ if ((bmHeight < bmWidth) != (maxHeight < maxWidth)) {
+ // the image would best be rotated
+ // scale it so it fits the maximum bounds
+ view.setRotation(-90); // bottom to bottom
+ float scale1 = maxHeight / (float)bmWidth;
+ float scale2 = maxWidth / (float)bmHeight;
+ float scale;
+ if (scale1 < scale2) {
+ // height determines size
+ // test with image which is longer than wide but does not fit in maxWidth
+ // example: http://gallery.quelltext.eu/images/freesvg.org/beachview.png
+ layoutParams.width = maxHeight;
+ layoutParams.height = maxHeight * bmHeight / bmWidth;
+ } else {
+ // width determines size
+ // test with image which is longer than wide but does fits in maxWidth
+ // example: http://gallery.quelltext.eu/images/freesvg.org/mascarin-parrot.png
+ // at the end of scaling this, the height of the view should equal maxWidth
+ layoutParams.width = maxWidth * bmWidth / bmHeight;
+ layoutParams.height = maxWidth;
+ }
+ } else {
+ float scale1 = maxHeight / (float)bmHeight;
+ float scale2 = maxWidth / (float)bmWidth;
+ if (scale1 < scale2) {
+ // height determines size
+ // test this is the case with the default image from the app
+ layoutParams.width = maxHeight * bmWidth / bmHeight;
+ layoutParams.height = maxHeight;
+ } else {
+ // width determines size
+ // test with an image which is very wide
+ // example: http://gallery.quelltext.eu/images/freesvg.org/cartoon_kids.png
+ // set width and height of view
+ // see https://stackoverflow.com/a/17066696/1320237
+ // see https://stackoverflow.com/a/5042326/1320237
+ layoutParams.width = maxWidth;
+ layoutParams.height = maxWidth * bmHeight / bmWidth;
+ }
+ }
+ view.setLayoutParams(params);
+ }
+ });
+ }
+
+ private void setImageBitmapWithSameSize(Bitmap bitmap) {
+ view.setImageBitmap(bitmap);
+ this.bitmap = bitmap;
+ }
+
+ public void setPaintColor(int color)
+ {
+ paintColor = color;
+ if (paintColor == FloodFill.BORDER_COLOR) {
+ paintColor = paintColor ^ 1;
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent e)
+ {
+ if (e.getAction() == MotionEvent.ACTION_DOWN)
+ {
+ // play default click sound
+ // see https://stackoverflow.com/a/10987791/1320237
+ view.playSoundEffect(SoundEffectConstants.CLICK);
+ // get the correct position with rotation
+ float eventX = e.getX();
+ float eventY = e.getY();
+ // set the position
+ int x = (int)(eventX * bitmap.getWidth() / view.getWidth());
+ int y = (int)(eventY * bitmap.getHeight() / view.getHeight());
+ Log.d("touch", "X (" + e.getRawX() + ") " + eventX + " -> " + x + " | Y (" + e.getRawY() + ") " + eventY + " -> " + y);
+ if (bitmap.getPixel(x, y) == FloodFill.BORDER_COLOR) {
+ // touching a border, we want to find a better place to color
+ BitmapColorSearch search = new BitmapColorSearch(bitmap);
+ search.startSearch(x, y, ColorComparator.unequal(FloodFill.BORDER_COLOR, paintColor), COLOR_SEARCH_RADIUS);
+ if (!search.wasSuccessful()) {
+ search.startSearch(x, y, ColorComparator.unequal(FloodFill.BORDER_COLOR), COLOR_SEARCH_RADIUS);
+ }
+ if (search.wasSuccessful()){
+ x = search.getX();
+ y = search.getY();
+ }
+ }
+ Bitmap newBitmap = FloodFill.fill(bitmap, x, y, paintColor);
+ setImageBitmapWithSameSize(newBitmap);
+ }
+ return true;
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public int getPaintColor() {
+ return paintColor;
+ }
+
+ public int getWidth() {
+ return view.getWidth() == 0 ? view.getWidth() : 640;
+ }
+
+ public int getHeight() {
+ return view.getHeight() == 0 ? view.getHeight() : 480;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/PaintView.java b/src/main/java/org/androidsoft/coloring/ui/widget/PaintView.java
deleted file mode 100644
index 1141cd6..0000000
--- a/src/main/java/org/androidsoft/coloring/ui/widget/PaintView.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2010 Peter Dornbach.
- *
- * 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.androidsoft.coloring.ui.widget;
-
-import org.androidsoft.coloring.util.FloodFill;
-import org.androidsoft.coloring.util.DrawUtils;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import java.io.FileOutputStream;
-
-public class PaintView extends View
-{
-
- public interface LifecycleListener
- {
- // After this method it is allowed to load resources.
-
- public void onPreparedToLoad();
- }
-
- public PaintView(Context context, AttributeSet attrs)
- {
- super(context, attrs);
- _state = new State();
- _paint = new Paint();
-
- }
-
- public PaintView(Context context)
- {
- this(context, null);
- }
-
- public synchronized void setLifecycleListener(LifecycleListener l)
- {
- _lifecycleListener = l;
-
- }
-
- public synchronized Object getState()
- {
- return _state;
- }
-
- public synchronized void setState(Object o)
- {
- _state = (State) o;
- }
-
- public void loadFromBitmap(Bitmap originalOutlineBitmap,
- Handler progressHandler)
- {
- // Proportion of progress in various places.
- // The sum of all progress should be 100.
- final int PROGRESS_RESIZE = 10;
- final int PROGRESS_SCAN = 90;
-
- int w = 0;
- int h = 0;
- State newState = new State();
- synchronized (this)
- {
- w = _state._width;
- h = _state._height;
- newState._color = _state._color;
- newState._width = w;
- newState._height = h;
- }
- final int n = w * h;
-
- // Resize so that it matches our paint size.
- Bitmap resizedBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- DrawUtils.convertSizeClip(originalOutlineBitmap, resizedBitmap);
- Progress.sendIncrementProgress(progressHandler, PROGRESS_RESIZE);
-
- // Scan through the bitmap. We create the "outline" bitmap that is
- // completely black and has the alpha channel set only. We also
- // create the "mask" that we will use later when filling.
- newState._outlineBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- newState._paintMask = new byte[n];
- {
- int pixels[] = new int[n];
- resizedBitmap.getPixels(pixels, 0, w, 0, 0, w, h);
- for (int i2 = 0; i2 < PROGRESS_SCAN; i2++)
- {
- final int iStart = i2 * n / PROGRESS_SCAN;
- final int iEnd = (i2 + 1) * n / PROGRESS_SCAN;
- for (int i = iStart; i < iEnd; i++)
- {
- int alpha = 255 - DrawUtils.brightness(pixels[i]);
- newState._paintMask[i] = (alpha < ALPHA_TRESHOLD ? (byte) 1
- : (byte) 0);
- pixels[i] = alpha << 24;
- }
- Progress.sendIncrementProgress(progressHandler, 1);
- }
- newState._outlineBitmap.setPixels(pixels, 0, w, 0, 0, w, h);
- }
-
- // Initialize the rest.
- newState._paintedBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- newState._paintedBitmap.eraseColor(Color.WHITE);
- newState._workingMask = new byte[n];
- newState._pixels = new int[n];
- Arrays.fill(newState._pixels, Color.WHITE);
-
- // Commit our changes. So far we have only worked on local variables
- // so we only synchronize now.
- synchronized (this)
- {
- _state = newState;
- }
- progressHandler.sendEmptyMessage(Progress.MESSAGE_DONE_OK);
- }
-
- public synchronized void saveToFile(File file, Bitmap originalOutlineBitmap,
- Handler progressHandler)
- {
- // Proportion of progress in various places.
- // The sum of all progress should be 100.
- final int PROGRESS_SCAN_PAINTED = 25;
- final int PROGRESS_DRAW_PAINTED = 5;
- final int PROGRESS_SCAN_OUTLINE = 45;
- final int PROGRESS_DRAW_OUTLINE = 10;
- final int PROGRESS_SAVE = 15;
-
- // First, get a copy of the painted bitmap. After that we do not have
- // to deal with class instance any more.
- Bitmap painted;
- // synchronized (this) // already synchronized
- {
- painted = _state._paintedBitmap.copy(_state._paintedBitmap.getConfig(),
- true);
- }
- // Now, scan over the original painted bitmap to "extend" the painted
- // regions by one pixel to fix accidental white areas because of resizing.
- {
- final int hp = painted.getHeight();
- final int wp = painted.getWidth();
- final int np = hp * wp;
- int[] origPixels = new int[np];
- int[] newPixels = new int[np];
- painted.getPixels(newPixels, 0, wp, 0, 0, wp, hp);
- System.arraycopy(newPixels, 0, origPixels, 0, np);
- for (int y2 = 0; y2 < PROGRESS_SCAN_PAINTED; y2++)
- {
- final int yStart = y2 * hp / PROGRESS_SCAN_PAINTED;
- final int yEnd = (y2 + 1) * hp / PROGRESS_SCAN_PAINTED;
- int p = yStart * wp;
- for (int y = yStart; y < yEnd; y++)
- {
- for (int x = 0; x < wp; x++)
- {
- if (origPixels[p] == Color.WHITE)
- {
- if (x > 0 && origPixels[p - 1] != Color.WHITE)
- {
- newPixels[p] = origPixels[p - 1];
- }
- else if (y > 0 && origPixels[p - wp] != Color.WHITE)
- {
- newPixels[p] = origPixels[p - wp];
- }
- else if (x < wp - 1 && origPixels[p + 1] != Color.WHITE)
- {
- newPixels[p] = origPixels[p + 1];
- }
- else if (y < hp - 1 && origPixels[p + wp] != Color.WHITE)
- {
- newPixels[p] = origPixels[p + wp];
- }
- }
- p++;
- }
- }
- Progress.sendIncrementProgress(progressHandler, 1);
- }
- painted.setPixels(newPixels, 0, wp, 0, 0, wp, hp);
- }
-
- // Calculate the proportions of the result and create it. The result
- // has more pixels than the bitmap we paint, it has the maximum
- // number of pixels possible with the original outline (while
- // maintaining the same aspect ratio as the drawing).
- final float aspectRatio = (float) painted.getWidth() / painted.getHeight();
- int hr = originalOutlineBitmap.getHeight();
- int wr = (int) (hr * aspectRatio);
- if (wr > originalOutlineBitmap.getWidth())
- {
- wr = originalOutlineBitmap.getWidth();
- hr = (int) (wr / aspectRatio);
- }
- int nr = wr * hr;
- Bitmap result = Bitmap.createBitmap(wr, hr, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(result);
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- // Draw and scale the painted bitmap onto the result.
- canvas.drawBitmap(painted, new Rect(0, 0, painted.getWidth(),
- painted.getHeight()), new Rect(0, 0, wr, hr), paint);
- Progress.sendIncrementProgress(progressHandler, PROGRESS_DRAW_PAINTED);
-
- // Process the outline, i.e. get which pixels are transparent and which
- // ones are not. This is almost the same as what we do when loading
- // except that this image has more pixels.
- Bitmap cropped = Bitmap.createBitmap(wr, hr, Bitmap.Config.ARGB_8888);
- {
- int[] pixels = new int[nr];
- // While getting the pixels, we also crop the unneeded parts.
- originalOutlineBitmap.getPixels(pixels, 0, wr,
- (originalOutlineBitmap.getWidth() - wr) / 2,
- (originalOutlineBitmap.getHeight() - hr) / 2, wr, hr);
- for (int i2 = 0; i2 < PROGRESS_SCAN_OUTLINE; i2++)
- {
- final int iStart = i2 * nr / PROGRESS_SCAN_OUTLINE;
- final int iEnd = (i2 + 1) * nr / PROGRESS_SCAN_OUTLINE;
- for (int i = iStart; i < iEnd; i++)
- {
- int alpha = 255 - DrawUtils.brightness(pixels[i]);
- pixels[i] = alpha << 24;
- }
- Progress.sendIncrementProgress(progressHandler, 1);
- }
- cropped.setPixels(pixels, 0, wr, 0, 0, wr, hr);
- }
-
- // As a final drawing step, draw the outline onto the result.
- canvas.drawBitmap(cropped, 0, 0, paint);
- Progress.sendIncrementProgress(progressHandler, PROGRESS_DRAW_OUTLINE);
-
- try
- {
- // Write the result to the dest file.
- file.getParentFile().mkdirs();
- OutputStream outStream = new FileOutputStream(file);
- result.compress(Bitmap.CompressFormat.PNG, 90, outStream);
- outStream.close();
- Progress.sendIncrementProgress(progressHandler, PROGRESS_SAVE);
- }
- catch (IOException e)
- {
- progressHandler.sendEmptyMessage(Progress.MESSAGE_DONE_ERROR);
- return;
- }
-
- progressHandler.sendEmptyMessage(Progress.MESSAGE_DONE_OK);
- }
-
- public synchronized boolean isInitialized()
- {
- return _state._paintedBitmap != null;
- }
-
- public synchronized void setPaintColor(int color)
- {
- _state._color = color;
- _paint.setColor(color);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh)
- {
- super.onSizeChanged(w, h, oldw, oldh);
-
- synchronized (this)
- {
- if (_state._width == 0 || _state._height == 0)
- {
- _state._width = w;
- _state._height = h;
- if (_lifecycleListener != null)
- {
- _lifecycleListener.onPreparedToLoad();
- }
- }
- }
- }
-
-
-
- @Override
- protected synchronized void onDraw(Canvas canvas)
- {
- if (_state._paintedBitmap != null)
- {
- canvas.drawBitmap(_state._paintedBitmap, 0, 0, _paint);
- }
- if (_state._outlineBitmap != null)
- {
- canvas.drawBitmap(_state._outlineBitmap, 0, 0, _paint);
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent e)
- {
- if (e.getAction() == MotionEvent.ACTION_DOWN)
- {
- paint((int) e.getX(), (int) e.getY());
- }
- return true;
- }
-
- private synchronized void paint(int x, int y)
- {
- // Copy the original mask to the working mask because it will be
- // modified.
- System.arraycopy(_state._paintMask, 0, _state._workingMask, 0,
- _state._width * _state._height);
-
- // Do the nasty stuff.
- FloodFill.fillRaw(x, y, _state._width, _state._height, _state._workingMask,
- _state._pixels, _state._color);
-
- // And now copy all the pixels back.
- _state._paintedBitmap.setPixels(_state._pixels, 0, _state._width, 0, 0,
- _state._width, _state._height);
- invalidate();
- }
- private static final int ALPHA_TRESHOLD = 224;
- // The listener whom we notify when ready to load images.
- private LifecycleListener _lifecycleListener;
-
- // We keep the state of the current drawing in a different class so that
- // we can quickly save and restore it when an orientation change happens.
- // Members of this class are not allowed to contain any references to the
- // view hierarchy.
- private static class State
- {
- // Bitmap containing the outlines that are never changed.
-
- private Bitmap _outlineBitmap;
- // Bitmap containing everything we have painted so far.
- private Bitmap _paintedBitmap;
- // Dimensions of both bitmaps.
- private int _height;
- private int _width;
- // Paint with the currently selected color.
- private int _color;
- // paintMask has 0 for each pixel that cannot be modified and 1
- // for each one that can.
- private byte _paintMask[];
- // workingMask is in fact only needed during the fill - it is a copy
- // of paintMask that is modified during the fill. To avoid reallocating
- // it each time we store it as a member.
- private byte _workingMask[];
- // All the pixels in _paintedBitmap. Because accessing an int array is
- // much faster than accessing pixels in a bitmap, we operate on this
- // and use setPixels() on the bitmap to copy them back.
- private int _pixels[];
- }
- private State _state;
- private Paint _paint;
-}
diff --git a/src/main/java/org/androidsoft/coloring/ui/widget/PreCachingLayoutManager.java b/src/main/java/org/androidsoft/coloring/ui/widget/PreCachingLayoutManager.java
new file mode 100644
index 0000000..a542830
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/ui/widget/PreCachingLayoutManager.java
@@ -0,0 +1,33 @@
+package org.androidsoft.coloring.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/* This layout manager loads images ahead.
+ * see https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager#setsmoothscrollbarenabled
+ * thanks to https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager#getextralayoutspace
+ * thanks to https://github.com/ovy9086/recyclerview-playground/blob/master/app/src/main/java/com/olu/recyclerview/widget/PreCachingLayoutManager.java
+ * thanks to https://androiddevx.wordpress.com/2014/12/05/recycler-view-pre-cache-views/
+ * thanks to https://github.com/facebook/fresco/issues/335#issuecomment-110280822
+ * also https://github.com/ovy9086/recyclerview-playground/blob/master/app/src/main/java/com/olu/recyclerview/fragments/CatsListFragment.java#L45
+ * deprecated, see https://developer.android.com/reference/androidx/recyclerview/widget/LinearLayoutManager?hl=en#getExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State)
+ * see https://developer.android.com/reference/androidx/recyclerview/widget/LinearLayoutManager?hl=en#calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State,%20int%5B%5D)
+ */
+public class PreCachingLayoutManager extends LinearLayoutManager {
+ private final int extraLayoutSpace;
+
+ public PreCachingLayoutManager(Context context, int extraLayoutSpace) {
+ super(context);
+ this.extraLayoutSpace = extraLayoutSpace;
+ }
+
+ @Override
+ protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, @NonNull int[] extraLayoutSpace) {
+ extraLayoutSpace[0] = this.extraLayoutSpace;
+ extraLayoutSpace[1] = this.extraLayoutSpace;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/BitmapColorSearch.java b/src/main/java/org/androidsoft/coloring/util/BitmapColorSearch.java
new file mode 100644
index 0000000..567583b
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/BitmapColorSearch.java
@@ -0,0 +1,11 @@
+package org.androidsoft.coloring.util;
+
+import android.graphics.Bitmap;
+
+import eu.quelltext.images.ColorSearch;
+
+public class BitmapColorSearch extends ColorSearch {
+ public BitmapColorSearch(Bitmap bitmap) {
+ super(new BitmapConverter(bitmap).getPixelsOfBitmap(), bitmap.getWidth(), bitmap.getHeight());
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/BitmapConverter.java b/src/main/java/org/androidsoft/coloring/util/BitmapConverter.java
new file mode 100644
index 0000000..1758a4f
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/BitmapConverter.java
@@ -0,0 +1,51 @@
+package org.androidsoft.coloring.util;
+
+import android.graphics.Bitmap;
+
+/* This class is a base class to wrap eu.quelltext.images pixel arrays with bitmaps.
+ * create(bitmap) --> getPixelsOfBitmap() --> convert --> getPixelsForNewBitmap() --> getNewBitmap()
+ */
+public class BitmapConverter {
+
+ private final int width;
+ private final int height;
+ private Bitmap bitmap;
+ private int[] pixels;
+
+ public BitmapConverter(Bitmap bitmap) {
+ // fill a pixel in a bitmap https://stackoverflow.com/a/5916506
+ width = bitmap.getWidth();
+ height = bitmap.getHeight();
+ this.bitmap = bitmap;
+ }
+
+ public int[] getPixelsOfBitmap() {
+ if (pixels != null) {
+ return pixels;
+ }
+ pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+ bitmap = null; // delete the bitmap when we do not need it
+ return pixels;
+ }
+
+ public int[] getPixelsForNewBitmap() {
+ return getPixelsOfBitmap();
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public Bitmap getNewBitmap() {
+ // create a new bitmap
+ // see https://stackoverflow.com/a/10180908
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(getPixelsForNewBitmap(), 0, width, 0, 0, width, height);
+ return bitmap;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/BitmapSaver.java b/src/main/java/org/androidsoft/coloring/util/BitmapSaver.java
new file mode 100644
index 0000000..092095c
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/BitmapSaver.java
@@ -0,0 +1,133 @@
+package org.androidsoft.coloring.util;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+
+import org.androidsoft.coloring.ui.activity.PaintActivity;
+import org.androidsoft.coloring.ui.widget.Progress;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import eu.quelltext.coloring.R;
+
+import static android.os.Environment.DIRECTORY_DCIM;
+
+public class BitmapSaver implements Runnable
+{
+ public static final String MIME_PNG = "image/png";
+
+ protected final Context context;
+ private File file;
+ private Bitmap bitmap;
+ private Thread thread = null;
+ private Uri imageUri;
+
+ public BitmapSaver(Context context, Bitmap bitmap)
+ {
+ this.context = context;
+ this.bitmap = bitmap;
+ file = newFileName();
+ thread = new Thread(this);
+ }
+
+ public void start() {
+ thread.start();
+ }
+
+ public File newFileName() {
+ // Get a filename.
+ String filename = newImageFileName();
+ File directory = getSavedImagesDirectory(context);
+ return new File(directory, filename);
+ }
+
+ public static File getSavedImagesDirectory(Context context) {
+ File directory = new File(
+ Environment.getExternalStoragePublicDirectory(DIRECTORY_DCIM),
+ context.getString(R.string.app_name));
+ directory.mkdirs();
+ return directory;
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ public void run() {
+ File file = getFile();
+ // save the bitmap, see https://stackoverflow.com/a/673014
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(file);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return;
+ }
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // bmp is your Bitmap instance
+ } catch (NullPointerException e) { // if the directory is removed
+ e.printStackTrace();
+ return;
+ }
+ saveToURI();
+ }
+
+ protected void saveToURI() {
+ File file = getFile();
+ String filename = file.getName();
+ // Save it to the MediaStore.
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Images.Media.TITLE, filename);
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
+ values.put(MediaStore.Images.Media.MIME_TYPE, MIME_PNG);
+ values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
+ values.put(MediaStore.Images.Media.DATA, file.toString());
+ File parentFile = file.getParentFile();
+ values.put(MediaStore.Images.Media.BUCKET_ID,
+ parentFile.toString().toLowerCase().hashCode());
+ values.put(MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
+ parentFile.getName().toLowerCase());
+ imageUri = context.getContentResolver().insert(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+
+ // Scan the file so that it appears in the system as it should.
+ if (imageUri != null)
+ {
+ new MediaScannerNotifier(context, file.toString(), MIME_PNG);
+ }
+ }
+
+ public Uri getImageUri() {
+ return imageUri;
+ }
+
+ private String newImageFileName()
+ {
+ final DateFormat fmt = new SimpleDateFormat("yyyyMMdd-HHmmss");
+ return fmt.format(new Date()) + ".png";
+ }
+
+ public boolean isRunning() {
+ return thread.isAlive();
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public void alreadySaved(BitmapSaver bitmapSaver) {
+
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/BitmapSharer.java b/src/main/java/org/androidsoft/coloring/util/BitmapSharer.java
new file mode 100644
index 0000000..9ddb4f4
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/BitmapSharer.java
@@ -0,0 +1,43 @@
+package org.androidsoft.coloring.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.activity.PaintActivity;
+
+import eu.quelltext.coloring.R;
+
+public class BitmapSharer extends BitmapSaver
+{
+
+ public BitmapSharer(Context context, Bitmap bitmap) {
+ super(context, bitmap);
+ }
+
+ @Override
+ protected void saveToURI()
+ {
+ super.saveToURI();
+ Uri uri = getImageUri();
+ shareUri(uri);
+ }
+
+ private void shareUri(Uri uri) {
+ if (uri != null)
+ {
+ Intent sharingIntent = new Intent(Intent.ACTION_SEND);
+ sharingIntent.setType("image/png");
+ sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ context.startActivity(Intent.createChooser(sharingIntent, context.getString( R.string.dialog_share )));
+ }
+ }
+
+ @Override
+ public void alreadySaved(BitmapSaver bitmapSaver) {
+ super.alreadySaved(bitmapSaver);
+ Uri uri = bitmapSaver.getImageUri();
+ shareUri(uri);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/DrawUtils.java b/src/main/java/org/androidsoft/coloring/util/DrawUtils.java
index a53e1c8..c39ae91 100644
--- a/src/main/java/org/androidsoft/coloring/util/DrawUtils.java
+++ b/src/main/java/org/androidsoft/coloring/util/DrawUtils.java
@@ -30,14 +30,8 @@ public static void convertSizeClip(Bitmap src, Bitmap dest) {
RectF srcRect = new RectF(0, 0, src.getWidth(), src.getHeight());
RectF destRect = new RectF(0, 0, dest.getWidth(), dest.getHeight());
- // Because the current SDK does not directly support the "dest fits
- // inside src" mode, we calculate the reverse matrix and invert to
- // get what we want.
- Matrix mDestSrc = new Matrix();
- mDestSrc.setRectToRect(destRect, srcRect, Matrix.ScaleToFit.CENTER);
Matrix mSrcDest = new Matrix();
- mDestSrc.invert(mSrcDest);
-
+ mSrcDest.setRectToRect(srcRect, destRect, Matrix.ScaleToFit.CENTER);
canvas.drawBitmap(src, mSrcDest, new Paint(Paint.DITHER_FLAG));
}
diff --git a/src/main/java/org/androidsoft/coloring/util/FloodFill.java b/src/main/java/org/androidsoft/coloring/util/FloodFill.java
index a7b5e34..409760f 100644
--- a/src/main/java/org/androidsoft/coloring/util/FloodFill.java
+++ b/src/main/java/org/androidsoft/coloring/util/FloodFill.java
@@ -16,108 +16,57 @@
package org.androidsoft.coloring.util;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.Queue;
+import android.graphics.Bitmap;
+import android.graphics.Color;
-public class FloodFill {
- public interface PixelMatcher {
- // Return true if the pixel at x,y should be filled. It must be prepared
- // to handle x < 0, y < 0. It must return false for a pixel already set.
- boolean match(int x, int y);
+import eu.quelltext.images.BlackAndWhiteConversion;
+
+public class FloodFill extends BitmapConverter {
+ public static int BORDER_COLOR = Color.BLACK;
+ public static int BACKGROUND_COLOR = Color.WHITE;
+ private final int color;
+ private final eu.quelltext.images.FloodFill floodFill;
+
+ public FloodFill(Bitmap bitmap, int color) {
+ super(bitmap);
+ this.color = color;
+ floodFill = new eu.quelltext.images.FloodFill(getPixelsOfBitmap(), getWidth(), getHeight(), BORDER_COLOR);
}
- public interface PixelSetter {
- // Set the a row of pixels from x1 until x2 in row y.
- // x1 included, x2 excluded.
- void set(int x1, int x2, int y);
+ public static Bitmap fill(Bitmap bitmap, int x, int y, int color) {
+ FloodFill fill = new FloodFill(bitmap, color);
+ fill.fillAt(x, y);
+ return fill.getNewBitmap();
}
- // The "nice" fill that works with any PixelMatcher and PixelSetter.
- public static void fill(int x, int y, PixelMatcher matcher, PixelSetter setter) {
- Queue queue = new LinkedList();
- queue.add(new Pixel(x, y));
- while (!queue.isEmpty()) {
- Pixel p = queue.remove();
- int px1 = p._x;
- int px2 = p._x;
- int py = p._y;
- if (matcher.match(px1, py)) {
- while (matcher.match(px1, py))
- px1--;
- px1++;
- while (matcher.match(px2, py))
- px2++;
- boolean prevMatchUp = false;
- boolean prevMatchDn = false;
- setter.set(px1, px2, py);
- for (int px = px1; px < px2; px++) {
- boolean matchUp = matcher.match(px, py - 1);
- if (matchUp && !prevMatchUp)
- queue.add(new Pixel(px, py - 1));
- boolean matchDn = matcher.match(px, py + 1);
- if (matchDn && !prevMatchDn)
- queue.add(new Pixel(px, py + 1));
- prevMatchUp = matchUp;
- prevMatchDn = matchDn;
- }
- }
- }
+ private void fillAt(int x, int y) {
+ floodFill.fillAt(x, y, color);
}
- // The plain dumb ugly fill - but because it's fast we use this one.
- // @param mask 1 if the pixel can be filled, 0 if it cannot. Must contain
- // exactly width * height pixels.
- // @param pixels the array where we fill matching pixels with color.
- // @param x, y we start the fill here.
- public static void fillRaw(int x, int y, int width, int height, byte[] mask,
- int[] pixels, int color) {
- Queue queue = new LinkedList();
- queue.add(new Pixel(x, y));
- while (!queue.isEmpty()) {
- Pixel p = queue.remove();
- int px1 = p._x;
- int px2 = p._x;
- int py = p._y;
- int pp = py * width;
+ @Override
+ public int[] getPixelsForNewBitmap() {
+ return super.getPixelsForNewBitmap();
+ }
- if (mask[pp + px1] != 0) {
- while (px1 >= 0 && mask[pp + px1] != 0)
- px1--;
- px1++;
- while (px2 < width && mask[pp + px2] != 0)
- px2++;
- Arrays.fill(pixels, pp + px1, pp + px2, color);
- Arrays.fill(mask, pp + px1, pp + px2, (byte) 0);
- boolean prevMatchUp = false;
- boolean prevMatchDn = false;
- int ppUp = pp - width;
- int ppDn = pp + width;
- for (int px = px1; px < px2; px++) {
- if (py > 0) {
- boolean matchUp = mask[ppUp + px] != 0;
- if (matchUp && !prevMatchUp)
- queue.add(new Pixel(px, py - 1));
- prevMatchUp = matchUp;
- }
- if (py + 1 < height) {
- boolean matchDn = mask[ppDn + px] != 0;
- if (matchDn && !prevMatchDn)
- queue.add(new Pixel(px, py + 1));
- prevMatchDn = matchDn;
- }
- }
- }
- }
+ public static Bitmap asBlackAndWhite(Bitmap bitmap) {
+ BlackAndWhite bin = new BlackAndWhite(bitmap);
+ return bin.getNewBitmap();
}
- private static class Pixel {
- public Pixel(int x, int y) {
- _x = x;
- _y = y;
+ public static class BlackAndWhite extends BitmapConverter {
+
+ private final BlackAndWhiteConversion bw;
+
+ public BlackAndWhite(Bitmap bitmap) {
+ super(bitmap);
+ bw = new BlackAndWhiteConversion(BACKGROUND_COLOR, BORDER_COLOR);
}
- public int _x;
- public int _y;
+ @Override
+ public int[] getPixelsForNewBitmap() {
+ int[] pixels = super.getPixelsForNewBitmap();
+ bw.toBlackAndWhite(pixels);
+ return pixels;
+ }
}
}
diff --git a/src/main/java/org/androidsoft/coloring/util/GenericFileProvider.java b/src/main/java/org/androidsoft/coloring/util/GenericFileProvider.java
new file mode 100644
index 0000000..b931aae
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/GenericFileProvider.java
@@ -0,0 +1,9 @@
+package org.androidsoft.coloring.util;
+
+import androidx.core.content.FileProvider;
+
+/* Used to open directories, see https://stackoverflow.com/a/38858040
+ *
+ */
+public class GenericFileProvider extends FileProvider {
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/MediaScannerNotifier.java b/src/main/java/org/androidsoft/coloring/util/MediaScannerNotifier.java
new file mode 100644
index 0000000..d60ff8f
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/MediaScannerNotifier.java
@@ -0,0 +1,33 @@
+package org.androidsoft.coloring.util;
+
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+
+// Class needed to work-around gallery crash bug. If we do not have this
+// scanner then the save succeeds but the Pictures app will crash when
+// trying to open.
+public class MediaScannerNotifier implements MediaScannerConnection.MediaScannerConnectionClient
+{
+
+ public MediaScannerNotifier(Context context, String path, String mimeType)
+ {
+ _path = path;
+ _mimeType = mimeType;
+ _connection = new MediaScannerConnection(context, this);
+ _connection.connect();
+ }
+
+ public void onMediaScannerConnected()
+ {
+ _connection.scanFile(_path, _mimeType);
+ }
+
+ public void onScanCompleted(String path, final Uri uri)
+ {
+ _connection.disconnect();
+ }
+ private MediaScannerConnection _connection;
+ private String _path;
+ private String _mimeType;
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/ScreenUtils.java b/src/main/java/org/androidsoft/coloring/util/ScreenUtils.java
new file mode 100644
index 0000000..dba4c1f
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/ScreenUtils.java
@@ -0,0 +1,143 @@
+package org.androidsoft.coloring.util;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+// Based on http://stackoverflow.com/questions/22265945/full-screen-action-bar-immersive#22560946
+public class ScreenUtils {
+ static public void setFullscreen(Activity act) {
+ if (android.os.Build.VERSION.SDK_INT >= 19) {
+ act.getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ }
+ }
+
+ public static class StatusBarCollapser {
+
+ private final Activity activity;
+ private final WindowManager manager;
+ private List addedViews = new ArrayList<>();
+ private boolean isInterceptingTheStatusBar = false;
+
+ public StatusBarCollapser(Activity activity) {
+ this.activity = activity;
+ manager = ((WindowManager) this.activity.getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE));
+ attachStatusBarListener();
+ }
+
+ private void attachStatusBarListener() {
+ // see https://stackoverflow.com/a/31349378/1320237
+ // see also https://stackoverflow.com/a/36101111/1320237
+ // see also https://stackoverflow.com/a/47103959/1320237
+ // see also https://stackoverflow.com/questions/32224452/android-unable-to-add-window-permission-denied-for-this-window-type#comment62111778_36101111
+ int[] types = new int[]{
+ WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
+ WindowManager.LayoutParams.TYPE_TOAST,
+ WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ };
+ int i = 0;
+ for (int type : types) {
+ WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
+ localLayoutParams.type = type;
+ localLayoutParams.gravity = Gravity.TOP;
+ localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+
+ // this is to enable the notification to recieve touch events
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
+
+ // Draws over status bar
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+
+ localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
+ //https://stackoverflow.com/questions/1016896/get-screen-dimensions-in-pixels
+ int resId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
+ int result = 0;
+ if (resId > 0) {
+ result = activity.getResources().getDimensionPixelSize(resId);
+ }
+
+ localLayoutParams.height = result;
+
+ localLayoutParams.format = PixelFormat.TRANSPARENT;
+
+ StatusBarViewGroup view = new StatusBarViewGroup(this.activity);
+
+ try {
+ manager.addView(view, localLayoutParams);
+ addedViews.add(view);
+ } catch (Exception e2) {
+ // permission is not granted
+ Log.e("collapseStatusBar", "could not attach: type: " + type + " at index: " + i);
+ //e2.printStackTrace();
+ }
+ i++;
+ }
+ isInterceptingTheStatusBar = true;
+ }
+
+ private void detachStatusBarListener() {
+ for (View view: addedViews) {
+ manager.removeView(view);
+ }
+ addedViews.clear();
+ isInterceptingTheStatusBar = false;
+ }
+
+ public void collapse() {
+ try {
+ // see https://stackoverflow.com/a/10380535/1320237
+ @SuppressLint("WrongConstant") Object service = activity.getSystemService("statusbar");
+ Class> statusbarManager = Class.forName("android.app.StatusBarManager");
+ Method collapse = statusbarManager.getMethod("collapse");
+ collapse.setAccessible(true);
+ collapse.invoke(service);
+ } catch (Exception e) {
+ // do not care - does not always work
+ }
+ }
+
+ public void shouldCollapse() {
+ attachStatusBarListener();
+ }
+
+ public void shouldNotCollapse() {
+ detachStatusBarListener();
+ }
+
+ public class StatusBarViewGroup extends ViewGroup {
+
+ public StatusBarViewGroup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ Log.v("StatusBarCollapser", "********** Intercepted Status Bar opening. Should collapse: " + isInterceptingTheStatusBar);
+ return isInterceptingTheStatusBar;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/Settings.java b/src/main/java/org/androidsoft/coloring/util/Settings.java
new file mode 100644
index 0000000..5135f65
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/Settings.java
@@ -0,0 +1,143 @@
+package org.androidsoft.coloring.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+
+import org.androidsoft.coloring.util.cache.Cache;
+import org.androidsoft.coloring.util.cache.FileCache;
+import org.androidsoft.coloring.util.cache.NullCache;
+import org.androidsoft.coloring.util.errors.UIErrorReporter;
+import org.androidsoft.coloring.util.images.GalleryImageDB;
+import org.androidsoft.coloring.util.images.RetrievalOptions;
+import org.androidsoft.coloring.util.images.SettingsImageDB;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.File;
+
+import eu.quelltext.coloring.R;
+
+import static org.androidsoft.coloring.util.cache.SimulateOfflineMode.SIMULATE_OFFLINE_MODE;
+
+public class Settings {
+ public static final Gallery[] DEFAULT_GALLERIES = new Gallery[]{
+ new Gallery("https://gallery.quelltext.eu", R.string.settings_gallery_quelltext),
+ new Gallery("http://gallery.quelltext.eu", R.string.settings_gallery_quelltext_http),
+ };
+ private static final String KEY_SETTINGS = "settings";
+ private static final String URL_CACHE_DIRECTORY = "urls";
+ private final Context context;
+ private final SharedPreferences preferences;
+ private SharedPreferences.Editor editor = null;
+ // also see SimulateOfflineMode
+
+ public Settings(Context context) {
+ this.context = context;
+ preferences = context.getSharedPreferences(KEY_SETTINGS, context.MODE_PRIVATE);
+ }
+
+ public static Settings of(Context context) {
+ return new Settings(context);
+ }
+
+ public SettingsImageDB getImageDB() {
+ SettingsImageDB db = new SettingsImageDB(this);
+ return db;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ public void setStringArray(String key, String[] array) {
+ JSONArray json = new JSONArray();
+ for (String item : array) {
+ json.put(item);
+ }
+ getEditor().putString(key, json.toString());
+ }
+
+ public String[] getStringArray(String key) {
+ String data = preferences.getString(key, null);
+ if (data == null) {
+ return null;
+ }
+ try {
+ JSONArray json = new JSONArray(data);
+ String[] result = new String[json.length()];
+ for (int i = 0; i < json.length(); i++) {
+ result[i] = json.getString(i);
+ }
+ return result;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void save() {
+ editor.apply();
+ editor = null;
+ }
+
+ private SharedPreferences.Editor getEditor() {
+ if (editor == null) {
+ editor = preferences.edit();
+ }
+ return editor;
+ }
+
+ public RetrievalOptions getRetrievalOptions() {
+ File directory = new File(context.getCacheDir(), URL_CACHE_DIRECTORY);
+ Cache cache = new FileCache(directory);
+ boolean networkIsConnected = hasNetworkConnection();
+ //cache = new NullCache();
+ RetrievalOptions options = new RetrievalOptions(cache, networkIsConnected);
+ options.setErrorReporter(UIErrorReporter.of(context));
+ return options;
+ }
+
+ private boolean hasNetworkConnection() {
+ if (SIMULATE_OFFLINE_MODE) {
+ return false;
+ }
+ // check for wifi connection, see https://stackoverflow.com/a/34904367/1320237
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ if (wifiMgr.isWifiEnabled()) { // Wi-Fi adapter is ON
+ WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
+ if( wifiInfo.getNetworkId() == -1 ){
+ return false; // Not connected to an access point
+ }
+ return true; // Connected to an access point
+ }
+ else {
+ return false; // Wi-Fi adapter is OFF
+ }
+ }
+
+ public boolean requireInternetConnection() {
+ SettingsImageDB db = getImageDB();
+ return db.requiresInternetConnection();
+ }
+
+ public static class Gallery {
+
+ private final String url;
+ private final int description;
+
+ public Gallery(String url, int description) {
+ this.url = url;
+ this.description = description;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public int getDescription() {
+ return description;
+ }
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/Cache.java b/src/main/java/org/androidsoft/coloring/util/cache/Cache.java
new file mode 100644
index 0000000..85f672c
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/Cache.java
@@ -0,0 +1,21 @@
+package org.androidsoft.coloring.util.cache;
+
+import android.os.Parcelable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+public interface Cache extends Parcelable {
+ /* Open a stream if it is available.
+ * This means that if the url is available, it is opened.
+ * If the Url is not available and nothing is cached, an IOError is raised.
+ */
+ InputStream openStreamIfAvailable(URL url) throws IOException;
+ /* Return a new cache object which caches requests under a given id.
+ */
+ Cache forId(String id);
+ Cache forId(String id, Date lastModified);
+ boolean isCached(String id);
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/FileCache.java b/src/main/java/org/androidsoft/coloring/util/cache/FileCache.java
new file mode 100644
index 0000000..265abf4
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/FileCache.java
@@ -0,0 +1,66 @@
+package org.androidsoft.coloring.util.cache;
+
+import android.os.Parcel;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+public class FileCache implements Cache {
+ private final File directory;
+
+ public FileCache(File directory) {
+ this.directory = directory;
+ }
+
+ @Override
+ public InputStream openStreamIfAvailable(URL url) throws IOException {
+ return forId(url.toString()).openStreamIfAvailable(url);
+ }
+
+ @Override
+ public FileCacheWithId forId(String id) {
+ return new FileCacheWithId(directory, id);
+ }
+
+ @Override
+ public Cache forId(String id, Date lastModified) {
+ FileCacheWithId cache = forId(id);
+ cache.invalidateIfOlderThan(lastModified);
+ return cache;
+ }
+
+ @Override
+ public boolean isCached(String id) {
+ return forId(id).isCached();
+ }
+
+ protected File getDirectory() {
+ return directory;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(directory.toString());
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public Cache createFromParcel(Parcel parcel) {
+ File directory = new File(parcel.readString());
+ return new FileCache(directory);
+ }
+
+ @Override
+ public Cache[] newArray(int i) {
+ return new Cache[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/FileCacheWithId.java b/src/main/java/org/androidsoft/coloring/util/cache/FileCacheWithId.java
new file mode 100644
index 0000000..012c8fd
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/FileCacheWithId.java
@@ -0,0 +1,152 @@
+package org.androidsoft.coloring.util.cache;
+
+import android.os.Parcel;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+import static org.androidsoft.coloring.util.cache.SimulateOfflineMode.SIMULATE_OFFLINE_MODE;
+
+/* This caches requests to a url under a given id.
+ *
+ */
+public class FileCacheWithId extends FileCache {
+ private static final String[] REMOVE_START = new String[]{"https://", "http://"};
+ private final String id;
+ private final boolean isCached;
+ private Date lastModified = null;
+
+ public FileCacheWithId(File directory, String id) {
+ super(directory);
+ this.id = makeIdReadyForDirectory(id);
+ isCached = getPath().isFile();
+ }
+
+ public static String makeIdReadyForDirectory(String id) {
+ id = id.endsWith("/") ? id + "index" : id;
+ for (String start: REMOVE_START) {
+ if (id.toLowerCase().startsWith(start)) {
+ id = id.substring(start.length());
+ }
+ }
+ id = id.replaceAll("//+", "/");
+ id = id.replaceAll("^/+", "");
+ return id;
+ }
+
+ /* Open a URL and cache it.
+ * 1. If the file is available and up to date, return the file
+ * 2. If the file is available and not up to date and the url is available, return the url
+ * 3. If the file is available and not up to date and the url is not available, return the file
+ * 4. If the file is not available, and the url is available, return the url
+ * 5. If the file is not available and the url is not available, throw an IOException.
+ */
+ @Override
+ public InputStream openStreamIfAvailable(URL url) throws IOException {
+ File path = getPath();
+ if (path.isFile()) {
+ /* case 1, 2, 3 */
+ boolean upToDate = isUpToDate(path);
+ if (upToDate) {
+ /* case 1 */
+ try {
+ // return a stream from file
+ // see https://www.baeldung.com/convert-file-to-input-stream
+ return new FileInputStream(path);
+ } catch (IOException e) {
+ /* case 4, 5 */
+ e.printStackTrace(); // This should not happen.
+ return retrieveFromUrlToFile(path, url);
+ }
+ } else {
+ try {
+ /* case 2 */
+ // return a stream from file
+ // see https://www.baeldung.com/convert-file-to-input-stream
+ return retrieveFromUrlToFile(path, url);
+ } catch (IOException e) {
+ /* case 3, 5 */
+ return new FileInputStream(path);
+ }
+ }
+ } else {
+ /* case 4, 5 */
+ return retrieveFromUrlToFile(path, url);
+ }
+ }
+
+ /* Check the update status.
+ * The lastModified of the file is rounded to seconds.
+ */
+ private boolean isUpToDate(File path) {
+ if (lastModified == null) {
+ return false;
+ }
+ long pathLastModified = path.lastModified() / 1000;
+ long urlLastModified = lastModified.getTime() / 1000;
+ return pathLastModified >= urlLastModified;
+ }
+
+ private InputStream retrieveFromUrlToFile(File path, URL url) throws IOException {
+ if (SIMULATE_OFFLINE_MODE) {
+ throw new IOException("test the offline capabilities");
+ }
+ new File(path.getParent()).mkdirs();
+ InputStream source = url.openStream();
+ FileOutputStream destination = new FileOutputStream(path);
+ IOUtils.copy(source, destination);
+ if (lastModified != null) {
+ // set the time if available
+ path.setLastModified(lastModified.getTime());
+ }
+ return new FileInputStream(path);
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ protected File getPath() {
+ return new File(getDirectory(), getId());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(getDirectory().toString());
+ parcel.writeString(getId());
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public Cache createFromParcel(Parcel parcel) {
+ File directory = new File(parcel.readString());
+ String id = parcel.readString();
+ return new FileCacheWithId(directory, id);
+ }
+
+ @Override
+ public Cache[] newArray(int i) {
+ return new Cache[0];
+ }
+ };
+
+ public void invalidateIfOlderThan(Date lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ public boolean isCached() {
+ return isCached;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/ImageCache.java b/src/main/java/org/androidsoft/coloring/util/cache/ImageCache.java
new file mode 100644
index 0000000..07d0b29
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/ImageCache.java
@@ -0,0 +1,9 @@
+package org.androidsoft.coloring.util.cache;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+public interface ImageCache {
+ boolean asPreviewImage(ImageDB.Image image, ImagePreview thumbPreview, LoadImageProgress progress);
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/MemoryImageCache.java b/src/main/java/org/androidsoft/coloring/util/cache/MemoryImageCache.java
new file mode 100644
index 0000000..9cbf71d
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/MemoryImageCache.java
@@ -0,0 +1,66 @@
+package org.androidsoft.coloring.util.cache;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+public class MemoryImageCache implements ImageCache {
+
+ HashMap cache = new HashMap<>();
+
+ @Override
+ public boolean asPreviewImage(ImageDB.Image image, ImagePreview preview, LoadImageProgress progress) {
+ if (cache.containsKey(image)) {
+ progress.stepDone();
+ preview.done(cache.get(image));
+ return true;
+ } else {
+ image.asPreviewImage(new CachingPreview(image, preview), progress);
+ return false;
+ }
+ }
+
+
+ private class CachingPreview implements ImagePreview {
+ private final ImageDB.Image image;
+ private final ImagePreview preview;
+
+ public CachingPreview(ImageDB.Image image, ImagePreview preview) {
+ this.image = image;
+ this.preview = preview;
+ }
+
+ @Override
+ public void setImage(Bitmap image) {
+ preview.setImage(image);
+ }
+
+ @Override
+ public int getWidth() {
+ return preview.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return preview.getHeight();
+ }
+
+ @Override
+ public InputStream openInputStream(Uri uri) throws FileNotFoundException {
+ return preview.openInputStream(uri);
+ }
+
+ @Override
+ public void done(Bitmap bitmap) {
+ cache.put(image, bitmap);
+ preview.done(bitmap);
+ }
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/NullCache.java b/src/main/java/org/androidsoft/coloring/util/cache/NullCache.java
new file mode 100644
index 0000000..ec3c778
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/NullCache.java
@@ -0,0 +1,55 @@
+package org.androidsoft.coloring.util.cache;
+
+import android.os.Parcel;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+/* This class implements the cache interface but actually caches nothing.
+ *
+ */
+public class NullCache implements Cache {
+ @Override
+ public InputStream openStreamIfAvailable(URL url) throws IOException {
+ return url.openStream();
+ }
+
+ @Override
+ public Cache forId(String id) {
+ return this;
+ }
+
+ @Override
+ public Cache forId(String id, Date lastModified) {
+ return this;
+ }
+
+ @Override
+ public boolean isCached(String id) {
+ return false;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public Cache createFromParcel(Parcel parcel) {
+ return new NullCache();
+ }
+
+ @Override
+ public Cache[] newArray(int i) {
+ return new Cache[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/NullImageCache.java b/src/main/java/org/androidsoft/coloring/util/cache/NullImageCache.java
new file mode 100644
index 0000000..625fc89
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/NullImageCache.java
@@ -0,0 +1,13 @@
+package org.androidsoft.coloring.util.cache;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.images.ImageDB;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+public class NullImageCache implements ImageCache {
+ @Override
+ public boolean asPreviewImage(ImageDB.Image image, ImagePreview preview, LoadImageProgress progress) {
+ image.asPreviewImage(preview, progress);
+ return false;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/cache/SimulateOfflineMode.java b/src/main/java/org/androidsoft/coloring/util/cache/SimulateOfflineMode.java
new file mode 100644
index 0000000..c1f9361
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/cache/SimulateOfflineMode.java
@@ -0,0 +1,6 @@
+package org.androidsoft.coloring.util.cache;
+
+public class SimulateOfflineMode {
+ /* Set this to true if you like to check app behavior in offline mode. */
+ public static final boolean SIMULATE_OFFLINE_MODE = false;
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/errors/ErrorReporter.java b/src/main/java/org/androidsoft/coloring/util/errors/ErrorReporter.java
new file mode 100644
index 0000000..69d5a80
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/errors/ErrorReporter.java
@@ -0,0 +1,5 @@
+package org.androidsoft.coloring.util.errors;
+
+public interface ErrorReporter {
+ void report(Exception e);
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/errors/LoggingErrorReporter.java b/src/main/java/org/androidsoft/coloring/util/errors/LoggingErrorReporter.java
new file mode 100644
index 0000000..4c3a3d2
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/errors/LoggingErrorReporter.java
@@ -0,0 +1,10 @@
+package org.androidsoft.coloring.util.errors;
+
+import android.util.Log;
+
+public class LoggingErrorReporter implements ErrorReporter {
+ @Override
+ public void report(Exception e) {
+ Log.e("Error:", e.toString(), e);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/errors/UIErrorReporter.java b/src/main/java/org/androidsoft/coloring/util/errors/UIErrorReporter.java
new file mode 100644
index 0000000..ecfbae3
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/errors/UIErrorReporter.java
@@ -0,0 +1,61 @@
+package org.androidsoft.coloring.util.errors;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.Toast;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import eu.quelltext.coloring.R;
+
+public class UIErrorReporter implements ErrorReporter {
+ private final Context context;
+
+ public UIErrorReporter(Context context) {
+
+ this.context = context;
+ }
+
+ public static UIErrorReporter of(Context context) {
+ return new UIErrorReporter(context);
+ }
+
+ @Override
+ public void report(final Exception e) {
+ e.printStackTrace();
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ String message = context.getResources().getString(R.string.error_occurred_toast);
+ Toast.makeText(context, message, Toast.LENGTH_LONG).show();
+ String report = getReport(e);
+ copyToClipboard(report);
+ }
+ });
+ }
+
+ private String getReport(Exception e) {
+ String email = context.getResources().getString(R.string.error_email);
+ String link = context.getResources().getString(R.string.error_link);
+ String message = context.getResources().getString(R.string.error_occurred_message, email, link);
+ String report = message + "\n\n" + errorToString(e);
+ return report;
+ }
+
+ private String errorToString(Exception e) {
+ // convert an error to a string, see https://stackoverflow.com/a/4812589/1320237
+ StringWriter errors = new StringWriter();
+ e.printStackTrace(new PrintWriter(errors));
+ return errors.toString();
+ }
+
+ public void copyToClipboard(String text) {
+ android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ String appName = context.getResources().getString(R.string.app_name);
+ String title = context.getResources().getString(R.string.error_report_title, appName);
+ android.content.ClipData clip = android.content.ClipData.newPlainText(title, text);
+ clipboard.setPrimaryClip(clip);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/BitmapHash.java b/src/main/java/org/androidsoft/coloring/util/images/BitmapHash.java
new file mode 100644
index 0000000..2ab2ee1
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/BitmapHash.java
@@ -0,0 +1,15 @@
+package org.androidsoft.coloring.util.images;
+
+import android.graphics.Bitmap;
+
+public class BitmapHash {
+ public static int hash(Bitmap bitmap) {
+ int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
+ bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+ int hash = 0;
+ for (int i = 0; i < pixels.length; i++) {
+ hash += pixels[i];
+ }
+ return hash & 0xffffffff;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/BitmapImage.java b/src/main/java/org/androidsoft/coloring/util/images/BitmapImage.java
new file mode 100644
index 0000000..d731028
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/BitmapImage.java
@@ -0,0 +1,76 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Parcel;
+import android.util.Log;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import java.io.ByteArrayOutputStream;
+
+public class BitmapImage implements ImageDB.Image {
+ private Bitmap bitmap;
+
+ public BitmapImage(Bitmap bitmap) {
+ this.bitmap = bitmap;
+ }
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ // create a scaled down version of a bitmap
+ // see https://stackoverflow.com/a/4837803
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, preview.getWidth(), preview.getHeight(), false);
+ progress.stepDone();
+ preview.done(bitmap);
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return true;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepDone();
+ preview.done(bitmap);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ // sending a bitmap
+ // see https://stackoverflow.com/a/11010565
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ byte[] byteArray = stream.toByteArray();
+ parcel.writeInt(byteArray.length);
+ parcel.writeByteArray(byteArray);
+ Log.d("BitmapImage->toParcel", "size " + byteArray.length / 1024 + "kb");
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ // sending a bitmap
+ // see https://stackoverflow.com/a/11010565
+ int length = parcel.readInt();
+ byte[] byteArray = new byte[length];
+ parcel.readByteArray(byteArray);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
+ return new BitmapImage(bitmap);
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/DirectoryImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/DirectoryImageDB.java
new file mode 100644
index 0000000..2f6c327
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/DirectoryImageDB.java
@@ -0,0 +1,65 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+
+import org.androidsoft.coloring.util.BitmapSaver;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DirectoryImageDB implements ImageDB {
+
+ private final File directory;
+
+ public DirectoryImageDB(File directory) {
+ this.directory = directory;
+ }
+
+ public static ImageDB atSaveLocationOf(Context context) {
+ File directory = BitmapSaver.getSavedImagesDirectory(context);
+ return new DirectoryImageDB(directory);
+ }
+
+ @Override
+ public int size() {
+ return getFileList().size();
+ }
+
+ protected List getFileList() {
+ return imagesInDirectory(directory);
+ }
+
+ protected List imagesInDirectory(File directory) {
+ File[] files = directory.listFiles();
+ List images = new ArrayList<>();
+ if (files == null) {
+ return images; // directory does not exist
+ }
+ for (File file : files) {
+ if (isImage(file)) {
+ // add latest files to the front
+ images.add(0, file);
+ }
+ }
+ return images;
+ }
+
+ protected boolean isImage(File file) {
+ return file.getName().toLowerCase().endsWith(".png");
+ }
+
+ @Override
+ public Image get(int index) {
+ List files = getFileList();
+ if (index >= files.size()) {
+ return new NullImage();
+ }
+ File file = files.get(index);
+ return PreparedUriImage.fromPaintedImageFile(file);
+ }
+
+ @Override
+ public void attachObserver(Subject.Observer observer) {
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/GalleryImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/GalleryImageDB.java
new file mode 100644
index 0000000..b743fa0
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/GalleryImageDB.java
@@ -0,0 +1,244 @@
+package org.androidsoft.coloring.util.images;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import org.androidsoft.coloring.util.cache.Cache;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class GalleryImageDB extends Subject implements ImageDB, Runnable {
+
+ private ImageDB db;
+ /* This the gallery Jekyll date format.
+ * "2020-03-10T22:16:41+01:00"
+ * see https://stackoverflow.com/a/4216767/1320237
+ * see https://learn.cloudcannon.com/jekyll-cheat-sheet/
+ * see https://stackoverflow.com/a/3914498/1320237
+ */
+ // public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH);
+
+ public static Date parseTimeStamp(String dateString) throws NumberFormatException {
+ // using the DATE_FORMAT did not work
+ int year = parseInt(dateString, 0, 4, 1000, 3000);
+ int month = parseInt(dateString, 5, 2, 1, 13) - 1;
+ int day = parseInt(dateString, 8, 2, 1, 32);
+ int hour = parseInt(dateString, 11, 2, 0, 24);
+ int minute = parseInt(dateString, 14, 2, 0, 60);
+ int second = parseInt(dateString, 17, 2, 1, 13);
+ String tzSign = dateString.substring(19, 20);
+ String tz = dateString.substring(19);
+ int tzHour = parseInt(dateString, 20, 2, 0, 13);
+ int tzMinute = parseInt(dateString, 23, 2, 0, 60);
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.DAY_OF_MONTH, day);
+ calendar.set(Calendar.HOUR_OF_DAY, hour);
+ calendar.set(Calendar.MINUTE, minute);
+ calendar.set(Calendar.SECOND, second);
+ //calendar.set(Calendar.ZONE_OFFSET, (tzSign.equals("+") ? 1 : -1) * (tzHour * 60 + tzMinute) * 1000);
+ //String tz = (tzSign.equals("+") ? tzHour : 24 - tzHour) + ":" + (tzMinute < 10 ? "0" : "") + tzMinute;
+ //calendar.setTimeZone(TimeZone.getTimeZone(tz));
+ return calendar.getTime();
+ }
+
+ /* Parse a string into a number, using max (exclusive) and min (inclusive) values
+ *
+ */
+ private static int parseInt(String dateString, int index, int length, int min, int max) {
+ String s = dateString.substring(index, index + length);
+ return Integer.parseInt(s);
+ }
+
+ public static class CODE {
+ public final static int UNINITIALIZED = 0;
+ public static final int STARTED = 1;
+ public static final int VERSION_INCOMPATIBLE = 2;
+ public static final int WAITING_FOR_MAIN_THREAD = 3;
+ public static final int SUCCESS = 4;
+
+ public static class ERROR {
+ public static final int URL = -1;
+ public static final int DECODE = -2;
+ public static final int JSON = -3;
+ public static final int VERSION_INVALID = -4;
+ }
+ }
+
+ /* This is the version compatibility
+ * If the VERSION_MAJOR changes, it is incompatible.
+ * If the VERSION_MINOR is lower, it is incompatible.
+ */
+ private static final int VERSION_MAJOR = 2;
+ private static final int VERSION_MINOR = 0;
+ private static final String JSON_VERSION = "version";
+ private final String url;
+ private final RetrievalOptions retrievalOptions;
+ private final Thread thread;
+ private int code = CODE.UNINITIALIZED;
+
+ public GalleryImageDB(String url, RetrievalOptions retrievalOptions) {
+ this.url = url;
+ this.retrievalOptions = retrievalOptions;
+ thread = new Thread(this);
+ db = new JoinedImageDB();
+ }
+
+ public void start() {
+ thread.start();
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ public int size() {
+ return db.size();
+ }
+
+ @Override
+ public Image get(int index) {
+ return db.get(index);
+ }
+
+ @Override
+ public void run() {
+ code = CODE.STARTED;
+ Cache cache = retrievalOptions.getCache();
+ try {
+ JSONObject lastModification = getJSONFrom(getUrl("latest-modification.json"), cache);
+ if (lastModification == null) {
+ return;
+ }
+ String lastModifiedString = lastModification.getString("last-modified");
+ Date lastModified = parseTimeStamp(lastModifiedString);
+ String url = getUrl("images.json");
+ JSONObject json = getJSONFrom(url, cache.forId(url, lastModified));
+ if (json == null) {
+ return;
+ }
+ final JoinedImageDB db = new JoinedImageDB();
+ JSONArray images = json.getJSONArray("images");
+ for (int i = 0; i < images.length(); i++) {
+ JSONObject imageJSON = images.getJSONObject(i);
+ String id = imageJSON.getString("id");
+ String path = imageJSON.getString("path");
+ lastModifiedString = imageJSON.getString("last-modified");
+ URL imageUrl = new URL(getUrl(path));
+ UrlImageWithPreview image = new UrlImageWithPreview(imageUrl, id, parseDate(lastModifiedString), retrievalOptions);
+ JSONArray thumbs = imageJSON.getJSONArray("thumbnails");
+ for (int j = 0; j < thumbs.length(); j++) {
+ JSONObject thumbJSON = thumbs.getJSONObject(j);
+ String thumbPath = thumbJSON.getString("path");
+ int thumbMaxWidth = thumbJSON.getInt("max-width");
+ String thumbLastModified = imageJSON.getString("last-modified");
+ String thumbId = thumbJSON.getString("id");
+ URL thumbUrl = new URL(getUrl(thumbPath));
+ ThumbNailImage thumb = new ThumbNailImage(thumbUrl, thumbId, parseDate(thumbLastModified), thumbMaxWidth, retrievalOptions);
+ image.addPreviewImage(thumb);
+ }
+ db.add(image);
+ }
+ // start a handler in the UI thread
+ // see https://developer.android.com/training/multiple-threads/communicate-ui
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ code = CODE.SUCCESS;
+ GalleryImageDB.this.db = new SelectiveImageDB(db, getImageSelector());
+ if (db.size() > 0) {
+ notifyObservers();
+ }
+ }
+ });
+ code = CODE.WAITING_FOR_MAIN_THREAD;
+ } catch (JSONException | ParseException | MalformedURLException e) {
+ code = CODE.ERROR.JSON;
+ e.printStackTrace();
+ return;
+ } catch (IOException e) {
+ code = CODE.ERROR.DECODE;
+ e.printStackTrace();
+ return;
+ }
+ }
+
+ private JSONObject getJSONFrom(String urlString, Cache cache) throws IOException, JSONException {
+ InputStream stream;
+ try {
+ URL url = new URL(urlString);
+ stream = cache.openStreamIfAvailable(url);
+ } catch (IOException e) {
+ e.printStackTrace();
+ code = CODE.ERROR.URL;
+ return null;
+ }
+ return getJSONFrom(stream);
+ }
+
+ private JSONObject getJSONFrom(InputStream stream) throws JSONException, IOException {
+ byte[] rawBytesFromTheSource = IOUtils.toByteArray(stream);
+ String data = new String(rawBytesFromTheSource, "UTF-8");
+ JSONObject json = new JSONObject(data);
+ if (!checkVersionIsCompatible(json)) {
+ return null;
+ }
+ return json;
+ }
+
+ private boolean checkVersionIsCompatible(JSONObject json) throws JSONException {
+ String version = json.getString(JSON_VERSION);
+ String[] codes = version.split("\\.");
+ if (codes.length < 2) {
+ code = CODE.ERROR.VERSION_INVALID;
+ return false;
+ }
+ if (Integer.parseInt(codes[0]) != VERSION_MAJOR || Integer.parseInt(codes[1]) < VERSION_MINOR) {
+ code = CODE.VERSION_INCOMPATIBLE;
+ return false;
+ }
+ return true;
+ }
+
+ private Date parseDate(String lastModified) throws ParseException {
+ try {
+ return parseTimeStamp(lastModified);
+ } catch (Exception e){
+ retrievalOptions.getErrorReporter().report(e);
+ throw e;
+ }
+ }
+
+ private String getUrl(String path) {
+ return getUrl() + (getUrl().endsWith("/") ? "" : "/" ) + path;
+ }
+
+ /* This selects which image gets shown */
+ private SelectiveImageDB.ImageSelector getImageSelector() {
+ return new SelectiveImageDB.ImageSelector() {
+ @Override
+ public boolean isSelected(Image image) {
+ UrlImageWithPreview imageWithPreview = (UrlImageWithPreview)image;
+ return imageWithPreview.isCached() || imageWithPreview.canBeRetrieved();
+ }
+ };
+ }
+
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/ImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/ImageDB.java
new file mode 100644
index 0000000..f20fd6e
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/ImageDB.java
@@ -0,0 +1,26 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Parcelable;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+public interface ImageDB {
+ int size();
+ /* Get an image at an index.
+ * Expected: index >= 0
+ * If index >= size(), a NullImage is returned.
+ */
+ Image get(int index);
+ void attachObserver(Subject.Observer observer);
+
+ interface Image extends Parcelable {
+ // a scaled down version is passed to preview
+ void asPreviewImage(ImagePreview preview, LoadImageProgress progress);
+ boolean canBePainted();
+ // a black and white version is passed to preview
+ void asPaintableImage(ImagePreview preview, LoadImageProgress progress);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/ImageListener.java b/src/main/java/org/androidsoft/coloring/util/images/ImageListener.java
new file mode 100644
index 0000000..3620390
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/ImageListener.java
@@ -0,0 +1,5 @@
+package org.androidsoft.coloring.util.images;
+
+public interface ImageListener {
+ void onImageChosen(ImageDB.Image image);
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/JoinedImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/JoinedImageDB.java
new file mode 100644
index 0000000..9cdaf93
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/JoinedImageDB.java
@@ -0,0 +1,52 @@
+package org.androidsoft.coloring.util.images;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JoinedImageDB extends Subject implements ImageDB, Subject.Observer {
+
+ private List imageDBs = new ArrayList<>();;
+
+ public JoinedImageDB() {
+ }
+
+ JoinedImageDB(List dbs) {
+ for (ImageDB db : dbs) {
+ add(db);
+ }
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (ImageDB imageDB : imageDBs) {
+ size += imageDB.size();
+ }
+ return size;
+ }
+
+ @Override
+ public Image get(int index) {
+ for (ImageDB imageDB : imageDBs) {
+ if (index < imageDB.size()) {
+ return imageDB.get(index);
+ }
+ index -= imageDB.size();
+ }
+ return new NullImage();
+ }
+
+ public void add(ImageDB imageDB) {
+ imageDBs.add(imageDB);
+ imageDB.attachObserver(this);
+ }
+
+ public void add(Image image) {
+ add(new SingeImageDB(image));
+ }
+
+ @Override
+ public void update() {
+ notifyObservers();
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/NullImage.java b/src/main/java/org/androidsoft/coloring/util/images/NullImage.java
new file mode 100644
index 0000000..c75f12b
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/NullImage.java
@@ -0,0 +1,51 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Parcel;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import eu.quelltext.coloring.R;
+
+public class NullImage implements ImageDB.Image {
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepFail();
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return false;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepFail();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ return new NullImage();
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/NullImageListener.java b/src/main/java/org/androidsoft/coloring/util/images/NullImageListener.java
new file mode 100644
index 0000000..ddff5a4
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/NullImageListener.java
@@ -0,0 +1,8 @@
+package org.androidsoft.coloring.util.images;
+
+class NullImageListener implements ImageListener {
+ @Override
+ public void onImageChosen(ImageDB.Image image) {
+
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/PreparedUriImage.java b/src/main/java/org/androidsoft/coloring/util/images/PreparedUriImage.java
new file mode 100644
index 0000000..4b2612e
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/PreparedUriImage.java
@@ -0,0 +1,85 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Parcel;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.BlackAndWhiteImageImport;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+import org.androidsoft.coloring.util.imports.UriImageImport;
+
+import java.io.File;
+
+/* This is an image which is prepared (black and white) to drawing located at a Uri.
+ *
+ */
+public class PreparedUriImage implements ImageDB.Image {
+ private final Uri uri;
+ private final boolean isBlackAndWhite;
+
+ public static PreparedUriImage fromResourceId(Context context, int resourceId) {
+ // get Uri, see https://stackoverflow.com/a/19567921/1320237
+ Resources resources = context.getResources();
+ Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + resources.getResourcePackageName(resourceId) + '/' + resources.getResourceTypeName(resourceId) + '/' + resources.getResourceEntryName(resourceId));
+ return new PreparedUriImage(uri, false);
+ }
+
+ public PreparedUriImage(Uri uri, boolean isBlackAndWhite) {
+ this.uri = uri;
+ this.isBlackAndWhite = isBlackAndWhite;
+ }
+
+ public static ImageDB.Image fromPaintedImageFile(File file) {
+ return new PreparedUriImage(Uri.fromFile(file), true);
+ }
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ // todo: speed up by caching
+ new UriImageImport(uri, progress, preview).start();
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return true;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ if (isBlackAndWhite) {
+ new UriImageImport(uri, progress, preview).start();
+ } else {
+ new BlackAndWhiteImageImport(uri, progress, preview).start();
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(uri.toString());
+ // java.lang.NoSuchMethodError: No virtual method writeBoolean
+ parcel.writeInt(isBlackAndWhite ? 1 : 0);
+ }
+
+ public static Creator CREATOR = new Creator() {
+
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ Uri uri = Uri.parse(parcel.readString());
+ boolean isBlackAndWhite = parcel.readInt() != 0;
+ return new PreparedUriImage(uri, isBlackAndWhite);
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/ResourceImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/ResourceImageDB.java
new file mode 100644
index 0000000..c0022cb
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/ResourceImageDB.java
@@ -0,0 +1,54 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import eu.quelltext.coloring.R;
+
+/*
+ * This object contains all images from the res/drawable folder
+ */
+public class ResourceImageDB implements ImageDB {
+ private static final String IMAGE_PREFIX = "outline";
+ private final List images = new ArrayList<>();
+
+ public ResourceImageDB(Context context) {
+ Field[] drawables = R.drawable.class.getDeclaredFields();
+ for (int i = 0; i < drawables.length; i++) {
+ String name = drawables[i].getName();
+ try {
+ if (name.startsWith(IMAGE_PREFIX))
+ {
+ images.add(PreparedUriImage.fromResourceId(context, drawables[i].getInt(null)));
+ }
+ } catch (IllegalAccessException e) {}
+ }
+
+ }
+
+ @Override
+ public int size() {
+ return images.size();
+ }
+
+ @Override
+ public Image get(int index) {
+ if (index > images.size()) {
+ return new NullImage();
+ }
+ return images.get(index);
+ }
+
+ @Override
+ public void attachObserver(Subject.Observer observer) {
+ }
+
+ public Image randomImage() {
+ int index = new Random().nextInt(size());
+ return get(index);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/RetrievalOptions.java b/src/main/java/org/androidsoft/coloring/util/images/RetrievalOptions.java
new file mode 100644
index 0000000..f25b4e8
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/RetrievalOptions.java
@@ -0,0 +1,64 @@
+package org.androidsoft.coloring.util.images;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.androidsoft.coloring.util.errors.ErrorReporter;
+import org.androidsoft.coloring.util.errors.LoggingErrorReporter;
+import org.androidsoft.coloring.util.errors.UIErrorReporter;
+import org.androidsoft.coloring.util.cache.Cache;
+
+/* This object represents the options of how remote urls should be retrieved.
+ *
+ */
+public class RetrievalOptions implements Parcelable {
+ private final Cache cache;
+ private final boolean networkIsConnected;
+ private ErrorReporter errorReporter = new LoggingErrorReporter();
+
+ public RetrievalOptions(Cache cache, boolean networkIsConnected) {
+ this.cache = cache;
+ this.networkIsConnected = networkIsConnected;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(cache, flags);
+ parcel.writeInt(networkIsConnected ? 1 : 0);
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public RetrievalOptions createFromParcel(Parcel parcel) {
+ Cache cache = parcel.readParcelable(Cache.class.getClassLoader());
+ boolean networkIsConnected = parcel.readInt() != 0;
+ return new RetrievalOptions(cache, networkIsConnected);
+ }
+
+ @Override
+ public RetrievalOptions[] newArray(int i) {
+ return new RetrievalOptions[0];
+ }
+ };
+
+ public Cache getCache() {
+ return cache;
+ }
+
+ public ErrorReporter getErrorReporter() {
+ return errorReporter;
+ }
+
+ public void setErrorReporter(ErrorReporter errorReporter) {
+ this.errorReporter = errorReporter;
+ }
+
+ public boolean networkIsConnected() {
+ return networkIsConnected;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/SectionsAdapter.java b/src/main/java/org/androidsoft/coloring/util/images/SectionsAdapter.java
new file mode 100644
index 0000000..c0a1458
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/SectionsAdapter.java
@@ -0,0 +1,208 @@
+package org.androidsoft.coloring.util.images;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.cache.ImageCache;
+import org.androidsoft.coloring.util.cache.MemoryImageCache;
+import org.androidsoft.coloring.util.cache.NullImageCache;
+import org.androidsoft.coloring.util.imports.FixedSizeImagePreview;
+
+import eu.quelltext.coloring.R;
+
+public class SectionsAdapter extends RecyclerView.Adapter {
+ private static final int MAX_WIDTH_HEIGHT_MULTIPLIER = 3;
+ private final SettingsImageDB imageDB;
+ private final int layoutId;
+ private final int[] imageViewIds;
+ private final int numberOfImagesPerRow;
+ private final ImageCache cache = new NullImageCache(); // TODO: use MemoryImageCache if willed so
+ private ImageListener imageListener = new NullImageListener();
+
+ public SectionsAdapter(SettingsImageDB imageDB, int layoutId, int[] imageViewIds) {
+ this.imageDB = imageDB;
+ imageDB.attachObserver(new Subject.Observer() {
+ @Override
+ public void update() {
+ notifyDataSetChanged();
+ }
+ });
+ this.layoutId = layoutId;
+ this.imageViewIds = imageViewIds;
+ this.numberOfImagesPerRow = imageViewIds.length;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(layoutId, parent, false);
+
+ ViewHolder vh = new ViewHolder(v);
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int row) {
+ ImageDB.Image[] images = new ImageDB.Image[numberOfImagesPerRow];
+ for (SettingsImageDB.Entry section : imageDB.entries()) {
+ int sectionRows = numberOfRows(section);
+ if (sectionRows > row) {
+ // we found the section!
+ int start = row * numberOfImagesPerRow;
+ for (int i = 0; i < numberOfImagesPerRow; i++) {
+ images[i] = section.get(start + i);
+ }
+ // display the images
+ ViewHolder holder = (ViewHolder)viewHolder;
+ holder.display(images);
+ if (row == 0) {
+ holder.addSectionStart(section);
+ } else {
+ holder.removeSectionStart();
+ }
+ break;
+ } else {
+ row -= sectionRows;
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ int rows = 0;
+ for (SettingsImageDB.Entry section : imageDB.entries()) {
+ rows += numberOfRows(section);
+ }
+ return rows;
+ }
+
+ private int numberOfRows(SettingsImageDB.Entry section) {
+ int numberOfImages = section.size();
+ // increase rows according to the images
+ int rows = numberOfImages / numberOfImagesPerRow;
+ if (numberOfImages % numberOfImagesPerRow != 0) {
+ rows++; // does not fully fit the count
+ }
+ return rows;
+ }
+
+ public void setImageListener(ImageListener listener) {
+ imageListener = listener;
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ private final View root;
+ private final TextView title;
+ private final TextView description;
+ private final View titleContainer;
+
+
+ public ViewHolder(@NonNull View root) {
+ super(root);
+ this.root = root;
+ title = root.findViewById(R.id.title);
+ description = root.findViewById(R.id.description);
+ titleContainer = root.findViewById(R.id.title_container);
+ }
+
+ public void display(ImageDB.Image[] images) {
+ for (int i = 0; i < images.length; i++) {
+ ImageView imageView = root.findViewById(imageViewIds[i]);
+ final ImageDB.Image image = images[i];
+ if (image.canBePainted()) {
+ int width = getWidthOf(imageView);
+ if (!cache.asPreviewImage(image, new ThumbPreview(imageView, width), new LoadImageProgress(null, null))) {
+ imageView.setImageResource(R.drawable.download);
+ }
+ imageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ imageListener.onImageChosen(image);
+ }
+ });
+ imageView.setVisibility(View.VISIBLE);
+ } else {
+ imageView.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ private int getWidthOf(ImageView imageView) {
+ int width = imageView.getWidth();
+ int maxWidth = getScreenWidth() / numberOfImagesPerRow;
+ width = width == 0 ? maxWidth : width;
+ if (width > maxWidth) {
+ width = maxWidth;
+ }
+ return width;
+ }
+
+ private int getScreenWidth() {
+ // from https://stackoverflow.com/a/4744499
+ Context context = root.getContext();
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ try {
+ ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ } catch (ClassCastException e) {
+ e.printStackTrace();
+ return 400; // default width
+ }
+ return displayMetrics.widthPixels;
+ }
+
+ public void addSectionStart(SettingsImageDB.Entry section) {
+ titleContainer.setVisibility(View.VISIBLE);
+ title.setText(section.getName());
+ int numberOfImages = section.size();
+ if (numberOfImages >= 5) {
+ String description = this.description.getContext().getString(R.string.image_list_section_description, numberOfImages);
+ this.description.setText(description);
+ this.description.setVisibility(View.VISIBLE);
+ } else {
+ this.description.setVisibility(View.GONE);
+ }
+ }
+
+ public void removeSectionStart() {
+ titleContainer.setVisibility(View.GONE);
+ }
+ }
+
+ class ThumbPreview extends FixedSizeImagePreview {
+
+ private final ImageView imageView;
+
+ public ThumbPreview(ImageView imageView, int maxWidth) {
+ super(imageView.getContext(), maxWidth, maxWidth * MAX_WIDTH_HEIGHT_MULTIPLIER);
+ this.imageView = imageView;
+ }
+
+ @Override
+ public void setImage(final Bitmap image) {
+ imageView.post(new Runnable() {
+ @Override
+ public void run() {
+ imageView.setImageBitmap(image);
+ }
+ });
+ }
+
+ @Override
+ public void done(Bitmap bitmap) {
+ setImage(bitmap);
+ }
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/SelectiveImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/SelectiveImageDB.java
new file mode 100644
index 0000000..b4218de
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/SelectiveImageDB.java
@@ -0,0 +1,46 @@
+package org.androidsoft.coloring.util.images;
+
+class SelectiveImageDB implements ImageDB {
+ private final ImageDB db;
+ private final ImageSelector selector;
+
+ public SelectiveImageDB(ImageDB db, ImageSelector selector) {
+ this.db = db;
+ this.selector = selector;
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (int i = 0; i < db.size(); i++) {
+ Image image = db.get(i);
+ if (selector.isSelected(image)) {
+ size++;
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public Image get(int index) {
+ for (int i = 0; i < db.size(); i++) {
+ Image image = db.get(i);
+ if (selector.isSelected(image)) {
+ if (index <= 0) {
+ return image;
+ }
+ index--;
+ }
+ }
+ return new NullImage();
+ }
+
+ @Override
+ public void attachObserver(Subject.Observer observer) {
+ db.attachObserver(observer);
+ }
+
+ interface ImageSelector {
+ boolean isSelected(Image image);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/SettingsImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/SettingsImageDB.java
new file mode 100644
index 0000000..9d6f363
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/SettingsImageDB.java
@@ -0,0 +1,362 @@
+package org.androidsoft.coloring.util.images;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.activity.ChoosePictureActivity;
+import org.androidsoft.coloring.util.Settings;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import eu.quelltext.coloring.R;
+
+import static org.androidsoft.coloring.util.Settings.DEFAULT_GALLERIES;
+
+public class SettingsImageDB extends Subject implements ImageDB, Subject.Observer {
+
+ private static final String VERSION = "-1";
+ private static final String ID_RESOURCES = "ResourceImageDB";
+ private static final String ID_SAVED_IMAGES = "SavedImages";
+ private static final String ID_LAST_PAINTED = "LastPaintedImage";
+ private static final String KEY_ENTRY_ORDER = "SettingsImageDB.entryIds" + VERSION;
+ private static final String KEY_ENTRY_ACTIVATED = "SettingsImageDB.entriesActivated" + VERSION;
+ private final Settings settings;
+ private final JoinedImageDB paintedImage;
+ private List entries = new ArrayList<>();
+
+ public SettingsImageDB(Settings settings) {
+ this.settings = settings;
+ paintedImage = new JoinedImageDB();
+ entries.add(new Entry(
+ ID_LAST_PAINTED,
+ paintedImage,
+ getString(R.string.settings_galleries_last_painted),
+ getString(R.string.settings_galleries_last_painted_title)));
+ entries.add(new BrowsableEntry(
+ ID_SAVED_IMAGES,
+ DirectoryImageDB.atSaveLocationOf(settings.getContext()),
+ getString(R.string.settings_galleries_saved_images),
+ getString(R.string.settings_galleries_saved_images_title),
+ new Browsable(){
+ @Override
+ public void browse(Context context) {
+ Intent intent = new Intent(context, ChoosePictureActivity.class);
+ context.startActivity(intent);
+ }
+ }));
+ entries.add(new BrowsableEntry(
+ ID_RESOURCES,
+ new ResourceImageDB(settings.getContext()),
+ getString(R.string.settings_galleries_resources),
+ getString(R.string.settings_galleries_resources_title),
+ new Browsable(){
+ @Override
+ public void browse(Context context) {
+ Intent intent = new Intent(context, ChoosePictureActivity.class);
+ context.startActivity(intent);
+ }
+ }));
+
+ for (Settings.Gallery gallery : DEFAULT_GALLERIES) {
+ entries.add(new GalleryEntry(gallery.getUrl(), gallery.getDescription()));
+ }
+ orderEntries();
+ }
+
+ private String getString(int resourceId) {
+ return settings.getContext().getString(resourceId);
+ }
+
+ private void orderEntries() {
+ String[] ids = getEntryOrderIds();
+ List newEntries = new ArrayList<>();
+ for (String id: ids) {
+ Entry entry = getEntryById(id);
+ newEntries.add(entry);
+ }
+ entries = newEntries;
+ }
+
+ private String[] getEntryOrderIds() {
+ String[] ids = settings.getStringArray(KEY_ENTRY_ORDER);
+ if (ids == null) {
+ String[] defaultIds = getDefaultIds();
+ ids = new String[defaultIds.length + DEFAULT_GALLERIES.length];
+ // see https://www.tutorialspoint.com/java/lang/system_arraycopy.htm
+ System.arraycopy(defaultIds, 0, ids, 0, defaultIds.length);
+ int i = defaultIds.length;
+ for (Settings.Gallery entry : DEFAULT_GALLERIES) {
+ ids[i] = entry.getUrl();
+ i++;
+ }
+ }
+ return ids;
+ }
+
+ private Entry getEntryById(String id) {
+ Entry result = new UserDefinedEntry(id);
+ for (Entry entry : entries) {
+ if (entry.getId().equals(id)) {
+ result = entry;
+ break;
+ }
+ }
+ result.setActivationInternally(false);
+ String[] activated = getActivatedIds();
+ for (String activatedId : activated) {
+ if (id.equals(activatedId)) {
+ result.setActivationInternally(true);
+ }
+ }
+ return result;
+ }
+
+ private String[] getActivatedIds() {
+ String[] ids = settings.getStringArray(KEY_ENTRY_ACTIVATED);
+ if (ids == null) {
+ ids = getDefaultIds();
+ }
+ return ids;
+ }
+
+ private String[] getDefaultIds() {
+ return new String[]{ID_LAST_PAINTED, ID_RESOURCES, ID_SAVED_IMAGES};
+ }
+
+ private void save() {
+ String[] ids = new String[entries.size()];
+ Set activated = new HashSet<>();
+ int i = 0;
+ for (Entry entry : entries) {
+ ids[i] = entry.getId();
+ if (entry.isActivated()) {
+ activated.add(entry.getId());
+ }
+ i++;
+ }
+ settings.setStringArray(KEY_ENTRY_ORDER, ids);
+ settings.setStringArray(KEY_ENTRY_ACTIVATED, activated.toArray(new String[0]));
+ settings.save();
+ }
+
+ public void addPaintedImage(Image image) {
+ paintedImage.add(image);
+ }
+
+ public List entries() {
+ return entries;
+ }
+
+ @Override
+ public int size() {
+ return getJoinedImageDB().size();
+ }
+
+ private ImageDB getJoinedImageDB() {
+ return new JoinedImageDB(new ArrayList(entries));
+ }
+
+ @Override
+ public Image get(int index) {
+ return getJoinedImageDB().get(index);
+ }
+
+ public boolean addUserDefinedGallery(String url) {
+ for (Entry entry : entries) {
+ if (entry.getId().equals(url)) {
+ return false; // no duplicates
+ }
+ }
+ entries.add(getDefaultIds().length, new UserDefinedEntry(url));
+ save();
+ notifyObservers();
+ return true;
+ }
+
+ @Override
+ public void update() {
+ notifyObservers();
+ }
+
+ public boolean requiresInternetConnection() {
+ for (Entry entry : entries) {
+ if (entry.isActivated() && entry.requiresInternetConnection()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public class Entry implements ImageDB {
+ private final String id;
+ private final ImageDB db;
+ private final String description;
+ private final String title;
+ private boolean activated;
+
+ public Entry(String id, ImageDB db, String description, String title) {
+ this.id = id;
+ this.db = db;
+ this.description = description;
+ this.title = title;
+ attachObserver(SettingsImageDB.this);
+ }
+
+ public void setActivation(boolean activated) {
+ if (this.activated != activated) {
+ setActivationInternally(activated);
+ save();
+ }
+ }
+
+ public void setActivationInternally(boolean activated) {
+ this.activated = activated;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getName() {
+ return title;
+ }
+
+ public boolean canBeDeleted() {
+ return false;
+ }
+
+ public void delete() {
+ }
+
+ @Override
+ public int size() {
+ return isActivated() ? db.size() : 0;
+ }
+
+ @Override
+ public Image get(int index) {
+ return db.get(index);
+ }
+
+ @Override
+ public void attachObserver(Observer observer) {
+ db.attachObserver(observer);
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean isActivated() {
+ return activated;
+ }
+
+ public boolean canBrowse() {
+ return false;
+ }
+
+ public void browse(Context context) {
+ }
+
+ public ImageDB getDb() {
+ return db;
+ }
+
+ public boolean requiresInternetConnection() {
+ return false;
+ }
+ }
+
+ private class GalleryEntry extends Entry {
+ public GalleryEntry(String url, int description) {
+ super(url, new GalleryImageDB(url, settings.getRetrievalOptions()), getString(description), url);
+ }
+
+ @Override
+ public String getName() {
+ Uri uri = Uri.parse(getUrl());
+ return uri.getHost();
+ }
+
+ @Override
+ public boolean canBrowse() {
+ return true;
+ }
+
+ @Override
+ public void browse(Context context) {
+ // open url in browser, see https://stackoverflow.com/a/2201999/1320237
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getUrl()));
+ context.startActivity(browserIntent);
+ }
+
+ public String getUrl() {
+ return getId();
+ }
+
+ @Override
+ public void setActivationInternally(boolean activated) {
+ super.setActivationInternally(activated);
+ if (activated) {
+ GalleryImageDB db = (GalleryImageDB) getDb();
+ db.start();
+ }
+ }
+
+ @Override
+ public boolean requiresInternetConnection() {
+ return true;
+ }
+ }
+
+ private class UserDefinedEntry extends GalleryEntry {
+ public UserDefinedEntry(String url) {
+ super(url, R.string.settings_galleries_user_defined);
+ }
+
+ @Override
+ public String getDescription() {
+ return settings.getContext().getString(R.string.settings_galleries_user_defined, getId());
+ }
+
+ @Override
+ public boolean canBeDeleted() {
+ return true;
+ }
+
+ @Override
+ public void delete() {
+ entries.remove(this);
+ notifyObservers();
+ save();
+ }
+ }
+
+ interface Browsable {
+ void browse(Context context);
+ }
+
+ private class BrowsableEntry extends Entry {
+
+ private final Browsable browsable;
+
+ public BrowsableEntry(String id, ImageDB db, String description, String title, Browsable browsable) {
+ super(id, db, description, title);
+ this.browsable = browsable;
+ }
+
+ @Override
+ public boolean canBrowse() {
+ return true;
+ }
+
+ @Override
+ public void browse(Context context) {
+ browsable.browse(context);
+ }
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/SingeImageDB.java b/src/main/java/org/androidsoft/coloring/util/images/SingeImageDB.java
new file mode 100644
index 0000000..6fbdcc7
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/SingeImageDB.java
@@ -0,0 +1,26 @@
+package org.androidsoft.coloring.util.images;
+
+class SingeImageDB implements ImageDB {
+ private final Image image;
+
+ public SingeImageDB(Image image) {
+ this.image = image;
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+ @Override
+ public Image get(int index) {
+ if (index == 0) {
+ return image;
+ }
+ return new NullImage();
+ }
+
+ @Override
+ public void attachObserver(Subject.Observer observer) {
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/Subject.java b/src/main/java/org/androidsoft/coloring/util/images/Subject.java
new file mode 100644
index 0000000..b157f85
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/Subject.java
@@ -0,0 +1,27 @@
+package org.androidsoft.coloring.util.images;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/* See https://en.wikipedia.org/wiki/Observer_pattern
+ *
+ */
+public class Subject {
+
+ List observers = new ArrayList<>();
+
+ public interface Observer {
+ void update();
+ }
+
+ public void attachObserver(Observer observer) {
+ observers.add(observer);
+ }
+
+ public void notifyObservers() {
+ for (Observer observer : observers) {
+ observer.update();
+ }
+ }
+
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/ThumbNailImage.java b/src/main/java/org/androidsoft/coloring/util/images/ThumbNailImage.java
new file mode 100644
index 0000000..0e8990e
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/ThumbNailImage.java
@@ -0,0 +1,71 @@
+package org.androidsoft.coloring.util.images;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+import org.androidsoft.coloring.util.imports.UriImageImport;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+
+class ThumbNailImage extends UrlImage {
+ private final int maxWidth;
+
+ public ThumbNailImage(URL thumbUrl, String thumbId, Date thumbLastModified, int maxWidth, RetrievalOptions retrievalOptions) {
+ super(thumbUrl, thumbId, thumbLastModified, retrievalOptions);
+ this.maxWidth = maxWidth;
+ }
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ new UriImageImport(getUri(), progress, preview).startWith(getCache());
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return false;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepFail();
+ }
+
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ super.writeToParcel(parcel, i);
+ parcel.writeInt(maxWidth);
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ String urlString = parcel.readString();
+ String id = parcel.readString();
+ Date lastModified = new Date(parcel.readLong());
+ Parcelable retrievalOptions = parcel.readParcelable(RetrievalOptions.class.getClassLoader());
+ int maxWidth = parcel.readInt();
+ URL url = null;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return new NullImage();
+ }
+ return new ThumbNailImage(url, id, lastModified, maxWidth, (RetrievalOptions) retrievalOptions);
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+
+ public int getWidth() {
+ return maxWidth;
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/UrlImage.java b/src/main/java/org/androidsoft/coloring/util/images/UrlImage.java
new file mode 100644
index 0000000..adc10ca
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/UrlImage.java
@@ -0,0 +1,98 @@
+package org.androidsoft.coloring.util.images;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.cache.Cache;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+
+class UrlImage implements ImageDB.Image {
+ private final URL url;
+ private final String id;
+ private final Date lastModified;
+ private final RetrievalOptions retrievalOptions;
+ private final Cache cache;
+
+ public UrlImage(URL url, String id, Date lastModified, RetrievalOptions retrievalOptions) {
+ this.url = url;
+ this.id = id;
+ this.lastModified = lastModified;
+ this.retrievalOptions = retrievalOptions;
+ cache = retrievalOptions.getCache().forId(id, lastModified);
+ }
+
+ protected Cache getCache() {
+ return cache;
+ }
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepFail();
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return false;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ progress.stepFail();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(url.toString());
+ parcel.writeString(id);
+ parcel.writeLong(lastModified.getTime());
+ parcel.writeParcelable(retrievalOptions, flags);
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ String urlString = parcel.readString();
+ String id = parcel.readString();
+ Date lastModified = new Date(parcel.readLong());
+ Parcelable retrievalOptions = parcel.readParcelable(RetrievalOptions.class.getClassLoader());
+ URL url = null;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return new NullImage();
+ }
+ return new UrlImage(url, id, lastModified, (RetrievalOptions) retrievalOptions);
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+
+ protected Uri getUri() {
+ // see https://stackoverflow.com/a/9662933/1320237
+ return Uri.parse(url.toString());
+ }
+
+ public boolean isCached() {
+ return getCache().isCached(id);
+ }
+
+ public boolean canBeRetrieved() {
+ // TODO: if the url is localhost should be true
+ return retrievalOptions.networkIsConnected();
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/images/UrlImageWithPreview.java b/src/main/java/org/androidsoft/coloring/util/images/UrlImageWithPreview.java
new file mode 100644
index 0000000..51b6ea4
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/images/UrlImageWithPreview.java
@@ -0,0 +1,105 @@
+package org.androidsoft.coloring.util.images;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.imports.BlackAndWhiteImageImport;
+import org.androidsoft.coloring.util.imports.ImagePreview;
+import org.androidsoft.coloring.util.imports.UriImageImport;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+public class UrlImageWithPreview extends UrlImage {
+
+ private final List thumbs = new ArrayList<>();
+
+
+ public UrlImageWithPreview(URL url, String id, Date lastModified, RetrievalOptions retrievalOptions) {
+ super(url, id, lastModified, retrievalOptions);
+ }
+
+ @Override
+ public void asPreviewImage(ImagePreview preview, LoadImageProgress progress) {
+ if (this.thumbs.size() == 0) {
+ new UriImageImport(getUri(), progress, preview).startWith(getCache());
+ return;
+ }
+ List thumbs = new ArrayList(this.thumbs);
+ final int width = preview.getWidth();
+ Collections.sort(thumbs, new Comparator() {
+ @Override
+ public int compare(ThumbNailImage thumb1, ThumbNailImage thumb2) {
+ int distance1 = Math.abs(thumb1.getWidth() - width);
+ int distance2 = Math.abs(thumb2.getWidth() - width);
+ return distance1 - distance2;
+ }
+ });
+ for (ThumbNailImage bestThumb : thumbs) {
+ if (bestThumb.canBeRetrieved() || bestThumb.isCached()) {
+ bestThumb.asPreviewImage(preview, progress);
+ return;
+ }
+ }
+ new UriImageImport(getUri(), progress, preview).startWith(getCache());
+ }
+
+ @Override
+ public boolean canBePainted() {
+ return true;
+ }
+
+ @Override
+ public void asPaintableImage(ImagePreview preview, LoadImageProgress progress) {
+ new BlackAndWhiteImageImport(getUri(), progress, preview).startWith(getCache());
+ }
+
+
+ public void addPreviewImage(ThumbNailImage thumb) {
+ thumbs.add(thumb);
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ super.writeToParcel(parcel, i);
+ parcel.writeInt(thumbs.size());
+ for (ThumbNailImage thumb : thumbs) {
+ parcel.writeParcelable(thumb, i);
+ }
+ }
+
+ public static Creator CREATOR = new Creator() {
+ @Override
+ public ImageDB.Image createFromParcel(Parcel parcel) {
+ String urlString = parcel.readString();
+ String id = parcel.readString();
+ Date lastModified = new Date(parcel.readLong());
+ Parcelable retrievalOptions = parcel.readParcelable(RetrievalOptions.class.getClassLoader());
+ int size = parcel.readInt();
+ URL url = null;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return new NullImage();
+ }
+ UrlImageWithPreview image = new UrlImageWithPreview(url, id, lastModified, (RetrievalOptions) retrievalOptions);
+ for (int i = 0; i < size; i++) {
+ ThumbNailImage thumb = parcel.readParcelable(ThumbNailImage.class.getClassLoader());
+ image.addPreviewImage(thumb);
+ }
+ return image;
+ }
+
+ @Override
+ public ImageDB.Image[] newArray(int i) {
+ return new ImageDB.Image[0];
+ }
+ };
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/imports/BlackAndWhiteImageImport.java b/src/main/java/org/androidsoft/coloring/util/imports/BlackAndWhiteImageImport.java
new file mode 100644
index 0000000..325d193
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/imports/BlackAndWhiteImageImport.java
@@ -0,0 +1,22 @@
+package org.androidsoft.coloring.util.imports;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.FloodFill;
+
+public class BlackAndWhiteImageImport extends UriImageImport {
+
+ public BlackAndWhiteImageImport(Uri imageUri, LoadImageProgress progress, ImagePreview imagePreview) {
+ super(imageUri, progress, imagePreview);
+ }
+
+ @Override
+ protected void runWithBitmap(Bitmap image) {
+ imagePreview.setImage(image);
+ progress.stepConvertingToBinaryImage();
+ Bitmap binaryImage = FloodFill.asBlackAndWhite(image);
+ super.runWithBitmap(binaryImage);
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/imports/ColoredImageImport.java b/src/main/java/org/androidsoft/coloring/util/imports/ColoredImageImport.java
new file mode 100644
index 0000000..84c1117
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/imports/ColoredImageImport.java
@@ -0,0 +1,122 @@
+package org.androidsoft.coloring.util.imports;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+
+import eu.quelltext.images.ArrayMapper;
+import eu.quelltext.images.ClusteredColors;
+import eu.quelltext.images.ConnectedComponents;
+import eu.quelltext.images.FastMaximumShiftFilter;
+import eu.quelltext.images.KMeansOnRGBColors;
+import eu.quelltext.images.LineasAroundAreas;
+import eu.quelltext.images.Measurement;
+import eu.quelltext.images.RandomColorGenerator;
+
+public class ColoredImageImport extends UriImageImport {
+
+ private static final int NUMBER_OF_COLORS = 9;
+ private static final int LINE_WIDTH = 7;
+ private static final int MAX_SHIFT_KERNEL_DIAMETER = 10;
+ private static final int COLOR_LINE = Color.BLACK;
+ private static final int COLOR_BACKGROUND = Color.WHITE;
+
+ // at least LINE_WIDTH wide between the lines so one can actually touch it
+ private static final int MINIMUM_AREA = 2 * (LINE_WIDTH * 2 * LINE_WIDTH * 2);
+ private ClusteredColors cluster;
+ private int[] colors;
+
+ public ColoredImageImport(Uri imageUri, LoadImageProgress progress, ImagePreview imagePreview) {
+ super(imageUri, progress, imagePreview);
+ }
+
+ @Override
+ protected void runWithBitmap(Bitmap image) {
+ colors = getPixels(image);
+ showImage(colors);
+ // run cluster the colors used in the image
+ try {
+ classifyColors();
+ } catch (Exception e) {
+ e.printStackTrace();
+ progress.stepFail();
+ return;
+ }
+ removeSmallAreasByKernel();
+ removeSmallAreasByConnectedComponents();
+ drawLinesAroundTheAreas();
+ super.runWithBitmap(getBitmap(colors));
+ }
+
+ private void drawLinesAroundTheAreas() {
+ progress.stepDrawLinesAround();
+ LineasAroundAreas areaLines = new LineasAroundAreas(colors, width, height);
+ colors = areaLines.draw(LINE_WIDTH, COLOR_BACKGROUND, COLOR_LINE);
+ showImage(colors);
+ }
+
+ private void removeSmallAreasByConnectedComponents() {
+ progress.stepConnectingComponents();
+ ConnectedComponents connectedComponents = new ConnectedComponents(colors, width, height);
+ ConnectedComponents.Result result = connectedComponents.compute();
+ progress.stepMeasuringAreas();
+ Measurement measurement = result.computeMeasurement();
+ // remove areas until a certain size is reached
+ while (measurement.getSmallestComponentSize() < MINIMUM_AREA) {
+ measurement.mergeSmallestAreaIntoItsBiggestNeighbor();
+ }
+ int[] components = measurement.computeArea();
+ int[] areaColors = new RandomColorGenerator().bright(measurement.getNumberOfComponents());
+ colors = ArrayMapper.mapFrom(components, areaColors).getArray();
+ progress.stepShowComponents();
+ showImage(colors);
+
+ }
+
+ private void removeSmallAreasByKernel() {
+ progress.stepRemovingNoise();
+ // TODO: speed up the algorithm by using the function getArrayWithClusterIds()
+ // and an array counter
+ FastMaximumShiftFilter filter = new FastMaximumShiftFilter(cluster.getClassifiedColors(), width, height);
+ colors = filter.compute(MAX_SHIFT_KERNEL_DIAMETER);
+ progress.stepShowSmoothedImage();
+ showImage(colors);
+ }
+
+ public static int[] getPixels(Bitmap bitmap) {
+ int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
+ bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+ return pixels;
+ }
+
+ private void classifyColors() throws Exception {
+ progress.stepPreparingClustering();
+ KMeansOnRGBColors kmeans = new KMeansOnRGBColors(colors, NUMBER_OF_COLORS);
+
+ progress.stepSampleDataForClassification();
+ kmeans.step01RandomSampling();
+
+ progress.stepClusteringData();
+ kmeans.step02clustering();
+
+ progress.stepCreateClusterImage();
+ cluster = kmeans.step03ClassifyData();
+ progress.stepShowClusterImage();
+ showImage(cluster.getClassifiedColors());
+ }
+
+ private void showImage(int[] colors) {
+ imagePreview.setImage(getBitmap(colors));
+ }
+
+ private Bitmap getBitmap(int[] colors) {
+ // build a colored bitmap from the classification
+ // see https://stackoverflow.com/a/10180908/1320237
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(colors, 0, width, 0, 0, width, height);
+ return bitmap;
+ }
+
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/imports/FixedSizeImagePreview.java b/src/main/java/org/androidsoft/coloring/util/imports/FixedSizeImagePreview.java
new file mode 100644
index 0000000..a8dd5bd
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/imports/FixedSizeImagePreview.java
@@ -0,0 +1,45 @@
+package org.androidsoft.coloring.util.imports;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import org.androidsoft.coloring.util.imports.ImagePreview;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class FixedSizeImagePreview implements ImagePreview {
+ private final int maxWidth;
+ private final Context context;
+ private int maxHeight;
+
+ public FixedSizeImagePreview(Context context, int maxWidth, int maxHeight) {
+ this.context = context;
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ }
+
+ @Override
+ public void setImage(final Bitmap image) {
+ }
+
+ @Override
+ public int getWidth() {
+ return maxWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return maxHeight;
+ }
+
+ @Override
+ public InputStream openInputStream(Uri uri) throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(uri);
+ }
+
+ @Override
+ public void done(Bitmap bitmap) {
+ }
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/imports/ImagePreview.java b/src/main/java/org/androidsoft/coloring/util/imports/ImagePreview.java
new file mode 100644
index 0000000..ab93600
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/imports/ImagePreview.java
@@ -0,0 +1,15 @@
+package org.androidsoft.coloring.util.imports;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public interface ImagePreview {
+ void setImage(Bitmap image);
+ int getWidth();
+ int getHeight();
+ InputStream openInputStream(Uri uri) throws FileNotFoundException;
+ void done(Bitmap bitmap);
+}
diff --git a/src/main/java/org/androidsoft/coloring/util/imports/UriImageImport.java b/src/main/java/org/androidsoft/coloring/util/imports/UriImageImport.java
new file mode 100644
index 0000000..ee591c2
--- /dev/null
+++ b/src/main/java/org/androidsoft/coloring/util/imports/UriImageImport.java
@@ -0,0 +1,139 @@
+package org.androidsoft.coloring.util.imports;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+
+import org.androidsoft.coloring.ui.widget.LoadImageProgress;
+import org.androidsoft.coloring.util.cache.Cache;
+import org.androidsoft.coloring.util.cache.NullCache;
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+// see https://developer.android.com/reference/java/lang/Thread
+public class UriImageImport implements Runnable {
+
+ private byte[] rawBytesFromTheSource = null;
+
+ protected final LoadImageProgress progress;
+ protected final ImagePreview imagePreview;
+ protected final Uri imageUri;
+ protected int width;
+ protected int height;
+ private Thread thread = null;
+ private Cache cache = new NullCache();
+
+ public UriImageImport(Uri imageUri, LoadImageProgress progress, ImagePreview imagePreview) {
+ this.imageUri = imageUri;
+ this.progress = progress;
+ this.imagePreview = imagePreview;
+ }
+
+ @Override
+ public void run() {
+ // create a preview image
+ Bitmap image;
+ try {
+ progress.stepInputPreview();
+ image = getThumbnail(imageUri, imagePreview.getWidth(), imagePreview.getHeight());
+ } catch (IOException e) {
+ e.printStackTrace();
+ progress.stepFail();
+ return;
+ }
+ rawBytesFromTheSource = null; // clean up
+ // set attributes
+ if (image == null) {
+ progress.stepFail();
+ return;
+ }
+ width = image.getWidth();
+ height = image.getHeight();
+ runWithBitmap(image);
+ }
+
+ protected void runWithBitmap(Bitmap image) {
+ progress.stepDone();
+ imagePreview.done(image);
+ }
+
+ private Bitmap getThumbnail(Uri uri, double maxWidth, double maxHeight) throws IOException {
+ InputStream input = getInputStream(uri);
+
+ BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
+ onlyBoundsOptions.inJustDecodeBounds = true;
+ onlyBoundsOptions.inDither=true;//optional
+ onlyBoundsOptions.inPreferredConfig= Bitmap.Config.ARGB_8888;//optional
+ BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
+ input.close();
+
+ if ((onlyBoundsOptions.outWidth == -1) || (onlyBoundsOptions.outHeight == -1)) {
+ return null;
+ }
+
+ if ((maxHeight < maxWidth) != (onlyBoundsOptions.outHeight < onlyBoundsOptions.outWidth)) {
+ // fit the image in the direction it is biggest
+ double t = maxWidth;
+ maxWidth = maxHeight;
+ maxHeight = t;
+ }
+
+ double imageRatio = (double)onlyBoundsOptions.outWidth / (double)onlyBoundsOptions.outHeight;
+ double ratio;
+ if (maxWidth / maxHeight < imageRatio) {
+ ratio = (double)onlyBoundsOptions.outWidth / maxWidth;
+ } else {
+ ratio = (double)onlyBoundsOptions.outHeight / maxHeight;
+ }
+
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inSampleSize = getPowerOfTwoForSampleRatio(ratio);
+ bitmapOptions.inDither = true; //optional
+ bitmapOptions.inPreferredConfig=Bitmap.Config.ARGB_8888;//
+ input = getInputStream(uri);
+ Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
+ input.close();
+ return bitmap;
+ }
+
+ private InputStream getInputStream(Uri uri) throws IOException {
+ if (rawBytesFromTheSource != null) {
+ return new ByteArrayInputStream(rawBytesFromTheSource);
+ }
+ if (uri.getScheme().startsWith("http")) {
+ // download file, see https://stackoverflow.com/a/51271706/1320237
+ InputStream stream = cache.openStreamIfAvailable(new URL(uri.toString()));
+ rawBytesFromTheSource = IOUtils.toByteArray(stream);
+ return new ByteArrayInputStream(rawBytesFromTheSource);
+ } else {
+ // from https://stackoverflow.com/a/6228188/1320237
+ return imagePreview.openInputStream(uri);
+ }
+ }
+
+ private int getPowerOfTwoForSampleRatio(double ratio){
+ int k = Integer.highestOneBit((int)Math.floor(ratio));
+ if(k==0) return 1;
+ else return k;
+ }
+
+ public synchronized void start() {
+ if (thread == null) {
+ thread = new Thread(this);
+ }
+ thread.start();
+ }
+
+ public void setCache(Cache cache) {
+ this.cache = cache;
+ }
+
+ public void startWith(Cache cache) {
+ setCache(cache);
+ start();
+ }
+}
diff --git a/src/main/res/drawable-hdpi/ic_delete.png b/src/main/res/drawable-hdpi/ic_delete.png
new file mode 100644
index 0000000..f3e53d7
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_delete.png differ
diff --git a/src/main/res/drawable-hdpi/ic_input_add.png b/src/main/res/drawable-hdpi/ic_input_add.png
new file mode 100644
index 0000000..d26ebac
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_input_add.png differ
diff --git a/src/main/res/drawable-hdpi/ic_menu_view.png b/src/main/res/drawable-hdpi/ic_menu_view.png
new file mode 100644
index 0000000..25c2ff3
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_menu_view.png differ
diff --git a/src/main/res/drawable-hdpi/icon.png b/src/main/res/drawable-hdpi/icon.png
deleted file mode 100644
index 34e70a1..0000000
Binary files a/src/main/res/drawable-hdpi/icon.png and /dev/null differ
diff --git a/src/main/res/drawable-hdpi/splash.png b/src/main/res/drawable-hdpi/splash.png
deleted file mode 100644
index 276f620..0000000
Binary files a/src/main/res/drawable-hdpi/splash.png and /dev/null differ
diff --git a/src/main/res/drawable-ldpi/ic_delete.png b/src/main/res/drawable-ldpi/ic_delete.png
new file mode 100644
index 0000000..a4cefa8
Binary files /dev/null and b/src/main/res/drawable-ldpi/ic_delete.png differ
diff --git a/src/main/res/drawable-ldpi/ic_input_add.png b/src/main/res/drawable-ldpi/ic_input_add.png
new file mode 100644
index 0000000..04cc27a
Binary files /dev/null and b/src/main/res/drawable-ldpi/ic_input_add.png differ
diff --git a/src/main/res/drawable-ldpi/ic_menu_view.png b/src/main/res/drawable-ldpi/ic_menu_view.png
new file mode 100644
index 0000000..f1acb3d
Binary files /dev/null and b/src/main/res/drawable-ldpi/ic_menu_view.png differ
diff --git a/src/main/res/drawable-mdpi/download.png b/src/main/res/drawable-mdpi/download.png
new file mode 100644
index 0000000..5abe320
Binary files /dev/null and b/src/main/res/drawable-mdpi/download.png differ
diff --git a/src/main/res/drawable-mdpi/ic_delete.png b/src/main/res/drawable-mdpi/ic_delete.png
new file mode 100644
index 0000000..f074db3
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_delete.png differ
diff --git a/src/main/res/drawable-mdpi/ic_input_add.png b/src/main/res/drawable-mdpi/ic_input_add.png
new file mode 100644
index 0000000..00770f8
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_input_add.png differ
diff --git a/src/main/res/drawable-mdpi/ic_menu_view.png b/src/main/res/drawable-mdpi/ic_menu_view.png
new file mode 100644
index 0000000..082810d
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_menu_view.png differ
diff --git a/src/main/res/drawable-mdpi/thumb001_balloons.png b/src/main/res/drawable-mdpi/thumb001_balloons.png
deleted file mode 100644
index 76609d1..0000000
Binary files a/src/main/res/drawable-mdpi/thumb001_balloons.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb002_spaceship.png b/src/main/res/drawable-mdpi/thumb002_spaceship.png
deleted file mode 100644
index f99144a..0000000
Binary files a/src/main/res/drawable-mdpi/thumb002_spaceship.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb003_horses.png b/src/main/res/drawable-mdpi/thumb003_horses.png
deleted file mode 100644
index 0044696..0000000
Binary files a/src/main/res/drawable-mdpi/thumb003_horses.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb004_castle.png b/src/main/res/drawable-mdpi/thumb004_castle.png
deleted file mode 100644
index 1a6e304..0000000
Binary files a/src/main/res/drawable-mdpi/thumb004_castle.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb005_house.png b/src/main/res/drawable-mdpi/thumb005_house.png
deleted file mode 100644
index e73d7d5..0000000
Binary files a/src/main/res/drawable-mdpi/thumb005_house.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb006_dino.png b/src/main/res/drawable-mdpi/thumb006_dino.png
deleted file mode 100644
index 65d50f6..0000000
Binary files a/src/main/res/drawable-mdpi/thumb006_dino.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb007_flowers.png b/src/main/res/drawable-mdpi/thumb007_flowers.png
deleted file mode 100644
index d73e6e1..0000000
Binary files a/src/main/res/drawable-mdpi/thumb007_flowers.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb008_sealife.png b/src/main/res/drawable-mdpi/thumb008_sealife.png
deleted file mode 100644
index 075e4c9..0000000
Binary files a/src/main/res/drawable-mdpi/thumb008_sealife.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb009_zoo.png b/src/main/res/drawable-mdpi/thumb009_zoo.png
deleted file mode 100644
index 638f87c..0000000
Binary files a/src/main/res/drawable-mdpi/thumb009_zoo.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb010_roadrunner.png b/src/main/res/drawable-mdpi/thumb010_roadrunner.png
deleted file mode 100644
index 4949344..0000000
Binary files a/src/main/res/drawable-mdpi/thumb010_roadrunner.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb011_plane.png b/src/main/res/drawable-mdpi/thumb011_plane.png
deleted file mode 100644
index c151580..0000000
Binary files a/src/main/res/drawable-mdpi/thumb011_plane.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb012_birthday.png b/src/main/res/drawable-mdpi/thumb012_birthday.png
deleted file mode 100644
index c337522..0000000
Binary files a/src/main/res/drawable-mdpi/thumb012_birthday.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb013_18wheeler.png b/src/main/res/drawable-mdpi/thumb013_18wheeler.png
deleted file mode 100644
index dab1eb1..0000000
Binary files a/src/main/res/drawable-mdpi/thumb013_18wheeler.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb014_motorbike.png b/src/main/res/drawable-mdpi/thumb014_motorbike.png
deleted file mode 100644
index 010f526..0000000
Binary files a/src/main/res/drawable-mdpi/thumb014_motorbike.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb015_f15eagle.png b/src/main/res/drawable-mdpi/thumb015_f15eagle.png
deleted file mode 100644
index ec33c54..0000000
Binary files a/src/main/res/drawable-mdpi/thumb015_f15eagle.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb016_beagle.png b/src/main/res/drawable-mdpi/thumb016_beagle.png
deleted file mode 100644
index 0271d41..0000000
Binary files a/src/main/res/drawable-mdpi/thumb016_beagle.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb017_butterfly.png b/src/main/res/drawable-mdpi/thumb017_butterfly.png
deleted file mode 100644
index ba3864b..0000000
Binary files a/src/main/res/drawable-mdpi/thumb017_butterfly.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb018_snail.png b/src/main/res/drawable-mdpi/thumb018_snail.png
deleted file mode 100644
index 2f14b9f..0000000
Binary files a/src/main/res/drawable-mdpi/thumb018_snail.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb019_helicopter.png b/src/main/res/drawable-mdpi/thumb019_helicopter.png
deleted file mode 100644
index c6fdada..0000000
Binary files a/src/main/res/drawable-mdpi/thumb019_helicopter.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb020_bee.png b/src/main/res/drawable-mdpi/thumb020_bee.png
deleted file mode 100644
index 6f0e429..0000000
Binary files a/src/main/res/drawable-mdpi/thumb020_bee.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb021_spider.png b/src/main/res/drawable-mdpi/thumb021_spider.png
deleted file mode 100644
index 7404648..0000000
Binary files a/src/main/res/drawable-mdpi/thumb021_spider.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb022_medeival_city.png b/src/main/res/drawable-mdpi/thumb022_medeival_city.png
deleted file mode 100644
index baf3f57..0000000
Binary files a/src/main/res/drawable-mdpi/thumb022_medeival_city.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb023_outer_space.png b/src/main/res/drawable-mdpi/thumb023_outer_space.png
deleted file mode 100644
index 0806788..0000000
Binary files a/src/main/res/drawable-mdpi/thumb023_outer_space.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/thumb024_world_map.png b/src/main/res/drawable-mdpi/thumb024_world_map.png
deleted file mode 100644
index fa23f6a..0000000
Binary files a/src/main/res/drawable-mdpi/thumb024_world_map.png and /dev/null differ
diff --git a/src/main/res/drawable-xhdpi/ic_delete.png b/src/main/res/drawable-xhdpi/ic_delete.png
new file mode 100644
index 0000000..9abc51a
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_delete.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_input_add.png b/src/main/res/drawable-xhdpi/ic_input_add.png
new file mode 100644
index 0000000..f1242f5
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_input_add.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_menu_view.png b/src/main/res/drawable-xhdpi/ic_menu_view.png
new file mode 100644
index 0000000..e97c30d
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_menu_view.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_menu_view.png b/src/main/res/drawable-xxhdpi/ic_menu_view.png
new file mode 100644
index 0000000..aff6c86
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_menu_view.png differ
diff --git a/src/main/res/drawable/ic_logo.xml b/src/main/res/drawable/ic_logo.xml
new file mode 100644
index 0000000..ce5279d
--- /dev/null
+++ b/src/main/res/drawable/ic_logo.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/drawable/ic_logo_round.xml b/src/main/res/drawable/ic_logo_round.xml
new file mode 100644
index 0000000..2647f94
--- /dev/null
+++ b/src/main/res/drawable/ic_logo_round.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/activity_image_import.xml b/src/main/res/layout/activity_image_import.xml
new file mode 100644
index 0000000..c6de808
--- /dev/null
+++ b/src/main/res/layout/activity_image_import.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/activity_settings.xml b/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..ae5fc46
--- /dev/null
+++ b/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/start_new.xml b/src/main/res/layout/choose_picture.xml
similarity index 51%
rename from src/main/res/layout/start_new.xml
rename to src/main/res/layout/choose_picture.xml
index 71c64a0..bbe84b8 100644
--- a/src/main/res/layout/start_new.xml
+++ b/src/main/res/layout/choose_picture.xml
@@ -13,20 +13,21 @@
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.
--->
+-->
-
-
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
diff --git a/src/main/res/layout/choose_picture_line.xml b/src/main/res/layout/choose_picture_line.xml
new file mode 100644
index 0000000..dcd8375
--- /dev/null
+++ b/src/main/res/layout/choose_picture_line.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/gallery.xml b/src/main/res/layout/gallery.xml
new file mode 100644
index 0000000..495c8e9
--- /dev/null
+++ b/src/main/res/layout/gallery.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/paint.xml b/src/main/res/layout/paint.xml
index 2459060..edc35c9 100644
--- a/src/main/res/layout/paint.xml
+++ b/src/main/res/layout/paint.xml
@@ -15,57 +15,81 @@
limitations under the License.
-->
+ xmlns:coloring="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#FFFFFF"
+ android:orientation="horizontal">
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
diff --git a/src/main/res/layout/splash.xml b/src/main/res/layout/splash.xml
index 5c3e1a6..c680cdd 100644
--- a/src/main/res/layout/splash.xml
+++ b/src/main/res/layout/splash.xml
@@ -18,35 +18,36 @@
-
-
+ android:gravity="center_horizontal|center_vertical"
+ android:orientation="vertical">
-
-
+
-
+
-
-
+
-
\ No newline at end of file
diff --git a/src/main/res/menu/menu_close.xml b/src/main/res/menu/menu_close.xml
index b07d450..6e9919b 100644
--- a/src/main/res/menu/menu_close.xml
+++ b/src/main/res/menu/menu_close.xml
@@ -1,7 +1,10 @@
-