diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..3ea5c13
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve Identiconizer
+title: '[BUG] '
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Device information:**
+ - Device: [e.g. Samsung Galaxy S21]
+ - OS: [e.g. Android 12]
+ - App version: [e.g. 1.5]
+ - Xposed Framework: [Yes/No]
+
+**Additional context**
+Add any other context about the problem here.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..c7ff776
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,25 @@
+---
+name: Feature request
+about: Suggest an idea for Identiconizer
+title: '[FEATURE] '
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
+
+**Would this feature be useful for:**
+- [ ] General users
+- [ ] Power users
+- [ ] Developers
\ No newline at end of file
diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 0000000..a98c513
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,48 @@
+# Identiconizer - Android Contact Identicons App
+
+[](https://f-droid.org/packages/com.germainz.identiconizer/)
+[](https://opensource.org/licenses/Apache-2.0)
+
+**Keywords:** android, contacts, identicons, avatars, android-app, contact-management, avatar-generator, android-development
+
+## About This Repository
+
+This repository contains the source code for **Identiconizer!**, an Android application that automatically generates unique identicons (geometric avatar images) for contacts without profile pictures.
+
+### 🔍 **Repository Topics & Keywords**
+- `android`
+- `contacts`
+- `identicons`
+- `avatars`
+- `android-app`
+- `contact-management`
+- `avatar-generator`
+- `java`
+- `android-development`
+- `f-droid`
+- `open-source`
+
+### 📱 **What is Identiconizer?**
+Identiconizer! automatically creates unique, colorful geometric patterns as profile pictures for your Android contacts. Instead of showing a generic silhouette for contacts without photos, each contact gets a distinctive identicon based on their name.
+
+### ✨ **Key Features**
+- 🎨 Six different identicon styles (Retro, Contemporary, Spirograph, Dot Matrix, Gmail, Unicornify)
+- 📏 Customizable sizes (96x96 to 720x720 pixels)
+- 🎨 Custom background colors
+- 🔄 Batch processing for all contacts
+- 🌐 Online avatar service support
+
+### 🛠 **For Developers**
+This is an open-source Android project using:
+- **Language:** Java
+- **Build System:** Gradle
+- **Target:** Android API levels up to 35
+- **Framework:** Standard Android SDK
+
+### 📦 **Distribution**
+- Available on [F-Droid](https://f-droid.org/packages/com.germainz.identiconizer/)
+- Source code available here on GitHub
+- Active development and community contributions welcome
+
+---
+*This repository should be discoverable when searching for: android contacts app, identicon generator, avatar creator, contact photos, android development, f-droid apps*
\ No newline at end of file
diff --git a/.github/REPOSITORY.md b/.github/REPOSITORY.md
new file mode 100644
index 0000000..d15a080
--- /dev/null
+++ b/.github/REPOSITORY.md
@@ -0,0 +1,24 @@
+# Repository Information
+
+**Name:** Identiconizer
+**Description:** Android app that generates unique geometric identicons for contacts without profile pictures. Supports 6 different styles.
+**Homepage:** https://f-droid.org/packages/com.germainz.identiconizer/
+
+## Topics
+- android
+- contacts
+- identicons
+- avatars
+- android-app
+- contact-management
+- avatar-generator
+- java
+- android-development
+- f-droid
+- open-source
+- contact-photos
+- profile-pictures
+- geometric-patterns
+
+## Keywords for Search
+android contacts app, identicon generator, avatar creator, contact photos, android development, f-droid apps, contact management, profile pictures, geometric avatars
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 572ad90..f6f0f61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,11 @@
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
+/.idea/appInsightsSettings.xml
+/.idea/deviceManager.xml
/.idea/caches
.DS_Store
build/
release/
/captures
-.externalNativeBuild
+.externalNativeBuild/
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 30aa626..ae78c11 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,29 +1,113 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..77ff185
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 5f934de..225fc39 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,20 +1,18 @@
+
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 74b7e12..61c7caf 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,5 +1,5 @@
-
+
@@ -25,7 +25,7 @@
-
+
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
index 7f68460..72f00ed 100644
--- a/.idea/runConfigurations.xml
+++ b/.idea/runConfigurations.xml
@@ -3,6 +3,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NOTICE.md b/NOTICE.md
index d085e1f..868293e 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -26,16 +26,76 @@ Android Support v4 Libraries:
Copyright (c) 2005-2008, The Android Open Source Project
Licensed under the Apache License, Version 2.0
-Xposed Bridge API library
--------------------------
-
-
- Copyright 2013 rovo89, Tungstwenty
- Licensed under the Apache License, Version 2.0
-
Holo Color Picker library
-------------------------
Copyright 2012 Lars Werkman
Licensed under the Apache License, Version 2.0
+
+
+IDENTICON STYLES
+================
+
+Retro Style
+-----------
+ Original implementation in ChameleonOS
+ Inspired by 8-bit retro game sprites
+ Generated locally on device from a hash of contact details
+
+Contemporary Style
+-----------------
+ Original implementation in ChameleonOS
+ Modern, abstract geometric identicon style
+ Generated locally on device from a hash of contact details
+
+Gmail Style
+-----------
+ Inspired by Google Mail/Gmail's contact icons
+ Simple colored letter icon style
+ Generated locally on device using the contact's initials
+
+Dot Matrix Style
+--------------
+ Original implementation in ChameleonOS
+ Pixel-based grid pattern similar to GitHub's original identicons
+ Generated locally on device from a hash of contact details
+
+Spirograph Style
+--------------
+ Original implementation in ChameleonOS
+ Creates geometric circular patterns similar to Spirograph toys
+ Generated locally on device from a hash of contact details
+
+Unicornify Style
+--------------
+ Based on unicornify.pictures by @balpha
+
+
+ This style downloads unicorn avatar images from an online service.
+ Images are dynamically fetched from https://unicornify.pictures/avatar/ when needed,
+ using the contact's hash to generate a unique unicorn avatar. Each request includes
+ the hash and desired image size. The images are not stored permanently on the server
+ but are generated algorithmically for each request.
+
+ NOTE: Unicorn icons are cached locally for offline use after they are first downloaded.
+ An internet connection is only required the first time an avatar is generated.
+ When offline, the app will display a warning message and use previously cached avatars.
+ If no cached avatar is available for a contact, the app will skip that contact and
+ preserve any existing photo
+
+Visiglyphs Style
+----------------
+ Based on Visiglyphs by Charles Darke @ digitalconsumption.com
+ Original PHP implementation: https://web.archive.org/web/20070929081457/http://digitalconsumption.com/files/pub-glyphs-beta.gz
+
+ This style generates geometric pattern identicons using a 3x3 grid layout.
+ Each identicon consists of colorful geometric shapes (triangles, diamonds, spikes,
+ blocks, etc.) arranged in a symmetric pattern. The algorithm uses 16 different
+ pattern types that can be rotated and colored based on the contact's hash.
+
+ The implementation is based on the original Visiglyphs algorithm which was
+ designed for IP address visualization but has been adapted for contact identicons.
+ All patterns are generated locally without requiring internet connectivity.
+
+ Original Visiglyphs license: BSD-like (see original source for full terms)
\ No newline at end of file
diff --git a/README.md b/README.md
index 02b5e77..349d6aa 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,131 @@
-Identiconizer!
-==============
+Identiconizer! - Android Contact Identicons App
+===============================================
+
+[](https://f-droid.org/packages/com.germainz.identiconizer/)
+[](https://opensource.org/licenses/Apache-2.0)
+
+**Generate unique geometric avatars for Android contacts automatically**
+
This is a port of ChameleonOS' contact identicons feature (available in the
-JellyBean versions) with some additional features and fixes.
+JellyBean versions) with additional features and fixes for modern Android.
When enabled, new contacts will be assigned a unique identicon instead of the
default picture.
-XDA Thread
-==========
-http://forum.xda-developers.com/showthread.php?t=2718943
+> **Keywords:** android app, contact management, identicons, avatars, profile pictures, geometric patterns, contact photos, f-droid
+
+Features
+========
+* Use identicons for newly created contacts. A background service is used to detect new contacts and automatically assign them an identicon.
+* Choose from seven different identicon styles: Retro, Contemporary, Spirograph, Dot Matrix, Gmail, Unicornify, and Visiglyphs.
+* Specify the identicon sizes, from 96x96 up to 720x720 (256x256 max on ICS.)
+* Choose a custom background color for the created identicons.
+* Option to use serif fonts in Gmail style identicons.
+* Option for more than one letter in Gmail Style identicons.
+* Create identicons for all contacts without a photo in one go.
+* Remove identicons from all contacts that have one set.
+* Contacts list to add/remove Identicon to/from wanted contacts only.
+* Short delay before creating identicons for new contacts, to avoid overwriting DAVdroid photos.
+
+How Identicons Work
+==================
+
+## Storage and Detection
+
+**Where are identicons stored?**
+Identicons are stored directly in Android's ContactsContract database as contact photos, just like regular profile pictures. They are saved in the `ContactsContract.Data` table with the MIME type `vnd.android.cursor.item/photo` in the `DATA15` field as binary data (byte arrays).
+
+**How does the app distinguish identicons from user photos?**
+The app uses a clever tagging system to identify its own generated identicons:
+- Each generated identicon contains a special marker string `"identicon_marker"` embedded in the image metadata
+- For PNG images: The marker is embedded as a custom chunk at the end of the file
+- For JPEG images: The marker is embedded in the EXIF data
+- When processing contacts, the app checks for this marker using `IdenticonUtils.isIdenticon()` to determine if an existing photo is an identicon or a user-uploaded image
+
+**Which contacts get identicons?**
+The app only replaces photos for contacts that:
+- Have no existing photo, OR
+- Have an existing identicon (when updating/changing styles)
+- Are in visible contact groups (unless "ignore visibility" is enabled)
+- Have a non-empty display name
+
+User-uploaded photos are never replaced - the app respects custom profile pictures.
+
+## Contact Sharing and Export
+
+**Are identicons exported when sharing contacts?**
+Yes! Since identicons are stored as standard contact photos in Android's database, they are included when:
+- Sharing contacts via Android's built-in sharing mechanisms
+- Exporting contacts to VCF (vCard) files
+- Syncing contacts with cloud services (Google Contacts, Exchange, etc.)
+- Backing up contacts
+
+The identicons will appear as regular profile pictures to other devices and applications, since they are stored using Android's standard photo storage format.
+
+**What happens on the receiving device?**
+- If the receiving device has Identiconizer installed: The app will recognize the identicon marker and can manage/replace these images
+- If the receiving device doesn't have Identiconizer: The identicons will appear as normal profile pictures and remain unchanged
+
+## Network Access and Privacy
+
+**Why does Identiconizer need the INTERNET permission?**
+
+Most identicon styles work entirely offline. However, the Unicornify style fetches unique unicorn avatar images from the public [unicornify.pictures](https://unicornify.pictures/) service. For this, the app requires the `INTERNET` permission.
+
+**What data is sent to the network?**
+
+- **No raw contact data is ever sent.**
+- For Unicornify, the app generates a privacy-preserving hash by combining a random per-installation salt with the contact's email (or other key), then computes the MD5 hash of this combination.
+- Only this salted hash is sent to the unicornify server to fetch the avatar image. The salt is unique to your app installation and never leaves your device.
+- This means the unicornify service cannot reverse the hash to obtain any contact information, and unicorn avatars are unique per app install.
+- All other identicon styles (Retro, Contemporary, Spirograph, Dot Matrix, Gmail, Visiglyphs) do not use the network at all.
+
+**Summary:**
+- The network permission is needed only for fetching unicorn avatars.
+- Only anonymized, salted hashes are sent; your contact data is never exposed to the network or third parties.
+
+## Technical Implementation
+
+**Contact Detection Process:**
+1. The app monitors the ContactsContract database for new contacts
+2. When a new contact is detected, it checks if the contact has an existing photo
+3. If no photo exists, or if the existing photo is an identicon, a new identicon is generated
+4. The identicon is created based on the contact's display name using MD5 hashing
+5. The generated image is tagged with the identicon marker and stored in the database
+
+**Offline Behavior:**
+- Most identicon styles work offline (Retro, Contemporary, Spirograph, Dot Matrix, Gmail, Visiglyphs)
+- The Unicornify style requires internet connection but caches downloaded avatars for offline use
+- When offline, Unicornify will use cached versions or skip contacts if no cache exists
+
+Links
+=====
+* [XDA Thread](http://forum.xda-developers.com/showthread.php?t=2718943)
+* [F-Droid Page](https://f-droid.org/packages/com.germainz.identiconizer/)
+
+## Topics and Keywords
+This repository contains an **Android application** for **contact management** that generates **identicons** and **avatars**. It's available on **F-Droid** and supports the **Xposed Framework**. Keywords: android app, contact photos, profile pictures, geometric patterns, avatar generator.
+
+## For Developers
+- **Language:** Java
+- **Platform:** Android (API 35+)
+- **Build System:** Gradle
+- **Distribution:** F-Droid
+- **Framework:** Android SDK + Xposed (optional)
+
+Development
+===========
+If you want to help developing the app, this will work as an example (tested on Ubuntu 24.04):
+
+- Install and open Android Studio
+- [Created a virtual AVD Device](https://developer.android.com/studio/run/managing-avds#createavd)
+- use File->New->Project from version control->Git
+- Wait for "Refreshing Identiconizer Gradle project" and "Updating Indices"
+- Run the App
+- Inside the simulator window: allow to access Contacts
+- use the Call Button to call any number and cancel
+- add the last Number as new contact and store it locally
+- Now the contact App works without google account and you can add more dummy contacts
+
+Licensing and Attributions
+=========================
+For detailed information about licensing, included libraries, and identicon styles, please see the [NOTICE.md](NOTICE.md) file.
diff --git a/build.gradle b/build.gradle
index e89d012..264d7c3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,17 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
- jcenter()
+ mavenCentral()
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.3.0'
+ classpath 'com.android.tools.build:gradle:8.11.1'
}
}
allprojects {
repositories {
- jcenter()
google()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
}
}
diff --git a/dev/TODO.md b/dev/TODO.md
new file mode 100644
index 0000000..ff538d9
--- /dev/null
+++ b/dev/TODO.md
@@ -0,0 +1,9 @@
+# TODO
+
+- [x] Add support for Android API level 35
+- [x] example icons in setting "Style" for each identicon type on Android 16 are missing
+- [x] remove XposedBridge in About
+- [x] add a new style "unicornify":
+- [x] add a new style "visiglyphs":
+- [x] randomly select from enabled styles
+- [x] visiglyphs identicons still have no visible radial gradient overlayer, it shoulc have one with two colors
\ No newline at end of file
diff --git a/dev/images/ic_identicons_style_unicornify_16.xcf b/dev/images/ic_identicons_style_unicornify_16.xcf
new file mode 100644
index 0000000..b5b78bd
Binary files /dev/null and b/dev/images/ic_identicons_style_unicornify_16.xcf differ
diff --git a/dev/plan.md b/dev/plan.md
new file mode 100644
index 0000000..422b820
--- /dev/null
+++ b/dev/plan.md
@@ -0,0 +1,42 @@
+# Identiconizer Project Plan
+
+## Notes
+- Android 12+ requires specifying PendingIntent mutability (FLAG_IMMUTABLE or FLAG_MUTABLE).
+- Errors are present in IdenticonCreationService.java and IdenticonRemovalService.java at lines where PendingIntent.getActivity is used without a flag.
+- FLAG_IMMUTABLE is usually preferred unless the PendingIntent needs to be mutable.
+- Patch version increased: versionName is now 1.4.1
+- Crash when adding contact pic due to missing FOREGROUND_SERVICE permission
+- Android 12+ (targetSdk 31+) requires specifying foregroundServiceType when starting a foreground service
+- foregroundServiceType added to IdenticonCreationService and IdenticonRemovalService in AndroidManifest.xml
+- Android 13+ with dataSync foreground service type requires FOREGROUND_SERVICE_DATA_SYNC permission
+- TODO.md contains additional roadmap items (API 35, icons in Style, remove XposedBridge, add unicornify style)
+- Build fixed: UnicornifyIdenticon now implements all required Identicon methods
+- Crash when using unicornify style due to missing INTERNET permission
+- INTERNET permission added to AndroidManifest.xml to support unicornify style
+- Unicorn avatars (unicornify style) are NOT cached for offline use; they are downloaded on demand each time needed.
+- Documentation and README updated: identicon style info, unicornify explanation, F-Droid/feature details added.
+- Unicorn avatars (unicornify style) should be cached locally for offline use. When offline, the app should show a warning and not replace existing identicons with blank images.
+
+## Task List
+- [x] Review all usages of PendingIntent.getActivity in IdenticonCreationService.java and IdenticonRemovalService.java
+- [x] Update each usage to specify FLAG_IMMUTABLE (or FLAG_MUTABLE if required)
+- [x] Increase patch version in build.gradle (versionName)
+- [x] Add FOREGROUND_SERVICE permission to AndroidManifest.xml
+- [x] Add foregroundServiceType to IdenticonCreationService and IdenticonRemovalService in AndroidManifest.xml
+- [x] Update IdenticonCreationService implementation to use startForeground with foregroundServiceType if required
+- [x] Update IdenticonRemovalService implementation to use startForeground with foregroundServiceType if required and import ServiceInfo if needed
+- [x] Add FOREGROUND_SERVICE_DATA_SYNC permission to AndroidManifest.xml
+- [x] Rebuild the APK and verify lint passes
+- [x] Add support for Android API level 35 (commit separately)
+- [x] Add example icons in Style setting for each identicon type on Android 16 (commit separately)
+- [x] Remove XposedBridge in About (commit separately)
+- [x] Add new style "unicornify" using unicornify.pictures avatar service (commit separately)
+- [x] Integrate UnicornifyIdenticon into IdenticonFactory
+- [x] Add unicornify style to style selection UI and resources
+- [x] Implement generateIdenticonByteArray(String) in UnicornifyIdenticon to fix build
+- [x] Add INTERNET permission to AndroidManifest.xml for unicornify style
+- [ ] Cache unicornify avatars locally for offline use
+- [ ] Show warning when offline and unicornify cannot download, do not replace existing identicons with blank images
+
+## Current Goal
+All tasks complete except for unicornify offline caching and warning behavior.
\ No newline at end of file
diff --git a/dev/test_visiglyphs_gradient.java b/dev/test_visiglyphs_gradient.java
new file mode 100644
index 0000000..9a45d01
--- /dev/null
+++ b/dev/test_visiglyphs_gradient.java
@@ -0,0 +1,43 @@
+import java.io.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Matrix;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+
+// Simple test to generate a Visiglyphs identicon with gradient
+public class TestVisiglyphsGradient {
+ public static void main(String[] args) {
+ // Test hash that should produce visible colors
+ String testInput = "test@example.com";
+
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] hash = md.digest(testInput.getBytes());
+
+ // Convert to hex string like the Android implementation
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ System.out.println("Test hash: " + hexString.toString());
+ System.out.println("This would generate a Visiglyphs identicon with gradient overlay");
+ System.out.println("The gradient should now use the identicon's own foreground colors");
+
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/dev/visiglyphs.php b/dev/visiglyphs.php
new file mode 100644
index 0000000..32ad97e
--- /dev/null
+++ b/dev/visiglyphs.php
@@ -0,0 +1,339 @@
+0){ // if we need to resample down
+ $blocksize=$resize;
+ $imgsizeR=$blocksize*3;
+ $imresize = imagecreatetruecolor($imgsizeR,$imgsizeR);
+ $backgroundcolor = imagecolorallocate($imresize, $bgr, $bgg, $bgb);
+ imagecopyresampled ( $imresize, $im, 0, 0, 0, 0, $imgsizeR, $imgsizeR, $imgsize, $imgsize );
+ //ImageColorTransparent($imresize,$backgroundcolor); // FIXME transparency feature not finished.
+ imagepng($imresize);
+ //imagepng($imresize,VISIUPLOAD.$filename.'.png'); // FIXME remove comments to generate file cache
+ } else {
+ imagepng($im);
+ //imagepng($im,VISIUPLOAD.$filename.'.png');// FIXME remove comments to generate file cache
+ }
+}
+
+function drawPatternOnCanvas($im, $patternType, $originx, $originy, $blocksize, $red, $quarter, $quarter3, $half) {
+ switch($patternType){
+ case 1: // #1 mountains
+ $points = array(
+ $originx, $originy,
+ $originx+$quarter, $originy+$blocksize,
+ $originx+$half, $originy
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ $points = array(
+ $originx+$half, $originy,
+ $originx+$quarter3, $originy+$blocksize,
+ $originx+$blocksize, $originy
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 2: // #2 half triangle
+ $points = array(
+ $originx, $originy,
+ $originx+$blocksize, $originy,
+ $originx, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 3: // #3 centre triangle
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy+$blocksize,
+ $originx+$blocksize, $originy
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 4: // #4 half block
+ imagefilledrectangle ( $im, $originx, $originy, $originx+$half, $originy+$blocksize, $red);
+ break;
+
+ case 5: // #5 half diamond
+ $points = array(
+ $originx+$quarter, $originy,
+ $originx, $originy+$half,
+ $originx+$quarter, $originy+$blocksize,
+ $originx+$half, $originy+$half
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 6: // #6 spike
+ $points = array(
+ $originx, $originy,
+ $originx+$blocksize, $originy+$half,
+ $originx+$blocksize, $originy+$blocksize,
+ $originx+$half, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 7: // #7 quarter triangle
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy+$blocksize,
+ $originx, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 8: // #8 diag triangle
+ $points = array(
+ $originx, $originy,
+ $originx+$blocksize, $originy+$half,
+ $originx+$half, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 9: // #9 centre mini triangle
+ $points = array(
+ $originx+$quarter, $originy+$quarter,
+ $originx+$quarter3, $originy+$quarter,
+ $originx+$quarter, $originy+$quarter3
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 10: // #10 diag mountains
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy,
+ $originx+$half, $originy+$half
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ $points = array(
+ $originx+$half, $originy+$half,
+ $originx+$blocksize, $originy+$half,
+ $originx+$blocksize, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 11: // #11 quarter block
+ imagefilledrectangle ( $im, $originx, $originy, $originx+$half, $originy+$half, $red);
+ break;
+
+ case 12: // #12 point out triangle
+ $points = array(
+ $originx, $originy+$half,
+ $originx+$half, $originy+$blocksize,
+ $originx+$blocksize, $originy+$half
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 13: // #13 point in triangle
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy+$half,
+ $originx+$blocksize, $originy
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 14: // #14 diag point in
+ $points = array(
+ $originx+$half, $originy+$half,
+ $originx, $originy+$half,
+ $originx+$half, $originy+$blocksize
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 15: // #15 diag point out
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy,
+ $originx, $originy+$half,
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ break;
+
+ case 16: // #16 diag side point out
+ default:
+ $points = array(
+ $originx, $originy,
+ $originx+$half, $originy,
+ $originx+$half, $originy+$half
+ );
+ $num = count($points) / 2;
+ imagefilledpolygon($im, $points, $num, $red);
+ } // end switch
+
+}
+$size=24;
+$filename='test';
+$ip=md5(rand(0,300)); // FIXME test random
+ glyph(
+ $size,
+ hexdec(substr($ip,0,1)), //block 1
+ hexdec(substr($ip,1,1)), //block 2
+ hexdec(substr($ip,2,1))&7, //centre
+ hexdec(substr($ip,3,1))&3, //rot 1
+ hexdec(substr($ip,4,1))&3, //rot 2
+ hexdec(substr($ip, 5,2))&239, //fg
+ hexdec(substr($ip, 7,2))&239,
+ hexdec(substr($ip, 9,2))&239,
+ hexdec(substr($ip,11,2))&239, //fg2
+ hexdec(substr($ip,13,2))&239,
+ hexdec(substr($ip,15,2))&239,
+ 255,255,255,
+ //hexdec(substr($ip,17,2)), //bg
+ //hexdec(substr($ip,19,2)),
+ //hexdec(substr($ip,21,2)),
+ $filename
+ );
+
+?>
diff --git a/dev/visiglyphs_example_1.png b/dev/visiglyphs_example_1.png
new file mode 100644
index 0000000..7d62a6a
Binary files /dev/null and b/dev/visiglyphs_example_1.png differ
diff --git a/dev/visiglyphs_example_2.png b/dev/visiglyphs_example_2.png
new file mode 100644
index 0000000..b8370a0
Binary files /dev/null and b/dev/visiglyphs_example_2.png differ
diff --git a/dev/visiglyphs_example_5.png b/dev/visiglyphs_example_5.png
new file mode 100644
index 0000000..a45a105
Binary files /dev/null and b/dev/visiglyphs_example_5.png differ
diff --git a/dev/visiglyphs_examples.jpg b/dev/visiglyphs_examples.jpg
new file mode 100644
index 0000000..2086d67
Binary files /dev/null and b/dev/visiglyphs_examples.jpg differ
diff --git a/fastlane/metadata/android/en-US/changelogs/1.5.txt b/fastlane/metadata/android/en-US/changelogs/1.5.txt
new file mode 100644
index 0000000..bf6434d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1.5.txt
@@ -0,0 +1,4 @@
+* Add new style "unicornify" using unicornify.pictures avatar service
+* Add new style "visiglyphs" with geometric pattern identicons
+* Unicornify avatars are now cached locally for offline use
+* Improved offline handling with user warnings
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 636ea41..d7d5a4b 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -3,7 +3,7 @@ This is a port of ChameleonOS' contact identicons feature (available in the Jell
Features:
* Use identicons for newly created contacts. A service is normally used to detect new contacts. If you use the Xposed Framework, you can enable Identiconizer! as a module instead to integrate the application into the system.
-* Choose from five different identicon styles: Retro, Contemporary, Spirograph, Dot Matrix and Gmail.
+* Choose from six different identicon styles: Retro, Contemporary, Spirograph, Dot Matrix, Gmail and Unicornify.
* Specify the identicon sizes, from 96x96 up to 720x720 (256x256 max on ICS.)
* Choose a custom background color for the created identicons.
* Create identicons for all contacts without a photo in one go.
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
index 9720255..f3c5b16 100644
Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4a3b778
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,7 @@
+# Project-wide Gradle settings.
+android.useAndroidX=true
+android.enableJetifier=true
+
+# Gradle settings
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7ec9ddc..11beb1c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
diff --git a/identiconizer/build.gradle b/identiconizer/build.gradle
index 3fba15b..058b9f9 100644
--- a/identiconizer/build.gradle
+++ b/identiconizer/build.gradle
@@ -1,31 +1,39 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+}
android {
- compileSdkVersion 27
+ namespace "com.germainz.identiconizer"
+ compileSdk 35
defaultConfig {
applicationId "com.germainz.identiconizer"
- minSdkVersion 14
- targetSdkVersion 27
- versionCode 11
- versionName "1.4"
+ minSdk 14
+ targetSdk 35
+ versionCode 13
+ versionName "1.5"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
}
}
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
dependencies {
- implementation 'com.android.support:appcompat-v7:27.1.1'
- implementation 'com.android.support:design:27.1.1'
- implementation 'com.android.support:support-v4:27.1.1'
- implementation 'com.android.support:recyclerview-v7:27.1.1'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.larswerkman:HoloColorPicker:1.5'
- implementation 'com.android.support:support-vector-drawable:27.1.1'
- compileOnly 'de.robv.android.xposed:api:82'
+ implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
+ // Xposed-Abhängigkeit wurde entfernt
}
diff --git a/identiconizer/src/main/AndroidManifest.xml b/identiconizer/src/main/AndroidManifest.xml
index 9b0e1ae..db58ab3 100644
--- a/identiconizer/src/main/AndroidManifest.xml
+++ b/identiconizer/src/main/AndroidManifest.xml
@@ -1,16 +1,19 @@
-
+
+
+
+
+
-
+
@@ -20,7 +23,7 @@
-
+
@@ -31,10 +34,12 @@
android:exported="false" />
+ android:exported="false"
+ android:foregroundServiceType="dataSync" />
+ android:exported="false"
+ android:foregroundServiceType="dataSync" />
getSelectedIdenticonStyles() {
+ java.util.Set defaultStyles = new java.util.HashSet<>();
+ defaultStyles.add("0"); // Default to retro style
+ return mPreferences.getStringSet(PREF_STYLES_MULTI, defaultStyles);
+ }
+
+ public void setSelectedIdenticonStyles(java.util.Set styles) {
+ mPreferences.edit().putStringSet(PREF_STYLES_MULTI, styles).commit();
+ }
+
+ public int getRandomIdenticonStyle() {
+ java.util.Set selectedStyles = getSelectedIdenticonStyles();
+ if (selectedStyles.isEmpty()) {
+ return 0; // Default to retro if no styles selected
+ }
+
+ // Convert to array for random selection
+ String[] stylesArray = selectedStyles.toArray(new String[0]);
+ int randomIndex = (int) (Math.random() * stylesArray.length);
+ return Integer.parseInt(stylesArray[randomIndex]);
+ }
+
+ /**
+ * Selects a consistent identicon style for a contact based on their name hash
+ * This ensures the same contact always gets the same style across recreations
+ *
+ * @param contactName The contact's name
+ * @return The style ID as integer, consistently selected for this contact
+ */
+ public int getConsistentIdenticonStyleForContact(String contactName) {
+ java.util.Set selectedStyles = getSelectedIdenticonStyles();
+ if (selectedStyles.isEmpty()) {
+ return 0; // Default to retro if no styles selected
+ }
+
+ if (selectedStyles.size() == 1) {
+ return Integer.parseInt(selectedStyles.iterator().next());
+ }
+
+ // Convert set to sorted list for consistent ordering
+ java.util.List stylesList = new java.util.ArrayList<>(selectedStyles);
+ java.util.Collections.sort(stylesList);
+
+ // Use contact name hash to deterministically select style
+ int hash = contactName.toLowerCase().hashCode();
+ int index = Math.abs(hash) % stylesList.size();
+
+ return Integer.parseInt(stylesList.get(index));
+ }
+
+ /**
+ * Gets the style name string for a given style ID
+ *
+ * @param styleId The style ID
+ * @return The style name string
+ */
+ public String getStyleNameForId(int styleId) {
+ switch (styleId) {
+ case 0: return "retro";
+ case 1: return "gmail";
+ case 2: return "github";
+ case 3: return "unicornify";
+ case 4: return "robohash";
+ case 5: return "wavatar";
+ case 6: return "visiglyphs";
+ default: return "retro";
+ }
+ }
+
+ private String getString(String key, String defaultValue) {
+ return mPreferences.getString(key, defaultValue);
+ }
+
+ private int getInt(String key, int defaultValue) {
+ return mPreferences.getInt(key, defaultValue);
+ }
+
+ private boolean getBoolean(String key, boolean defaultValue) {
+ return mPreferences.getBoolean(key, defaultValue);
}
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/ContactsListActivity.java b/identiconizer/src/main/java/com/germainz/identiconizer/ContactsListActivity.java
index dc5146f..eb4f821 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/ContactsListActivity.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/ContactsListActivity.java
@@ -27,12 +27,12 @@
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
-import android.support.annotation.NonNull;
-import android.support.v4.content.LocalBroadcastManager;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.DividerItemDecoration;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
+import androidx.annotation.NonNull;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -103,26 +103,22 @@ public boolean onCreateOptionsMenu(Menu menu) {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_add:
- startIdenticonService(SERVICE_ADD);
- break;
- case R.id.action_clear:
- startIdenticonService(SERVICE_REMOVE);
- break;
- case R.id.action_select_all:
- checkedItems.clear();
- for (int i = 0, j = mAdapter.getCount(); i < j; i++)
- checkedItems.add(i);
- mAdapter.notifyDataSetChanged();
- break;
- case R.id.action_deselect_all:
- checkedItems.clear();
- mAdapter.notifyDataSetChanged();
- break;
- case android.R.id.home:
- onBackPressed();
- break;
+ int itemId = item.getItemId();
+
+ if (itemId == R.id.action_add) {
+ startIdenticonService(SERVICE_ADD);
+ } else if (itemId == R.id.action_clear) {
+ startIdenticonService(SERVICE_REMOVE);
+ } else if (itemId == R.id.action_select_all) {
+ checkedItems.clear();
+ for (int i = 0, j = mAdapter.getCount(); i < j; i++)
+ checkedItems.add(i);
+ mAdapter.notifyDataSetChanged();
+ } else if (itemId == R.id.action_deselect_all) {
+ checkedItems.clear();
+ mAdapter.notifyDataSetChanged();
+ } else if (itemId == android.R.id.home) {
+ onBackPressed();
}
return true;
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/CursorRecyclerViewAdapter.java b/identiconizer/src/main/java/com/germainz/identiconizer/CursorRecyclerViewAdapter.java
index ca7d484..7703e03 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/CursorRecyclerViewAdapter.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/CursorRecyclerViewAdapter.java
@@ -20,7 +20,7 @@
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
/**
* Created by skyfishjy on 10/31/14.
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/IdenticonsSettings.java b/identiconizer/src/main/java/com/germainz/identiconizer/IdenticonsSettings.java
index 948b21c..f8f0aba 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/IdenticonsSettings.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/IdenticonsSettings.java
@@ -29,14 +29,16 @@
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
+import android.preference.MultiSelectListPreference;
+import com.germainz.identiconizer.preferences.IdenticonStyleMultiSelectPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.provider.ContactsContract;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.ActionBar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.ActionBar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -62,7 +64,7 @@ public class IdenticonsSettings extends AppCompatPreferenceActivity implements O
private static final int PERMISSIONS_REQUEST_CODE = 123;
private static final String ACTION_SETTINGS_ABOUT = "com.germainz.identiconizer.SETTINGS_ABOUT";
private SwitchPreference mEnabledPref;
- private ImageListPreference mStylePref;
+ private IdenticonStyleMultiSelectPreference mStylesPref;
private SwitchPreference mSerifPref;
private Preference mLengthPref;
private Preference mBgColorPref;
@@ -99,16 +101,16 @@ public void onCreate(Bundle savedInstanceState) {
mEnabledPref = (SwitchPreference) findPreference(Config.PREF_ENABLED);
mEnabledPref.setChecked(mConfig.isEnabled());
- if (mEnabledPref.isChecked() && !mConfig.isXposedModActive())
+ if (mEnabledPref.isChecked())
startService(new Intent(this, ContactsObserverService.class));
mEnabledPref.setOnPreferenceChangeListener(this);
PreferenceScreen prefSet = getPreferenceScreen();
- mStylePref = (ImageListPreference) prefSet.findPreference(Config.PREF_STYLE);
- mStylePref.setOnPreferenceChangeListener(this);
- int style = mConfig.getIdenticonStyle();
- mStylePref.setValue(String.valueOf(style));
- updateStyleSummary(style);
+ mStylesPref = (IdenticonStyleMultiSelectPreference) prefSet.findPreference(Config.PREF_STYLES_MULTI);
+ mStylesPref.setOnPreferenceChangeListener(this);
+ java.util.Set selectedStyles = mConfig.getSelectedIdenticonStyles();
+ mStylesPref.setValues(selectedStyles);
+ updateStylesSummary(selectedStyles);
Preference startServicePref = findPreference(Config.PREF_CREATE);
startServicePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@@ -176,8 +178,10 @@ public void onClick(DialogInterface dialogInterface, int i) {
mSerifPref = (SwitchPreference) findPreference(Config.PREF_SERIF);
mSerifPref.setChecked(mConfig.isIdenticonSerif());
- if (mConfig.getIdenticonStyle() != IdenticonFactory.IDENTICON_STYLE_GMAIL)
- mSerifPref.setEnabled(false);
+
+ // Enable serif preference if Gmail style is selected
+ boolean gmailSelected = selectedStyles.contains(String.valueOf(IdenticonFactory.IDENTICON_STYLE_GMAIL));
+ mSerifPref.setEnabled(gmailSelected);
mSerifPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean serif = !mConfig.isIdenticonSerif();
@@ -216,12 +220,13 @@ public void onClick(DialogInterface dialogInterface, int i) {
return true;
}
});
- if (mConfig.getIdenticonStyle() != IdenticonFactory.IDENTICON_STYLE_GMAIL)
- mLengthPref.setEnabled(false);
+ // Enable length preference if Gmail style is selected
+ mLengthPref.setEnabled(gmailSelected);
mBgColorPref = findPreference(Config.PREF_BG_COLOR);
- if (mConfig.getIdenticonStyle() == IdenticonFactory.IDENTICON_STYLE_GMAIL)
- mBgColorPref.setEnabled(false);
+ // Enable background color preference unless only Gmail style is selected
+ boolean onlyGmailSelected = selectedStyles.size() == 1 && gmailSelected;
+ mBgColorPref.setEnabled(!onlyGmailSelected);
mBgColorPref.setSummary(colorIntToRGB(mConfig.getIdenticonBgColor()));
mBgColorPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
@@ -339,25 +344,59 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], in
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference == mEnabledPref) {
mConfig.setEnabled((Boolean) newValue);
- if ((Boolean) newValue && !mConfig.isXposedModActive())
+ if ((Boolean) newValue)
startService(new Intent(this, ContactsObserverService.class));
- else if (!mConfig.isXposedModActive())
+ else
stopService(new Intent(this, ContactsObserverService.class));
return true;
- } else if (preference == mStylePref) {
- int style = Integer.valueOf((String) newValue);
- updateStyleSummary(style);
- mBgColorPref.setEnabled(style != IdenticonFactory.IDENTICON_STYLE_GMAIL);
- mSerifPref.setEnabled(style == IdenticonFactory.IDENTICON_STYLE_GMAIL);
- mLengthPref.setEnabled(style == IdenticonFactory.IDENTICON_STYLE_GMAIL);
+ } else if (preference == mStylesPref) {
+ @SuppressWarnings("unchecked")
+ java.util.Set selectedStyles = (java.util.Set) newValue;
+ updateStylesSummary(selectedStyles);
+ mConfig.setSelectedIdenticonStyles(selectedStyles);
+
+ // Enable/disable preferences based on whether Gmail style is selected
+ boolean gmailSelected = selectedStyles.contains(String.valueOf(IdenticonFactory.IDENTICON_STYLE_GMAIL));
+ boolean onlyGmailSelected = selectedStyles.size() == 1 && gmailSelected;
+ mBgColorPref.setEnabled(!onlyGmailSelected);
+ mSerifPref.setEnabled(gmailSelected);
+ mLengthPref.setEnabled(gmailSelected);
return true;
}
return false;
}
- private void updateStyleSummary(int value) {
- mStylePref.setSummary(mStylePref.getEntries()[mStylePref.findIndexOfValue("" + value)]);
- mConfig.setIdenticonStyle(value);
+ private void updateStylesSummary(java.util.Set selectedStyles) {
+ if (selectedStyles.isEmpty()) {
+ mStylesPref.setSummary("No styles selected");
+ return;
+ }
+
+ StringBuilder summary = new StringBuilder();
+ String[] entries = getResources().getStringArray(R.array.identicons_style_entries);
+ String[] values = getResources().getStringArray(R.array.identicons_style_values);
+
+ boolean first = true;
+ for (String styleValue : selectedStyles) {
+ if (!first) {
+ summary.append(", ");
+ }
+
+ // Find the corresponding entry name
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].equals(styleValue)) {
+ summary.append(entries[i]);
+ break;
+ }
+ }
+ first = false;
+ }
+
+ if (selectedStyles.size() > 1) {
+ summary.append(" (random selection)");
+ }
+
+ mStylesPref.setSummary(summary.toString());
}
public int getMaxContactPhotoSize() {
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/Identicon.java b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/Identicon.java
index af738bb..4c0b52f 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/Identicon.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/Identicon.java
@@ -28,6 +28,7 @@
public abstract class Identicon {
public static final String IDENTICON_MARKER = "identicon_marker";
+ public static final String STYLE_MARKER_PREFIX = "style:";
public static final String DEFAULT_IDENTICON_SALT =
"zG~v(+&>fLX|!#9D*BTj*#K>amB&TUB}T/jBOQih|Sg8}@N-^Rk|?VEXI,9EQPH]";
@@ -103,6 +104,25 @@ protected static byte[] makeTaggedIdenticon(byte[] original) {
return taggedImage;
}
+ /**
+ * Creates identicon metadata that includes the selected style
+ *
+ * @param original The png image to add the comment to
+ * @param styleName The identicon style name to store in metadata
+ * @return The same image provided with the added chunk containing style info
+ */
+ public static byte[] makeTaggedIdenticonWithStyle(byte[] original, String styleName) {
+ // Create combined marker with style info
+ String combinedMarker = IDENTICON_MARKER + "|" + STYLE_MARKER_PREFIX + styleName;
+ byte[] taggedBlock = makeTextBlock(combinedMarker);
+
+ byte[] taggedImage = new byte[original.length + taggedBlock.length];
+ ByteBuffer buffer = ByteBuffer.wrap(taggedImage);
+ buffer.put(original);
+ buffer.put(taggedBlock);
+ return taggedImage;
+ }
+
private static byte[] makeTextBlock(String text) {
byte[] block = new byte[text.length() + 1];
ByteBuffer blockBuffer = ByteBuffer.wrap(block);
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonFactory.java b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonFactory.java
index e609305..2404f31 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonFactory.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonFactory.java
@@ -31,6 +31,8 @@ public class IdenticonFactory {
public static final int IDENTICON_STYLE_SPIROGRAPH = 2;
public static final int IDENTICON_STYLE_DOTMATRIX = 3;
public static final int IDENTICON_STYLE_GMAIL = 4;
+ public static final int IDENTICON_STYLE_UNICORNIFY = 5;
+ public static final int IDENTICON_STYLE_VISIGLYPHS = 6;
/**
* Get the appropriate identicon class based on the type passed in
@@ -43,6 +45,21 @@ public class IdenticonFactory {
* @return
*/
public static Identicon makeIdenticon(int type, int size, int bgColor, boolean serif, int length) {
+ return makeIdenticon(null, type, size, bgColor, serif, length);
+ }
+
+ /**
+ * Get the appropriate identicon class based on the type passed in, with optional context
+ *
+ * @param context Optional context, required for UnicornifyIdenticon
+ * @param type Identicon type
+ * @param size Size of the identicon
+ * @param bgColor Background color
+ * @param serif Whether to use serif font
+ * @param length Length of text
+ * @return Appropriate Identicon implementation
+ */
+ public static Identicon makeIdenticon(Context context, int type, int size, int bgColor, boolean serif, int length) {
Identicon.SIZE = size;
Identicon.BG_COLOR = bgColor;
Identicon.SERIF = serif;
@@ -58,8 +75,12 @@ public static Identicon makeIdenticon(int type, int size, int bgColor, boolean s
return new DotMatrixIdenticon();
case IDENTICON_STYLE_GMAIL:
return new LetterTile();
+ case IDENTICON_STYLE_UNICORNIFY:
+ return new UnicornifyIdenticon(context);
+ case IDENTICON_STYLE_VISIGLYPHS:
+ return new VisiglyphsIdenticon();
default:
- throw new IllegalArgumentException("Unkown identicon type.");
+ throw new IllegalArgumentException("Unknown identicon type.");
}
}
@@ -72,7 +93,57 @@ public static Identicon makeIdenticon(int type, int size, int bgColor, boolean s
*/
public static Identicon makeIdenticon(Context context) {
Config config = Config.getInstance(context);
- return makeIdenticon(config.getIdenticonStyle(), config.getIdenticonSize(), config.getIdenticonBgColor(), config.isIdenticonSerif(), config.getIdenticonLength());
+ return makeIdenticon(context, config.getRandomIdenticonStyle(), config.getIdenticonSize(), config.getIdenticonBgColor(), config.isIdenticonSerif(), config.getIdenticonLength());
+ }
+
+ /**
+ * Get the appropriate identicon class using consistent style selection for a contact
+ *
+ * @param context Application context
+ * @param contactName Contact name for consistent style selection
+ * @return Appropriate Identicon implementation
+ */
+ public static Identicon makeIdenticonForContact(Context context, String contactName) {
+ Config config = Config.getInstance(context);
+ int styleId = config.getConsistentIdenticonStyleForContact(contactName);
+ return makeIdenticon(context, styleId, config.getIdenticonSize(), config.getIdenticonBgColor(), config.isIdenticonSerif(), config.getIdenticonLength());
+ }
+
+ /**
+ * Creates an identicon with style metadata embedded
+ *
+ * @param context Application context
+ * @param contactName Contact name for consistent style selection
+ * @param key The key to generate identicon for
+ * @return Identicon byte array with style metadata
+ */
+ public static byte[] makeIdenticonWithStyleMetadata(Context context, String contactName, String key) {
+ Config config = Config.getInstance(context);
+ int styleId = config.getConsistentIdenticonStyleForContact(contactName);
+ String styleName = config.getStyleNameForId(styleId);
+
+ Identicon identicon = makeIdenticon(context, styleId, config.getIdenticonSize(),
+ config.getIdenticonBgColor(), config.isIdenticonSerif(), config.getIdenticonLength());
+
+ // Generate the identicon bitmap
+ android.graphics.Bitmap bitmap = identicon.generateIdenticonBitmap(key);
+ if (bitmap == null) return null;
+
+ // Convert to byte array and add style metadata
+ java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream);
+ byte[] bytes = stream.toByteArray();
+ try {
+ stream.close();
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ }
+
+ if (bytes != null) {
+ return Identicon.makeTaggedIdenticonWithStyle(bytes, styleName);
+ }
+
+ return bytes;
}
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonUtils.java b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonUtils.java
index c95bc23..75cc931 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonUtils.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/IdenticonUtils.java
@@ -34,27 +34,146 @@ public static boolean isIdenticon(byte[] data) {
if (data == null || format == OTHER_FORMAT)
return false;
+ if (format == JPG_FORMAT) {
+ // Handle JPG format with fixed position
+ int start = data.length - 18;
+ int end = data.length - 2;
+ String charSet = "US-ASCII";
+
+ if (start < 0 || end <= start || end > data.length) {
+ return false;
+ }
+
+ byte[] tag = Arrays.copyOfRange(data, start, end);
+ try {
+ String tagString = new String(tag, charSet);
+ return Identicon.IDENTICON_MARKER.equals(tagString);
+ } catch (UnsupportedEncodingException e) {
+ return false;
+ }
+ } else {
+ // Handle PNG format - try multiple approaches to be robust
+
+ // Try different possible marker lengths to handle both old and new identicons
+ String[] possibleMarkers = {
+ Identicon.IDENTICON_MARKER, // Old format: "identicon_marker"
+ };
+
+ // Try the original fixed-length approach first
+ for (String expectedMarker : possibleMarkers) {
+ int start = data.length - (expectedMarker.length() + 1);
+ int end = data.length - 1;
+
+ if (start >= 0 && end > start && end <= data.length) {
+ byte[] tag = Arrays.copyOfRange(data, start, end);
+ try {
+ String tagString = new String(tag, "ISO-8859-1");
+ if (expectedMarker.equals(tagString)) {
+ return true;
+ }
+ } catch (UnsupportedEncodingException e) {
+ // Continue to next approach
+ }
+ }
+ }
+
+ // Try a broader search approach - look for the marker anywhere in the last part of the file
+ int searchStart = Math.max(0, data.length - 200); // Search last 200 bytes
+ try {
+ String endOfFile = new String(Arrays.copyOfRange(data, searchStart, data.length), "ISO-8859-1");
+ if (endOfFile.contains(Identicon.IDENTICON_MARKER)) {
+ return true;
+ }
+ } catch (UnsupportedEncodingException e) {
+ // Ignore and return false
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Extracts the style from an existing identicon's metadata
+ *
+ * @param imageData The identicon image data
+ * @return The style name, or null if not found or not an identicon
+ */
+ public static String getStyleFromIdenticon(byte[] imageData) {
+ if (!isIdenticon(imageData)) {
+ return null;
+ }
+
+ String markerString = extractMarkerString(imageData);
+ if (markerString != null && markerString.contains(Identicon.STYLE_MARKER_PREFIX)) {
+ String[] parts = markerString.split("\\|");
+ for (String part : parts) {
+ if (part.startsWith(Identicon.STYLE_MARKER_PREFIX)) {
+ return part.substring(Identicon.STYLE_MARKER_PREFIX.length());
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts the full marker string from identicon metadata
+ *
+ * @param data The identicon image data
+ * @return The marker string, or null if not found
+ */
+ private static String extractMarkerString(byte[] data) {
+ int format = getDataFormat(data);
+ if (data == null || format == OTHER_FORMAT)
+ return null;
+
int start, end;
String charSet;
if (format == JPG_FORMAT) {
start = data.length - 18;
- end = data.length -2;
+ end = data.length - 2;
charSet = "US-ASCII";
} else {
- start = data.length - (Identicon.IDENTICON_MARKER.length() + 1);
+ // For PNG, we need to find the actual marker length dynamically
+ // since it can now contain style information
+ start = findMarkerStart(data);
+ if (start == -1) return null;
end = data.length - 1;
charSet = "ISO-8859-1";
}
+
+ if (start < 0 || end <= start) return null;
+
byte[] tag = Arrays.copyOfRange(data, start, end);
-
- String tagString;
+
try {
- tagString = new String(tag, charSet);
+ return new String(tag, charSet);
} catch (UnsupportedEncodingException e) {
- return false;
+ return null;
}
+ }
- return Identicon.IDENTICON_MARKER.equals(tagString);
+ /**
+ * Finds the start position of the marker in PNG data
+ */
+ private static int findMarkerStart(byte[] data) {
+ // Search backwards for the identicon marker
+ String marker = Identicon.IDENTICON_MARKER;
+ byte[] markerBytes = marker.getBytes();
+
+ for (int i = data.length - markerBytes.length - 1; i >= 0; i--) {
+ boolean found = true;
+ for (int j = 0; j < markerBytes.length; j++) {
+ if (data[i + j] != markerBytes[j]) {
+ found = false;
+ break;
+ }
+ }
+ if (found) {
+ return i;
+ }
+ }
+ return -1;
}
private static int getDataFormat(byte[] data) {
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/UnicornifyIdenticon.java b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/UnicornifyIdenticon.java
new file mode 100644
index 0000000..421d185
--- /dev/null
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/UnicornifyIdenticon.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2013-2014 GermainZ@xda-developers.com
+ * Based on unicornify.pictures by @balpha
+ *
+ * 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 com.germainz.identiconizer.identicons;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Locale;
+
+public class UnicornifyIdenticon extends Identicon {
+
+ private static final String TAG = "UnicornifyIdenticon";
+ private static final String CACHE_DIR = "unicornify_cache";
+ private boolean offlineWarningShown = false;
+ private Context mContext;
+
+ /**
+ * Constructor that takes a Context for file operations and toast messages
+ *
+ * @param context Application context
+ */
+ public UnicornifyIdenticon(Context context) {
+ this.mContext = context;
+ }
+
+ /**
+ * Default constructor (required for compatibility)
+ * Note: Cache operations and toast messages will not work without setting context
+ */
+ public UnicornifyIdenticon() {
+ // Context must be set manually if using this constructor
+ }
+
+ /**
+ * Sets the context for this instance
+ * @param context Application context
+ */
+ public void setContext(Context context) {
+ this.mContext = context;
+ }
+
+ /**
+ * Generates a unicornify identicon bitmap using the provided hash
+ *
+ * @param hash A 16 byte hash used to generate the identicon
+ * @return The bitmap of the identicon created or null if offline and no cached version available
+ */
+ /**
+ * Generates a unicornify identicon bitmap using the provided key (email, name, etc.)
+ * Uses per-installation salt for privacy.
+ *
+ * @param key The contact identifier (email, etc.)
+ * @return The bitmap of the identicon created or null if offline and no cached version available
+ */
+ @Override
+ public Bitmap generateIdenticonBitmap(String key) {
+ if (key == null || key.isEmpty() || mContext == null) return null;
+ // Get per-installation salt
+ String salt = com.germainz.identiconizer.Config.getInstance(mContext).getUnicornifySalt(mContext);
+ String saltedKey = salt + key;
+ byte[] hash = generateHash(saltedKey);
+ // Convert hash to hex string
+ StringBuilder hexHash = new StringBuilder();
+ for (byte b : hash) {
+ hexHash.append(String.format("%02x", b & 0xff));
+ }
+ String hexHashStr = hexHash.toString();
+
+ // Determine the appropriate size (power of 2 between 32 and 128)
+ int size = SIZE;
+ // Round to nearest power of 2 between 32 and 128
+ if (size < 32) {
+ size = 32;
+ } else if (size > 128) {
+ size = 128;
+ } else {
+ // Find the nearest power of 2
+ size = 32;
+ while (size * 2 <= SIZE && size * 2 <= 128) {
+ size *= 2;
+ }
+ }
+
+ // Try to load from cache first
+ Bitmap cachedBitmap = loadFromCache(hexHashStr, size);
+ if (cachedBitmap != null) {
+ return cachedBitmap;
+ }
+
+ try {
+ // Build URL with hash and size
+ String urlString = String.format(Locale.US,
+ "https://unicornify.pictures/avatar/%s?s=%d",
+ hexHashStr, size);
+
+ // Download the image
+ URL url = new URL(urlString);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoInput(true);
+ connection.connect();
+ InputStream input = connection.getInputStream();
+ Bitmap bitmap = BitmapFactory.decodeStream(input);
+ input.close();
+ connection.disconnect();
+
+ // Save to cache for offline use
+ saveToCache(hexHashStr, size, bitmap);
+
+ return bitmap;
+ } catch (UnknownHostException e) {
+ // Network is unreachable - offline
+ if (!offlineWarningShown) {
+ showOfflineWarning();
+ offlineWarningShown = true;
+ }
+ Log.w(TAG, "Device is offline. Cannot download unicorn avatar.");
+ return null;
+ } catch (IOException e) {
+ Log.e(TAG, "Error downloading unicorn avatar: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Generates an identicon bitmap, as a byte array, using the provided hash
+ *
+ * @param hash A 16 byte hash used to generate the identicon
+ * @return The bitmap byte array of the identicon created or null if offline and no cached version available
+ */
+ @Override
+ public byte[] generateIdenticonByteArray(String key) {
+ Bitmap bitmap = generateIdenticonBitmap(key);
+ if (bitmap == null) {
+ return null;
+ }
+ return bitmapToByteArray(bitmap);
+ }
+
+ // Deprecated: byte[] version is not used for unicornify anymore
+ @Override
+ public Bitmap generateIdenticonBitmap(byte[] hash) {
+ // Not used for unicornify anymore, kept for compatibility
+ return null;
+ }
+
+ @Override
+ public byte[] generateIdenticonByteArray(byte[] hash) {
+ return null;
+ }
+ // --- Removed duplicate generateIdenticonBitmap(String) and generateIdenticonByteArray(String) below ---
+
+ /**
+ * Generates an identicon bitmap using the provided key to generate a hash
+ *
+ * @param key A non empty string used to generate a hash when creating the identicon
+ * @return The bitmap of the identicon created
+ */
+ // (Removed duplicate generateIdenticonBitmap(String) and generateIdenticonByteArray(String) here -- now handled above with salt)
+
+
+ /**
+ * Saves a bitmap to the local cache
+ *
+ * @param hexHash The hex hash string used as filename
+ * @param size The size of the bitmap
+ * @param bitmap The bitmap to save
+ */
+ private void saveToCache(String hexHash, int size, Bitmap bitmap) {
+ if (bitmap == null) {
+ return;
+ }
+
+ File cacheDir = new File(mContext.getCacheDir(), CACHE_DIR);
+ if (!cacheDir.exists()) {
+ cacheDir.mkdirs();
+ }
+
+ String fileName = String.format("%s_%d.png", hexHash, size);
+ File cacheFile = new File(cacheDir, fileName);
+
+ try (FileOutputStream out = new FileOutputStream(cacheFile)) {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ Log.d(TAG, "Saved unicorn avatar to cache: " + fileName);
+ } catch (IOException e) {
+ Log.e(TAG, "Error saving unicorn avatar to cache: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Loads a bitmap from the local cache
+ *
+ * @param hexHash The hex hash string used as filename
+ * @param size The size of the bitmap
+ * @return The cached bitmap or null if not found
+ */
+ public Bitmap loadFromCache(String hexHash, int size) {
+ File cacheDir = new File(mContext.getCacheDir(), CACHE_DIR);
+ if (!cacheDir.exists()) {
+ return null;
+ }
+
+ String fileName = String.format("%s_%d.png", hexHash, size);
+ File cacheFile = new File(cacheDir, fileName);
+
+ if (cacheFile.exists()) {
+ try (FileInputStream in = new FileInputStream(cacheFile)) {
+ Log.d(TAG, "Loaded unicorn avatar from cache: " + fileName);
+ return BitmapFactory.decodeStream(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading unicorn avatar from cache: " + e.getMessage());
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets a random cached Unicornify image for preview purposes
+ * @param size The desired size
+ * @return A random cached bitmap or null if no cache exists
+ */
+ public Bitmap getRandomCachedImage(int size) {
+ File cacheDir = new File(mContext.getCacheDir(), CACHE_DIR);
+ if (!cacheDir.exists()) {
+ return null;
+ }
+
+ // Get all cached files for this size
+ File[] cacheFiles = cacheDir.listFiles((dir, name) ->
+ name.endsWith("_" + size + ".png"));
+
+ if (cacheFiles == null || cacheFiles.length == 0) {
+ return null;
+ }
+
+ // Pick a random cached file
+ File randomFile = cacheFiles[(int) (Math.random() * cacheFiles.length)];
+
+ try (FileInputStream in = new FileInputStream(randomFile)) {
+ Log.d(TAG, "Using random cached unicorn: " + randomFile.getName());
+ return BitmapFactory.decodeStream(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading random cached unicorn: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Shows a warning toast when device is offline
+ */
+ private void showOfflineWarning() {
+ Toast.makeText(mContext,
+ "Device is offline. Using cached unicorn avatars if available.",
+ Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/identicons/VisiglyphsIdenticon.java b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/VisiglyphsIdenticon.java
new file mode 100644
index 0000000..ca563be
--- /dev/null
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/identicons/VisiglyphsIdenticon.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2013-2014 GermainZ@xda-developers.com
+ * Based on Visiglyphs by Charles Darke @ digitalconsumption.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.germainz.identiconizer.identicons;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+
+import java.io.ByteArrayOutputStream;
+
+public class VisiglyphsIdenticon extends Identicon {
+
+ /**
+ * Generates a visiglyphs identicon bitmap using the provided hash
+ *
+ * @param hash A 16 byte hash used to generate the identicon
+ * @return The bitmap of the identicon created
+ */
+ @Override
+ public Bitmap generateIdenticonBitmap(byte[] hash) {
+ if (hash.length != 16)
+ return null;
+
+ // Convert hash to hex string
+ StringBuilder hexHash = new StringBuilder();
+ for (byte b : hash) {
+ hexHash.append(String.format("%02x", b & 0xff));
+ }
+ String hexString = hexHash.toString();
+
+ // Extract pattern and color information from hash - exactly matching PHP
+ int i = Integer.parseInt(hexString.substring(0, 1), 16); // block 1 pattern
+ int j = Integer.parseInt(hexString.substring(1, 2), 16); // block 2 pattern
+ int k = Integer.parseInt(hexString.substring(2, 3), 16) & 7; // center pattern
+ int rot1 = Integer.parseInt(hexString.substring(3, 4), 16) & 3; // rotation 1
+ int rot2 = Integer.parseInt(hexString.substring(4, 5), 16) & 3; // rotation 2
+
+ // Extract colors from hash (ensure contrast by AND'ing foreground with 239)
+ int fgR = Integer.parseInt(hexString.substring(5, 7), 16) & 239;
+ int fgG = Integer.parseInt(hexString.substring(7, 9), 16) & 239;
+ int fgB = Integer.parseInt(hexString.substring(9, 11), 16) & 239;
+ int fgR2 = Integer.parseInt(hexString.substring(11, 13), 16) & 239;
+ int fgG2 = Integer.parseInt(hexString.substring(13, 15), 16) & 239;
+ int fgB2 = Integer.parseInt(hexString.substring(15, 17), 16) & 239;
+
+ // Background color - using white as in PHP default
+ int bgR = 255;
+ int bgG = 255;
+ int bgB = 255;
+
+ return generateVisiglyphBitmap(SIZE, i, j, k, rot1, rot2,
+ fgR, fgG, fgB, fgR2, fgG2, fgB2,
+ bgR, bgG, bgB);
+ }
+
+ /**
+ * Generates an identicon bitmap, as a byte array, using the provided hash
+ *
+ * @param hash A 16 byte hash used to generate the identicon
+ * @return The bitmap byte array of the identicon created
+ */
+ @Override
+ public byte[] generateIdenticonByteArray(byte[] hash) {
+ Bitmap bitmap = generateIdenticonBitmap(hash);
+ if (bitmap == null) {
+ return null;
+ }
+ return convertBitmapToByteArray(bitmap);
+ }
+
+ /**
+ * Generates an identicon bitmap using the provided key to generate a hash
+ *
+ * @param key A non empty string used to generate a hash when creating the identicon
+ * @return The bitmap of the identicon created
+ */
+ @Override
+ public Bitmap generateIdenticonBitmap(String key) {
+ return generateIdenticonBitmap(generateHash(saltedKey(key)));
+ }
+
+ /**
+ * Generates an identicon bitmap, as a byte array, using the provided key to generate a hash
+ *
+ * @param key A non empty string used to generate a hash when creating the identicon
+ * @return The bitmap byte array of the identicon created
+ */
+ @Override
+ public byte[] generateIdenticonByteArray(String key) {
+ Bitmap bitmap = generateIdenticonBitmap(key);
+ if (bitmap == null) {
+ return null;
+ }
+ return convertBitmapToByteArray(bitmap);
+ }
+
+ /**
+ * Converts a bitmap to a byte array
+ */
+ private byte[] convertBitmapToByteArray(Bitmap bitmap) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ return makeTaggedIdenticon(stream.toByteArray());
+ }
+
+ /**
+ * Generates the actual visiglyphs bitmap - exactly matching the PHP glyph() function
+ */
+ private Bitmap generateVisiglyphBitmap(int blockSize, int i, int j, int k,
+ int rot1, int rot2, int fgR, int fgG, int fgB,
+ int fgR2, int fgG2, int fgB2, int bgR, int bgG, int bgB) {
+
+ // set a minimum blocksize below which we draw bigger and then resample downwards for a better look
+ int resize = 0;
+ int minblocksize = 24;
+ if (blockSize < minblocksize) {
+ resize = blockSize;
+ blockSize = minblocksize;
+ }
+
+ int imgSize = blockSize * 3;
+ float quarter = blockSize / 4.0f;
+ float quarter3 = quarter * 3;
+ float half = blockSize / 2.0f;
+ float third = blockSize / 3.0f;
+ float center = imgSize / 2.0f;
+
+ // Create main bitmap
+ Bitmap bitmap = Bitmap.createBitmap(imgSize, imgSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.rgb(bgR, bgG, bgB));
+
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.FILL);
+
+ // Create temporary bitmaps for rotation
+ Bitmap tempBlock = Bitmap.createBitmap(blockSize * 2, blockSize, Bitmap.Config.ARGB_8888);
+ Bitmap rotateTemp = Bitmap.createBitmap(blockSize, blockSize, Bitmap.Config.ARGB_8888);
+
+ // Draw first pattern at origin (0,0)
+ paint.setColor(Color.rgb(fgR, fgG, fgB));
+ drawPatternOnCanvas(canvas, paint, i, 0, 0, blockSize);
+
+ // rotate block
+ Canvas rotateTempCanvas = new Canvas(rotateTemp);
+ rotateTempCanvas.drawColor(Color.rgb(bgR, bgG, bgB));
+ rotateTempCanvas.drawBitmap(bitmap, new Rect(0, 0, blockSize, blockSize), new Rect(0, 0, blockSize, blockSize), null);
+
+ // Rotate first block
+ if (rot1 > 0) {
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rot1 * 90, blockSize / 2.0f, blockSize / 2.0f);
+ rotateTemp = Bitmap.createBitmap(rotateTemp, 0, 0, blockSize, blockSize, matrix, true);
+ }
+
+ // Clear and redraw rotated first block
+ canvas.drawColor(Color.rgb(bgR, bgG, bgB));
+ canvas.drawBitmap(rotateTemp, 0, 0, null);
+
+ // Draw second pattern at (blockSize, 0)
+ paint.setColor(Color.rgb(fgR2, fgG2, fgB2));
+ drawPatternOnCanvas(canvas, paint, j, blockSize, 0, blockSize);
+
+ // rotate block
+ rotateTempCanvas.drawColor(Color.rgb(bgR, bgG, bgB));
+ rotateTempCanvas.drawBitmap(bitmap, new Rect(blockSize, 0, blockSize * 2, blockSize), new Rect(0, 0, blockSize, blockSize), null);
+
+ // Rotate second block
+ if (rot2 > 0) {
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rot2 * 90, blockSize / 2.0f, blockSize / 2.0f);
+ rotateTemp = Bitmap.createBitmap(rotateTemp, 0, 0, blockSize, blockSize, matrix, true);
+ }
+
+ // Redraw second block rotated
+ canvas.drawBitmap(rotateTemp, blockSize, 0, null);
+
+ // copy blocks to form radial pattern
+ Canvas tempBlockCanvas = new Canvas(tempBlock);
+ for (int roundabout = 0; roundabout < 3; roundabout++) {
+ // Copy current blocks
+ tempBlockCanvas.drawColor(Color.rgb(bgR, bgG, bgB));
+ tempBlockCanvas.drawBitmap(bitmap, new Rect(0, 0, blockSize * 2, blockSize), new Rect(0, 0, blockSize * 2, blockSize), null);
+
+ // rotate
+ Matrix matrix = new Matrix();
+ matrix.postRotate(90, imgSize / 2.0f, imgSize / 2.0f);
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, imgSize, imgSize, matrix, true);
+ canvas = new Canvas(bitmap);
+
+ // paste back
+ canvas.drawBitmap(tempBlock, 0, 0, null);
+ }
+
+ // Draw center pattern
+ paint.setColor(Color.rgb(fgR, fgG, fgB));
+ int centerX = blockSize;
+ int centerY = blockSize;
+ // draw centre
+
+ switch (k) {
+ case 1: // circle
+ canvas.drawCircle(center, center, quarter3 / 2.0f, paint);
+ break;
+
+ case 2: // quarter square
+ canvas.drawRect(centerX + quarter, centerY + quarter,
+ centerX + quarter3, centerY + quarter3, paint);
+ break;
+
+ case 3: // full square
+ canvas.drawRect(centerX, centerY, centerX + blockSize, centerY + blockSize, paint);
+ break;
+
+ case 4: // quarter diamond
+ Path path = new Path();
+ path.moveTo(centerX + half, centerY + quarter);
+ path.lineTo(centerX + quarter3, centerY + half);
+ path.lineTo(centerX + half, centerY + quarter3);
+ path.lineTo(centerX + quarter, centerY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 5: // diamond
+ Path diamondPath = new Path();
+ diamondPath.moveTo(centerX + half, centerY);
+ diamondPath.lineTo(centerX, centerY + half);
+ diamondPath.lineTo(centerX + half, centerY + blockSize);
+ diamondPath.lineTo(centerX + blockSize, centerY + half);
+ diamondPath.close();
+ canvas.drawPath(diamondPath, paint);
+ break;
+
+ default:
+ // empty space
+ break;
+ }
+
+ // Apply radial gradient overlay (matching PHP Visiglyphs finish)
+ applyRadialGradientOverlay(canvas, imgSize, fgR, fgG, fgB, fgR2, fgG2, fgB2);
+
+ // if we need to resample down
+ if (resize > 0) {
+ blockSize = resize;
+ int imgsizeR = blockSize * 3;
+ Bitmap imresize = Bitmap.createBitmap(imgsizeR, imgsizeR, Bitmap.Config.ARGB_8888);
+ Canvas resizeCanvas = new Canvas(imresize);
+ resizeCanvas.drawColor(Color.rgb(bgR, bgG, bgB));
+
+ // Scale down the bitmap
+ Matrix matrix = new Matrix();
+ float scale = (float) imgsizeR / imgSize;
+ matrix.postScale(scale, scale);
+ resizeCanvas.drawBitmap(bitmap, matrix, null);
+
+ return imresize;
+ }
+
+ return bitmap;
+ }
+
+ /**
+ * Applies a radial gradient overlay to create the classic Visiglyphs finish effect
+ * Uses the identicon's own foreground colors to create a visible gradient effect
+ */
+ private void applyRadialGradientOverlay(Canvas canvas, int imgSize, int fgR, int fgG, int fgB, int fgR2, int fgG2, int fgB2) {
+ float centerX = imgSize / 2.0f;
+ float centerY = imgSize / 2.0f;
+ float radius = imgSize * 0.7f; // Slightly larger radius for better coverage
+
+ // Create radial gradient using the identicon's own foreground colors
+ // Center: Semi-transparent first foreground color (lighter)
+ // Edge: Semi-transparent second foreground color (darker)
+ int centerColor = Color.argb(30, fgR, fgG, fgB); // 12% opacity - subtle center
+ int edgeColor = Color.argb(80, fgR2, fgG2, fgB2); // 31% opacity - more visible edge
+
+ RadialGradient gradient = new RadialGradient(
+ centerX, centerY, radius,
+ centerColor, edgeColor, // Center to edge: light to dark
+ Shader.TileMode.CLAMP
+ );
+
+ Paint gradientPaint = new Paint();
+ gradientPaint.setShader(gradient);
+ gradientPaint.setAntiAlias(true);
+
+ // Use multiply blend mode for better color interaction
+ gradientPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+
+ // Draw the gradient overlay
+ canvas.drawCircle(centerX, centerY, radius, gradientPaint);
+ }
+
+ /**
+ * Draws a specific pattern exactly matching the PHP switch statement
+ */
+ private void drawPatternOnCanvas(Canvas canvas, Paint paint, int patternType, int originX, int originY, int blockSize) {
+
+ float quarter = blockSize / 4.0f;
+ float quarter3 = quarter * 3;
+ float half = blockSize / 2.0f;
+
+ Path path = new Path();
+
+ switch (patternType) {
+ case 1: // #1 mountains
+ // First mountain
+ path.moveTo(originX, originY);
+ path.lineTo(originX + quarter, originY + blockSize);
+ path.lineTo(originX + half, originY);
+ path.close();
+ canvas.drawPath(path, paint);
+
+ // Second mountain
+ path.reset();
+ path.moveTo(originX + half, originY);
+ path.lineTo(originX + quarter3, originY + blockSize);
+ path.lineTo(originX + blockSize, originY);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 2: // #2 half triangle
+ path.moveTo(originX, originY);
+ path.lineTo(originX + blockSize, originY);
+ path.lineTo(originX, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 3: // #3 centre triangle
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY + blockSize);
+ path.lineTo(originX + blockSize, originY);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 4: // #4 half block
+ canvas.drawRect(originX, originY, originX + half, originY + blockSize, paint);
+ break;
+
+ case 5: // #5 half diamond
+ path.moveTo(originX + quarter, originY);
+ path.lineTo(originX, originY + half);
+ path.lineTo(originX + quarter, originY + blockSize);
+ path.lineTo(originX + half, originY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 6: // #6 spike
+ path.moveTo(originX, originY);
+ path.lineTo(originX + blockSize, originY + half);
+ path.lineTo(originX + blockSize, originY + blockSize);
+ path.lineTo(originX + half, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 7: // #7 quarter triangle
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY + blockSize);
+ path.lineTo(originX, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 8: // #8 diag triangle
+ path.moveTo(originX, originY);
+ path.lineTo(originX + blockSize, originY + half);
+ path.lineTo(originX + half, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 9: // #9 centre mini triangle
+ path.moveTo(originX + quarter, originY + quarter);
+ path.lineTo(originX + quarter3, originY + quarter);
+ path.lineTo(originX + quarter, originY + quarter3);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 10: // #10 diag mountains
+ // First part
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY);
+ path.lineTo(originX + half, originY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+
+ // Second part
+ path.reset();
+ path.moveTo(originX + half, originY + half);
+ path.lineTo(originX + blockSize, originY + half);
+ path.lineTo(originX + blockSize, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 11: // #11 quarter block
+ canvas.drawRect(originX, originY, originX + half, originY + half, paint);
+ break;
+
+ case 12: // #12 point out triangle
+ path.moveTo(originX, originY + half);
+ path.lineTo(originX + half, originY + blockSize);
+ path.lineTo(originX + blockSize, originY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 13: // #13 point in triangle
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY + half);
+ path.lineTo(originX + blockSize, originY);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 14: // #14 diag point in
+ path.moveTo(originX + half, originY + half);
+ path.lineTo(originX, originY + half);
+ path.lineTo(originX + half, originY + blockSize);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 15: // #15 diag point out
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY);
+ path.lineTo(originX, originY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+
+ case 0: // #16 diag side point out (case 16 becomes 0 after modulo)
+ default:
+ path.moveTo(originX, originY);
+ path.lineTo(originX + half, originY);
+ path.lineTo(originX + half, originY + half);
+ path.close();
+ canvas.drawPath(path, paint);
+ break;
+ }
+ }
+}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/preferences/IdenticonStyleMultiSelectPreference.java b/identiconizer/src/main/java/com/germainz/identiconizer/preferences/IdenticonStyleMultiSelectPreference.java
new file mode 100644
index 0000000..4fd609f
--- /dev/null
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/preferences/IdenticonStyleMultiSelectPreference.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2013-2014 GermainZ@xda-developers.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.germainz.identiconizer.preferences;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.preference.MultiSelectListPreference;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.germainz.identiconizer.Config;
+import com.germainz.identiconizer.R;
+import com.germainz.identiconizer.identicons.Identicon;
+import com.germainz.identiconizer.identicons.IdenticonFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Random;
+import android.util.Log;
+
+public class IdenticonStyleMultiSelectPreference extends MultiSelectListPreference {
+
+ private Context mContext;
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private Set mValues = new HashSet<>();
+ private boolean[] mCheckedItems;
+ private Random mRandom = new Random();
+ private String[] mSampleNames = {"Alice", "Bob", "Charlie", "Diana", "Emma", "Frank", "Grace", "Henry", "Ivy", "Jack"};
+
+ public IdenticonStyleMultiSelectPreference(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public IdenticonStyleMultiSelectPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ mEntries = getEntries();
+ mEntryValues = getEntryValues();
+
+ if (mEntries == null || mEntryValues == null || mEntries.length != mEntryValues.length) {
+ throw new IllegalStateException("Entries and entryValues must be non-null and have the same length");
+ }
+
+ mValues = getValues();
+ mCheckedItems = new boolean[mEntries.length];
+
+ for (int i = 0; i < mEntryValues.length; i++) {
+ mCheckedItems[i] = mValues.contains(mEntryValues[i].toString());
+ }
+
+ IdenticonStyleAdapter adapter = new IdenticonStyleAdapter();
+ ListView listView = new ListView(mContext);
+ listView.setAdapter(adapter);
+ listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ builder.setView(listView);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Set newValues = new HashSet<>();
+ for (int i = 0; i < mCheckedItems.length; i++) {
+ if (mCheckedItems[i]) {
+ newValues.add(mEntryValues[i].toString());
+ }
+ }
+
+ if (callChangeListener(newValues)) {
+ setValues(newValues);
+ }
+ }
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, null);
+ }
+
+ private class IdenticonStyleAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mEntries.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mEntries[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ view = inflater.inflate(R.layout.identicon_style_preference_item, parent, false);
+ }
+
+ ImageView iconView = view.findViewById(R.id.identicon_preview);
+ TextView titleView = view.findViewById(R.id.style_title);
+ CheckBox checkBox = view.findViewById(R.id.style_checkbox);
+
+ // Set the style name
+ titleView.setText(mEntries[position]);
+
+ // Set checkbox state
+ checkBox.setChecked(mCheckedItems[position]);
+
+ // Generate preview identicon
+ int styleId = Integer.parseInt(mEntryValues[position].toString());
+ generatePreviewIdenticon(iconView, styleId);
+
+ // Handle item clicks
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCheckedItems[position] = !mCheckedItems[position];
+ checkBox.setChecked(mCheckedItems[position]);
+ }
+ });
+
+ return view;
+ }
+
+ private void generatePreviewIdenticon(ImageView imageView, int styleId) {
+ // Set a loading placeholder first
+ imageView.setImageResource(R.drawable.ic_settings_identicons);
+
+ // Generate random sample name for variety
+ String sampleName = mSampleNames[mRandom.nextInt(mSampleNames.length)] + mRandom.nextInt(1000);
+
+ // Handle Unicornify (styleId 5) with special fallback logic
+ if (styleId == 5) { // Unicornify
+ generateUnicornifyPreviewWithFallback(imageView, sampleName);
+ } else {
+ // Generate other identicons synchronously (they're fast)
+ generateRegularPreview(imageView, styleId, sampleName);
+ }
+ }
+
+ private void generateRegularPreview(ImageView imageView, int styleId, String sampleName) {
+ try {
+ Config config = Config.getInstance(mContext);
+
+ // Create identicon with smaller size for preview
+ Identicon identicon = IdenticonFactory.makeIdenticon(mContext, styleId,
+ 64, config.getIdenticonBgColor(), config.isIdenticonSerif(), config.getIdenticonLength());
+
+ Bitmap bitmap = identicon.generateIdenticonBitmap(sampleName);
+
+ if (bitmap != null) {
+ imageView.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
+ } else {
+ // Fallback to default icon if generation fails
+ imageView.setImageResource(R.drawable.ic_settings_identicons);
+ }
+ } catch (Exception e) {
+ // Fallback to default icon on error
+ imageView.setImageResource(R.drawable.ic_settings_identicons);
+ }
+ }
+
+ private void generateUnicornifyPreviewWithFallback(ImageView imageView, String sampleName) {
+ Log.d("IdenticonPreview", "Generating Unicornify preview with fallback");
+
+ // First try to get any random cached Unicornify image (fast)
+ try {
+ com.germainz.identiconizer.identicons.UnicornifyIdenticon unicornify =
+ new com.germainz.identiconizer.identicons.UnicornifyIdenticon(mContext);
+
+ // Try to get any random cached image first
+ android.graphics.Bitmap cachedBitmap = unicornify.getRandomCachedImage(64);
+
+ if (cachedBitmap != null) {
+ Log.d("IdenticonPreview", "Using random cached Unicornify image");
+ imageView.setImageDrawable(new BitmapDrawable(mContext.getResources(), cachedBitmap));
+ return;
+ }
+
+ Log.d("IdenticonPreview", "No cached images found, using drawable fallback");
+ } catch (Exception e) {
+ Log.w("IdenticonPreview", "Cache check failed: " + e.getMessage());
+ }
+
+ // If no cache, use the embedded drawable fallback
+ imageView.setImageResource(R.drawable.unicorn_preview_fallback);
+ }
+
+
+ }
+}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/receivers/BootCompletedReceiver.java b/identiconizer/src/main/java/com/germainz/identiconizer/receivers/BootCompletedReceiver.java
index b493ff6..1de642e 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/receivers/BootCompletedReceiver.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/receivers/BootCompletedReceiver.java
@@ -27,7 +27,7 @@ public class BootCompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Config config = Config.getInstance(context);
- if (config.isEnabled() && !config.isXposedModActive())
+ if (config.isEnabled())
context.startService(new Intent(context, ContactsObserverService.class));
}
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonCreationService.java b/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonCreationService.java
index 10de64f..f3f0941 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonCreationService.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonCreationService.java
@@ -22,6 +22,7 @@
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.pm.ServiceInfo;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -30,9 +31,10 @@
import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.LocalBroadcastManager;
+import androidx.core.app.NotificationCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.text.TextUtils;
+import android.util.Log;
import com.germainz.identiconizer.Config;
import com.germainz.identiconizer.ContactInfo;
@@ -59,7 +61,12 @@ public IdenticonCreationService() {
@Override
protected void onHandleIntent(Intent intent) {
- startForeground(SERVICE_NOTIFICATION_ID, createNotification());
+ // For Android 12+ (API 31+), foreground service type must be specified
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(SERVICE_NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
+ } else {
+ startForeground(SERVICE_NOTIFICATION_ID, createNotification());
+ }
// If a predefined contacts list is provided, use it directly.
// contactsList is set when this service is started from ContactsListActivity.
if (intent.hasExtra("contactsList")) {
@@ -76,7 +83,7 @@ protected void onHandleIntent(Intent intent) {
}
if (mUpdateErrors.size() > 0 || mInsertErrors.size() > 0)
createNotificationForError();
- LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("CONTACTS_UPDATED"));
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("CONTACTS_UPDATED"));
getContentResolver().notifyChange(ContactsContract.Data.CONTENT_URI, null);
stopForeground(true);
}
@@ -137,15 +144,122 @@ private byte[] getContactPhotoBlob(long photoId) {
return blob;
}
+ /**
+ * Gets existing contact photo for style preservation check
+ *
+ * @param contactId The contact's raw contact ID
+ * @return The existing photo bytes, or null if none exists
+ */
+ private byte[] getExistingContactPhoto(int contactId) {
+ String[] projection = new String[]{ContactsContract.Data.DATA15};
+ String where = ContactsContract.Data.RAW_CONTACT_ID + " == "
+ + String.valueOf(contactId) + " AND " + ContactsContract.Data.MIMETYPE + "=='"
+ + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'";
+ Cursor cursor = getContentResolver().query(
+ ContactsContract.Data.CONTENT_URI,
+ projection,
+ where,
+ null,
+ null);
+ byte[] blob = null;
+ if (cursor.moveToFirst()) {
+ blob = cursor.getBlob(0);
+ }
+ cursor.close();
+ return blob;
+ }
+
+ /**
+ * Generates identicon with a specific style, preserving the style metadata
+ *
+ * @param name Contact name
+ * @param styleName Style name to use
+ * @return Identicon byte array with style metadata
+ */
+ private byte[] generateIdenticonWithSpecificStyle(String name, String styleName) {
+ Config config = Config.getInstance(this);
+ int styleId = getStyleIdFromName(styleName);
+
+ Identicon identicon = IdenticonFactory.makeIdenticon(this, styleId,
+ config.getIdenticonSize(), config.getIdenticonBgColor(),
+ config.isIdenticonSerif(), config.getIdenticonLength());
+
+ // Generate the identicon bitmap
+ android.graphics.Bitmap bitmap = identicon.generateIdenticonBitmap(name);
+ if (bitmap == null) return null;
+
+ // Convert to byte array and add style metadata
+ java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream);
+ byte[] bytes = stream.toByteArray();
+ try {
+ stream.close();
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ }
+
+ if (bytes != null) {
+ return Identicon.makeTaggedIdenticonWithStyle(bytes, styleName);
+ }
+
+ return bytes;
+ }
+
+ /**
+ * Converts style name to style ID
+ *
+ * @param styleName The style name
+ * @return The style ID
+ */
+ private int getStyleIdFromName(String styleName) {
+ if (styleName == null) return 0;
+
+ switch (styleName.toLowerCase()) {
+ case "retro": return 0;
+ case "gmail": return 1;
+ case "github": return 2;
+ case "unicornify": return 3;
+ case "robohash": return 4;
+ case "wavatar": return 5;
+ case "visiglyphs": return 6;
+ default: return 0;
+ }
+ }
+
private void generateIdenticon(int contactId, String name) {
if (!TextUtils.isEmpty(name)) {
updateNotification(getString(R.string.identicons_creation_service_running_title),
String.format(getString(R.string.identicons_creation_service_contact_summary),
name)
);
- final Identicon identicon = IdenticonFactory.makeIdenticon(this);
- final byte[] identiconImage = identicon.generateIdenticonByteArray(name);
- setContactPhoto(getContentResolver(), identiconImage, contactId, name);
+
+ // First, check if contact already has an identicon with style metadata
+ byte[] existingPhoto = getExistingContactPhoto(contactId);
+ String preservedStyle = null;
+
+ if (existingPhoto != null && IdenticonUtils.isIdenticon(existingPhoto)) {
+ preservedStyle = IdenticonUtils.getStyleFromIdenticon(existingPhoto);
+ Log.d(TAG, "Found existing identicon for " + name + " with style: " + preservedStyle);
+ }
+
+ byte[] identiconImage;
+
+ if (preservedStyle != null) {
+ // Use preserved style to recreate identicon
+ identiconImage = generateIdenticonWithSpecificStyle(name, preservedStyle);
+ } else {
+ // Generate new identicon with style metadata using consistent selection
+ identiconImage = IdenticonFactory.makeIdenticonWithStyleMetadata(this, name, name);
+ }
+
+ // Check if the identicon generation was successful
+ // This is important for UnicornifyIdenticon which might return null when offline and no cached version exists
+ if (identiconImage != null) {
+ setContactPhoto(getContentResolver(), identiconImage, contactId, name);
+ } else {
+ // Skip this contact when offline and no cached avatar available
+ Log.w(TAG, "Skipping contact " + name + " - Cannot generate identicon (device may be offline)");
+ }
}
}
@@ -205,7 +319,7 @@ private Notification createNotification() {
manager.createNotificationChannel(chan);
}
Intent intent = new Intent(this, IdenticonsSettings.class);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
return new NotificationCompat.Builder(this, TAG)
.setAutoCancel(false)
.setOngoing(true)
@@ -219,11 +333,10 @@ private Notification createNotification() {
private void updateNotification(String title, String text) {
Intent intent = new Intent(this, IdenticonsSettings.class);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationManager nm =
(NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
- @SuppressWarnings("deprecation")
- Notification notice = new Notification.Builder(this)
+ Notification notice = new NotificationCompat.Builder(this, TAG)
.setAutoCancel(false)
.setOngoing(true)
.setContentTitle(title)
@@ -231,7 +344,7 @@ private void updateNotification(String title, String text) {
.setSmallIcon(R.drawable.ic_settings_identicons)
.setWhen(System.currentTimeMillis())
.setContentIntent(contentIntent)
- .getNotification();
+ .build();
nm.notify(SERVICE_NOTIFICATION_ID, notice);
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonRemovalService.java b/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonRemovalService.java
index 07a3007..685b6b8 100644
--- a/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonRemovalService.java
+++ b/identiconizer/src/main/java/com/germainz/identiconizer/services/IdenticonRemovalService.java
@@ -19,6 +19,7 @@
import android.app.IntentService;
import android.app.Notification;
+import android.content.pm.ServiceInfo;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -32,8 +33,8 @@
import android.os.Build;
import android.os.RemoteException;
import android.provider.ContactsContract;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.LocalBroadcastManager;
+import androidx.core.app.NotificationCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.Log;
import com.germainz.identiconizer.ContactInfo;
@@ -55,7 +56,12 @@ public IdenticonRemovalService() {
@Override
protected void onHandleIntent(Intent intent) {
- startForeground(SERVICE_NOTIFICATION_ID, createNotification());
+ // For Android 12+ (API 31+), foreground service type must be specified
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(SERVICE_NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
+ } else {
+ startForeground(SERVICE_NOTIFICATION_ID, createNotification());
+ }
// If a predefined contacts list is provided, use it directly.
// contactsList is set when this service is started from ContactsListActivity.
if (intent.hasExtra("contactsList")) {
@@ -64,7 +70,7 @@ protected void onHandleIntent(Intent intent) {
} else {
processPhotos();
}
- LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("CONTACTS_UPDATED"));
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("CONTACTS_UPDATED"));
stopForeground(true);
}
@@ -183,7 +189,7 @@ private Notification createNotification() {
manager.createNotificationChannel(chan);
}
Intent intent = new Intent(this, IdenticonsSettings.class);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
return new NotificationCompat.Builder(this, TAG)
.setAutoCancel(false)
.setOngoing(true)
@@ -197,11 +203,10 @@ private Notification createNotification() {
private void updateNotification(String title, String text) {
Intent intent = new Intent(this, IdenticonsSettings.class);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationManager nm =
(NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
- @SuppressWarnings("deprecation")
- Notification notice = new Notification.Builder(this)
+ Notification notice = new NotificationCompat.Builder(this, TAG)
.setAutoCancel(false)
.setOngoing(true)
.setContentTitle(title)
@@ -209,7 +214,7 @@ private void updateNotification(String title, String text) {
.setSmallIcon(R.drawable.ic_settings_identicons)
.setWhen(System.currentTimeMillis())
.setContentIntent(contentIntent)
- .getNotification();
+ .build();
nm.notify(SERVICE_NOTIFICATION_ID, notice);
}
}
diff --git a/identiconizer/src/main/java/com/germainz/identiconizer/xposed/XposedMod.java b/identiconizer/src/main/java/com/germainz/identiconizer/xposed/XposedMod.java
deleted file mode 100644
index 811b3f1..0000000
--- a/identiconizer/src/main/java/com/germainz/identiconizer/xposed/XposedMod.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2013-2014 GermainZ@xda-developers.com
- * Portions Copyright (C) 2013 The ChameleonOS Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.germainz.identiconizer.xposed;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.XModuleResources;
-import android.database.sqlite.SQLiteDatabase;
-import android.provider.ContactsContract;
-import android.support.v4.app.NotificationCompat;
-import android.text.TextUtils;
-
-import com.germainz.identiconizer.Config;
-import com.germainz.identiconizer.R;
-import com.germainz.identiconizer.identicons.Identicon;
-import com.germainz.identiconizer.identicons.IdenticonFactory;
-
-import de.robv.android.xposed.IXposedHookInitPackageResources;
-import de.robv.android.xposed.IXposedHookLoadPackage;
-import de.robv.android.xposed.IXposedHookZygoteInit;
-import de.robv.android.xposed.XC_MethodHook;
-import de.robv.android.xposed.XC_MethodReplacement;
-import de.robv.android.xposed.callbacks.XC_InitPackageResources;
-import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
-
-import static de.robv.android.xposed.XposedHelpers.callMethod;
-import static de.robv.android.xposed.XposedHelpers.callStaticMethod;
-import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
-import static de.robv.android.xposed.XposedHelpers.findClass;
-import static de.robv.android.xposed.XposedHelpers.getStaticObjectField;
-
-public class XposedMod implements IXposedHookLoadPackage, IXposedHookInitPackageResources, IXposedHookZygoteInit {
- private static final Config CONFIG = new Config();
- private static String MODULE_PATH;
- private static int NOTIF_ICON_RES_ID;
-
- @Override
- public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
- if ("com.android.providers.contacts".equals(lpparam.packageName)) {
- try {
- findAndHookMethod("com.android.providers.contacts.DataRowHandlerForStructuredName",
- lpparam.classLoader, "insert", SQLiteDatabase.class,
- "com.android.providers.contacts.TransactionContext",
- long.class, ContentValues.class, new XC_MethodHook() {
-
- @Override
- protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
- CONFIG.reload();
- if (CONFIG.isEnabled()) {
- ContentValues values = (ContentValues) param.args[3];
- String name = values.getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
-
- if (!TextUtils.isEmpty(name)) {
- long rawContactId = ((Number) param.args[2]).longValue();
- SQLiteDatabase db = (SQLiteDatabase) param.args[0];
- Identicon identicon = IdenticonFactory.makeIdenticon(CONFIG.getIdenticonStyle(),
- CONFIG.getIdenticonSize(), CONFIG.getIdenticonBgColor(),
- CONFIG.isIdenticonSerif(), CONFIG.getIdenticonLength());
-
- ContentValues identiconValues = new ContentValues();
- identiconValues.put("mimetype_id", 10);
- identiconValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
- identiconValues.put(ContactsContract.CommonDataKinds.Photo.PHOTO,
- identicon.generateIdenticonByteArray(name));
- identiconValues.put(ContactsContract.Data.IS_PRIMARY, 1);
- identiconValues.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
-
- db.insert(ContactsContract.Contacts.Data.CONTENT_DIRECTORY, null, identiconValues);
- }
- }
- }
- }
- );
- } catch (Throwable e) {
- Context systemContext = (Context) getStaticObjectField(findClass("android.app.ActivityThread", null), "mSystemContext");
- if (systemContext == null) {
- Object activityThread = callStaticMethod(findClass("android.app.ActivityThread", null), "currentActivityThread");
- systemContext = (Context) callMethod(activityThread, "getSystemContext");
- }
-
- Context contactsProviderContext = systemContext.createPackageContext("com.android.providers.contacts", Context.CONTEXT_IGNORE_SECURITY);
- Context identiconizerContext = systemContext.createPackageContext(Config.PACKAGE_NAME, Context.CONTEXT_IGNORE_SECURITY);
-
- String contentText = identiconizerContext.getString(R.string.xposed_error_text);
- Notification notice = new NotificationCompat.Builder(contactsProviderContext)
- .setSmallIcon(NOTIF_ICON_RES_ID)
- .setContentTitle(identiconizerContext.getString(R.string.xposed_error_title))
- .setContentText(contentText)
- .setStyle(new NotificationCompat.BigTextStyle().bigText(contentText))
- .build();
- NotificationManager nm = (NotificationManager) contactsProviderContext.getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(1, notice);
- }
- }
-
- if (Config.PACKAGE_NAME.equals(lpparam.packageName)) {
- findAndHookMethod(Config.PACKAGE_NAME + ".Config", lpparam.classLoader,
- "isXposedModActive", XC_MethodReplacement.returnConstant(true));
- }
- }
-
- @Override
- public void handleInitPackageResources(XC_InitPackageResources.InitPackageResourcesParam resparam) throws Throwable {
- if (!resparam.packageName.equals("com.android.providers.contacts"))
- return;
- Resources res = XModuleResources.createInstance(MODULE_PATH, resparam.res);
- NOTIF_ICON_RES_ID = resparam.res.addResource(res, R.drawable.ic_settings_identicons);
- }
-
- @Override
- public void initZygote(StartupParam startupParam) throws Throwable {
- MODULE_PATH = startupParam.modulePath;
- }
-}
diff --git a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_contemporary.png b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_contemporary.png
index d8a3dab..ca74198 100644
Binary files a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_contemporary.png and b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_contemporary.png differ
diff --git a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_dotmatrix.png b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_dotmatrix.png
index b176dd4..b9e8890 100644
Binary files a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_dotmatrix.png and b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_dotmatrix.png differ
diff --git a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_spirograph.png b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_spirograph.png
index 5c596f0..0673c26 100644
Binary files a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_spirograph.png and b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_spirograph.png differ
diff --git a/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_unicornify.png b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_unicornify.png
new file mode 100644
index 0000000..aa4aaed
Binary files /dev/null and b/identiconizer/src/main/res/drawable-hdpi/ic_identicons_style_unicornify.png differ
diff --git a/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_contemporary.png b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_contemporary.png
index 199f02e..03fa5ef 100644
Binary files a/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_contemporary.png and b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_contemporary.png differ
diff --git a/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_spirograph.png b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_spirograph.png
index 1bc862d..752c7a9 100644
Binary files a/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_spirograph.png and b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_spirograph.png differ
diff --git a/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_unicornify.png b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_unicornify.png
new file mode 100644
index 0000000..aa4aaed
Binary files /dev/null and b/identiconizer/src/main/res/drawable-mdpi/ic_identicons_style_unicornify.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_contemporary.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_contemporary.png
new file mode 100644
index 0000000..03fa5ef
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_contemporary.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_dotmatrix.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_dotmatrix.png
new file mode 100644
index 0000000..259355c
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_dotmatrix.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_gmail.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_gmail.png
new file mode 100644
index 0000000..0d55d0a
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_gmail.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_retro.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_retro.png
new file mode 100644
index 0000000..ef98b5a
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_retro.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_spirograph.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_spirograph.png
new file mode 100644
index 0000000..752c7a9
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_spirograph.png differ
diff --git a/identiconizer/src/main/res/drawable-v16/ic_identicons_style_unicornify.png b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_unicornify.png
new file mode 100644
index 0000000..aa4aaed
Binary files /dev/null and b/identiconizer/src/main/res/drawable-v16/ic_identicons_style_unicornify.png differ
diff --git a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_dotmatrix.png b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_dotmatrix.png
index b04a2fe..b591554 100644
Binary files a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_dotmatrix.png and b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_dotmatrix.png differ
diff --git a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_retro.png b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_retro.png
index ab8db55..d47a8d7 100644
Binary files a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_retro.png and b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_retro.png differ
diff --git a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_spirograph.png b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_spirograph.png
index 5c0e5d1..662fcff 100644
Binary files a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_spirograph.png and b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_spirograph.png differ
diff --git a/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_unicornify.png b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_unicornify.png
new file mode 100644
index 0000000..aa4aaed
Binary files /dev/null and b/identiconizer/src/main/res/drawable-xhdpi/ic_identicons_style_unicornify.png differ
diff --git a/identiconizer/src/main/res/drawable-xhdpi/ic_settings_identicons.png b/identiconizer/src/main/res/drawable-xhdpi/ic_settings_identicons.png
index 1ac2611..2548889 100644
Binary files a/identiconizer/src/main/res/drawable-xhdpi/ic_settings_identicons.png and b/identiconizer/src/main/res/drawable-xhdpi/ic_settings_identicons.png differ
diff --git a/identiconizer/src/main/res/drawable/ic_identicons_style_unicornify.xml b/identiconizer/src/main/res/drawable/ic_identicons_style_unicornify.xml
new file mode 100644
index 0000000..d3f0c7a
--- /dev/null
+++ b/identiconizer/src/main/res/drawable/ic_identicons_style_unicornify.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/identiconizer/src/main/res/drawable/ic_identicons_style_visiglyphs.xml b/identiconizer/src/main/res/drawable/ic_identicons_style_visiglyphs.xml
new file mode 100644
index 0000000..3263cf2
--- /dev/null
+++ b/identiconizer/src/main/res/drawable/ic_identicons_style_visiglyphs.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/identiconizer/src/main/res/drawable/identicon_preview_background.xml b/identiconizer/src/main/res/drawable/identicon_preview_background.xml
new file mode 100644
index 0000000..e55d0a0
--- /dev/null
+++ b/identiconizer/src/main/res/drawable/identicon_preview_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/identiconizer/src/main/res/drawable/unicorn_preview_fallback.png b/identiconizer/src/main/res/drawable/unicorn_preview_fallback.png
new file mode 100644
index 0000000..2eb65f2
Binary files /dev/null and b/identiconizer/src/main/res/drawable/unicorn_preview_fallback.png differ
diff --git a/identiconizer/src/main/res/layout/activity_contacts_list.xml b/identiconizer/src/main/res/layout/activity_contacts_list.xml
index da47924..d758842 100644
--- a/identiconizer/src/main/res/layout/activity_contacts_list.xml
+++ b/identiconizer/src/main/res/layout/activity_contacts_list.xml
@@ -1,5 +1,5 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/identiconizer/src/main/res/mipmap-hdpi/ic_launcher.png b/identiconizer/src/main/res/mipmap-hdpi/ic_launcher.png
index 92815ed..a172689 100644
Binary files a/identiconizer/src/main/res/mipmap-hdpi/ic_launcher.png and b/identiconizer/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/identiconizer/src/main/res/mipmap-xhdpi/ic_launcher.png b/identiconizer/src/main/res/mipmap-xhdpi/ic_launcher.png
index ed39ed4..c798e62 100644
Binary files a/identiconizer/src/main/res/mipmap-xhdpi/ic_launcher.png and b/identiconizer/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/identiconizer/src/main/res/mipmap-xxhdpi/ic_launcher.png b/identiconizer/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 1513d2a..78f5ab2 100644
Binary files a/identiconizer/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/identiconizer/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/identiconizer/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/identiconizer/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 9720255..f3c5b16 100644
Binary files a/identiconizer/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/identiconizer/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/identiconizer/src/main/res/values/arrays.xml b/identiconizer/src/main/res/values/arrays.xml
index 46edfc3..9515ff6 100644
--- a/identiconizer/src/main/res/values/arrays.xml
+++ b/identiconizer/src/main/res/values/arrays.xml
@@ -23,6 +23,8 @@
- @string/identicons_style_spirograph
- @string/identicons_style_dotmatrix
- @string/identicons_style_gmail
+ - @string/identicons_style_unicornify
+ - @string/identicons_style_visiglyphs
@@ -31,6 +33,8 @@
- 2
- 3
- 4
+ - 5
+ - 6
@@ -39,6 +43,12 @@
- @drawable/ic_identicons_style_spirograph
- @drawable/ic_identicons_style_dotmatrix
- @drawable/ic_identicons_style_gmail
+ - @drawable/ic_identicons_style_unicornify
+ - @drawable/ic_identicons_style_visiglyphs
+
+ - 0
+
+
diff --git a/identiconizer/src/main/res/values/strings.xml b/identiconizer/src/main/res/values/strings.xml
index 8e9b502..e16c897 100644
--- a/identiconizer/src/main/res/values/strings.xml
+++ b/identiconizer/src/main/res/values/strings.xml
@@ -67,6 +67,10 @@
Spirograph
Dot matrix
Gmail
+ Unicornify
+ Visiglyphs
+ Styles
+ Select one or more identicon styles (random selection)
Saturation
Value
diff --git a/identiconizer/src/main/res/xml/identicons_prefs.xml b/identiconizer/src/main/res/xml/identicons_prefs.xml
index ec7cb44..b9edec5 100644
--- a/identiconizer/src/main/res/xml/identicons_prefs.xml
+++ b/identiconizer/src/main/res/xml/identicons_prefs.xml
@@ -28,13 +28,13 @@
-
+ android:defaultValue="@array/identicons_style_default_multi"/>
-
-
-