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 +[![Android CI](https://github.com/niccokunzmann/androidsoft-coloring/workflows/Android%20CI/badge.svg)](https://github.com/niccokunzmann/androidsoft-coloring/actions?query=workflow%3A%22Android+CI%22) + + + +## Download + +[Get it on F-Droid](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. [![Android CI](https://github.com/niccokunzmann/androidsoft-coloring/workflows/Android%20CI/badge.svg)](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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + +