22import os
33from glob import glob as path_find
44import math
5+ from timeit import default_timer
56
67from materialyoucolor .scheme .scheme_tonal_spot import SchemeTonalSpot
78from materialyoucolor .scheme .scheme_expressive import SchemeExpressive
1718from materialyoucolor .palettes .tonal_palette import TonalPalette
1819from materialyoucolor .scheme .variant import Variant
1920from materialyoucolor .utils .color_utils import argb_from_rgba_01 , srgb_to_argb
21+ from materialyoucolor .utils .math_utils import sanitize_degrees_double
2022from materialyoucolor .hct import Hct
2123from materialyoucolor .quantize import QuantizeCelebi
2224from materialyoucolor .score .score import Score
2325from materialyoucolor .dynamiccolor .material_dynamic_colors import MaterialDynamicColors
2426
27+ autoclass = None
28+
2529try :
2630 from jnius import autoclass
2731 from android import mActivity
32+
33+ Integer = autoclass ("java.lang.Integer" )
34+ BuildVERSION = autoclass ("android.os.Build$VERSION" )
35+ context = mActivity .getApplicationContext ()
36+ WallpaperManager = autoclass ("android.app.WallpaperManager" ).getInstance (mActivity )
37+ except Exception :
38+ pass
39+
40+ try :
2841 from PIL import Image
2942except Exception :
30- autoclass = None
43+ Image = None
3144
3245SCHEMES = {
3346 "TONAL_SPOT" : SchemeTonalSpot ,
4962 "neutral_palette" : "system_neutral1_{}" ,
5063 "neutral_variant_palette" : "system_neutral2_{}" ,
5164}
52-
65+ APPROX_TONE = 200
66+ APPROX_CHROMA = 50
5367DEFAULT_RESIZE_BITMAP_AREA = 112 * 112
5468
5569
5670def _is_android () -> bool :
5771 try :
5872 from android import mActivity
73+
5974 return True
6075 except Exception as e :
61- print ("Platform does not seems to be android" )
6276 pass
6377 return False
6478
@@ -67,11 +81,12 @@ def save_and_resize_bitmap(drawable, path):
6781 CompressFormat = autoclass ("android.graphics.Bitmap$CompressFormat" )
6882 FileOutputStream = autoclass ("java.io.FileOutputStream" )
6983 Bitmap = autoclass ("android.graphics.Bitmap" )
84+ BitmapConfig = autoclass ("android.graphics.Bitmap$Config" )
7085 Canvas = autoclass ("android.graphics.Canvas" )
7186 bitmap = Bitmap .createBitmap (
7287 drawable .getIntrinsicWidth (),
7388 drawable .getIntrinsicHeight (),
74- Bitmap . Config .ARGB_8888 ,
89+ BitmapConfig .ARGB_8888 ,
7590 )
7691 canvas = Canvas (bitmap )
7792 drawable .setBounds (0 , 0 , canvas .getWidth (), canvas .getHeight ())
@@ -94,101 +109,122 @@ def save_and_resize_bitmap(drawable, path):
94109 100 ,
95110 FileOutputStream (path ),
96111 )
97- return bitmap .getWidth (), bitmap .getHeight
112+ return bitmap .getWidth (), bitmap .getHeight ()
98113
99114
100115def reverse_color_from_primary (color , scheme ):
116+ # TODO: Find solution
117+ # Here we are using APPROX_TONE and APPROX_CHROMA
118+ # because information is lost.
119+ # Which likely will affect these colors:
120+ # primaryContainer, tertiaryContainer
121+ temp_hct = Hct .from_int (color )
101122 reversed_color = color
102- # if scheme == "TONAL_SPOT":
103- # temp_hct = Hct.from_int(color)
104-
123+ if scheme in ["TONAL_SPOT" , "SPRITZ" , "VIBRANT" , "RAINBOW" , "CHROMA" ]:
124+ reversed_color = Hct .from_hct (temp_hct .hue , APPROX_CHROMA , APPROX_TONE ).to_int ()
125+ elif scheme == "EXPRESSIVE" :
126+ reversed_color = Hct .from_hct (
127+ sanitize_degrees_double (temp_hct .hue - 240.0 ), APPROX_CHROMA , APPROX_TONE
128+ ).to_int ()
129+ elif scheme == "FRUIT_SALAD" :
130+ reversed_color = Hct .from_hct (
131+ sanitize_degrees_double (temp_hct .hue + 50.0 ), APPROX_CHROMA , APPROX_TONE
132+ ).to_int ()
133+ elif scheme in ["FIDELITY" , "CONTENT" ]:
134+ # We have chroma info same as source here!
135+ reversed_color = Hct .from_hct (
136+ temp_hct .hue , temp_hct .chroma , APPROX_TONE
137+ ).to_int ()
105138 return reversed_color
106139
107140
108- def get_scheme (
109- wallpaper_path = None ,
110- fallback_scheme = "TONAL_SPOT" ,
141+ def _get_android_12_above (
142+ logger , selected_scheme = "TONAL_SPOT" , contrast = 0.0 , dark_mode = False
143+ ) -> DynamicScheme :
144+ SettingsSecure = autoclass ("android.provider.Settings$Secure" )
145+ theme_settings = json .loads (
146+ SettingsSecure .getString (
147+ context .getContentResolver (),
148+ SettingsSecure .THEME_CUSTOMIZATION_OVERLAY_PACKAGES ,
149+ )
150+ )
151+ # Android 14 has this method
152+ try :
153+ contrast = mActivity .getSystemService (context .UI_MODE_SERVICE ).getContrast ()
154+ logger ("Got contrast '{}'" .format (contrast ))
155+ except Exception :
156+ pass
157+
158+ # See if system supports mutiple schemes
159+ if OPTION_THEME_STYLE in theme_settings .keys ():
160+ selected_scheme = theme_settings [OPTION_THEME_STYLE ]
161+ logger ("Got system theme style '{}'" .format (selected_scheme ))
162+
163+ # Get system colors
164+ get_system_color = lambda color_name : srgb_to_argb (
165+ context .getColor (
166+ context .getResources ().getIdentifier (
167+ COLOR_NAMES [color_name ].format (APPROX_TONE ),
168+ "color" ,
169+ "android" ,
170+ )
171+ )
172+ )
173+ color_names = COLOR_NAMES .copy ()
174+ for color_name in COLOR_NAMES .keys ():
175+ hct = Hct .from_int (get_system_color (color_name ))
176+ color_names [color_name ] = TonalPalette .from_hue_and_chroma (hct .hue , hct .chroma )
177+
178+ return DynamicScheme (
179+ DynamicSchemeOptions (
180+ reverse_color_from_primary (
181+ get_system_color ("primary_palette" ),
182+ selected_scheme ,
183+ ),
184+ getattr (Variant , selected_scheme ),
185+ contrast ,
186+ dark_mode ,
187+ ** color_names ,
188+ )
189+ )
190+
191+
192+ def open_wallpaper_file (file_path ) -> Image :
193+ try :
194+ return Image .open (file_path )
195+ except Exception :
196+ return None
197+
198+
199+ def get_dynamic_scheme (
200+ # Scheme options
111201 dark_mode = True ,
112202 contrast = 0.0 ,
113203 dynamic_color_quality = 10 ,
204+ # Fallbacks
205+ fallback_wallpaper_path = None ,
206+ fallback_scheme_name = "TONAL_SPOT" ,
207+ force_fallback_wallpaper = False ,
208+ # Logging
114209 message_logger = print ,
115- fallback_color = 0xFF0000FF ,
116210 logger_head = "MaterialYouColor" ,
117- ):
211+ ) -> DynamicScheme :
212+ logger = lambda message : message_logger (logger_head + " : " + message )
213+
118214 is_android = _is_android ()
215+ selected_scheme = None
119216 selected_color = None
120- logger = lambda message : message_logger (logger_head + " : " + message )
121- dynamic_scheme = None
122217
123218 if is_android :
124- Integer = autoclass ("java.lang.Integer" )
125- BuildVERSION = autoclass ("android.os.Build$VERSION" )
126- context = mActivity .getApplicationContext ()
127- WallpaperManager = autoclass ("android.app.WallpaperManager" ).getInstance (
128- mActivity
129- )
130219 # For Android 12 and 12+
131220 if BuildVERSION .SDK_INT >= 31 :
132- logger ("Device supports MaterialYou" )
133- SettingsSecure = autoclass ("android.provider.Settings$Secure" )
134- theme_settings = json .loads (
135- SettingsSecure .getString (
136- context .getContentResolver (),
137- SettingsSecure .THEME_CUSTOMIZATION_OVERLAY_PACKAGES ,
138- )
139- )
140- # Android 14 has this method
141- try :
142- contrast = mActivity .getSystemService (
143- context .UI_MODE_SERVICE
144- ).getContrast ()
145- logger ("Got contrast '{}'" .format (contrast ))
146- except Exception :
147- pass
148-
149- if OPTION_THEME_STYLE in theme_settings .keys ():
150- fallback_scheme = theme_settings [OPTION_THEME_STYLE ]
151- logger ("Got system theme style '{}'" .format (fallback_scheme ))
152-
153- color_names = COLOR_NAMES .copy ()
154- for color_name in list (COLOR_NAMES .keys ())[::- 1 ]:
155- hct = Hct .from_int (
156- srgb_to_argb (
157- context .getColor (
158- context .getResources ().getIdentifier (
159- color_names [color_name ].format (100 ),
160- "color" ,
161- "android" ,
162- )
163- )
164- )
165- )
166- color_names [color_name ] = TonalPalette .from_hue_and_chroma (
167- hct .hue , hct .chroma
168- )
169-
170- dynamic_scheme = DynamicScheme (
171- DynamicSchemeOptions (
172- # TODO: Get accurate source color
173- srgb_to_argb (
174- context .getColor (
175- context .getResources ().getIdentifier (
176- COLOR_NAMES ["primary_palette" ].format (100 ),
177- "color" ,
178- "android" ,
179- )
180- )
181- ),
182- getattr (Variant , fallback_scheme ),
183- contrast ,
184- dark_mode ,
185- ** color_names
186- )
221+ selected_scheme = _get_android_12_above (
222+ logger , selected_scheme , contrast , dark_mode
187223 )
188224
189225 # For Android 8.1 and 8.1+
190226 elif BuildVERSION .SDK_INT >= 27 :
191- logger ("Device does not supports MaterialYou" )
227+ logger ("Device doesn't supports MaterialYou" )
192228 selected_color = argb_from_rgba_01 (
193229 WallpaperManager .getWallpaperColors (WallpaperManager .FLAG_SYSTEM )
194230 .getPrimaryColor ()
@@ -197,57 +233,67 @@ def get_scheme(
197233 logger ("Got top color from wallpaper '{}'" .format (selected_color ))
198234
199235 # Lower than 8.1
200- else :
236+ elif not force_fallback_wallpaper :
201237 logger (
202- "Device does neither supports materialyoucolor"
203- " nor provides pregenerated colors"
238+ "Device does neither supports materialyoucolor "
239+ "nor provides pregenerated colors"
204240 )
205241 wallpaper_store_dir = context .getFilesDir ().getAbsolutePath ()
206242 wallpaper_file = ".wallpaper-{}.png" .format (
207243 WallpaperManager .getWallpaperId (WallpaperManager .FLAG_SYSTEM )
208244 )
209- wallpaper_path = os .path .join (wallpaper_store_dir , wallpaper_file )
245+ fallback_wallpaper_path = os .path .join (wallpaper_store_dir , wallpaper_file )
210246
211- if not os .path .isfile (wallpaper_path ):
247+ if not os .path .isfile (fallback_wallpaper_path ):
212248 previous_files = path_find (
213249 os .path .join (wallpaper_store_dir , ".wallpaper-*.png" )
214250 )
215251 [os .remove (file ) for file in previous_files ]
216252 try :
253+ # Requires `android.permission.READ_EXTERNAL_STORAGE` permission
217254 wallpaper_drawable = WallpaperManager .getDrawable ()
218255 width , height = save_and_resize_bitmap (
219- wallpaper_drawable , wallpaper_path
256+ wallpaper_drawable , fallback_wallpaper_path
220257 )
221258 logger (
222- "Got the system wallpaper with size: '{}x{}'" .format (
223- width , height
224- )
259+ "Resized the system wallpaper : '{}x{}'" .format (width , height )
225260 )
226261 except Exception as e :
227262 logger ("Failed to get system wallpaper : " + str (e ))
228- wallpaper_path = None
263+ fallback_wallpaper_path = None
229264
230- if not selected_color and wallpaper_path :
231- image = Image .open (wallpaper_path )
265+ if (
266+ not selected_color
267+ and fallback_wallpaper_path
268+ and (image := open_wallpaper_file (fallback_wallpaper_path ))
269+ ):
270+ timer_start = default_timer ()
232271 pixel_len = image .width * image .height
233272 image_data = image .getdata ()
234273 # TODO: Think about getting data from bitmap
235274 pixel_array = [
236275 image_data [_ ]
237276 for _ in range (0 , pixel_len , dynamic_color_quality if not is_android else 1 )
238277 ]
278+ logger (
279+ f"Created an array of pixels from a "
280+ f"system wallpaper file - { default_timer () - timer_start } sec."
281+ )
282+ timer_start = default_timer ()
239283 colors = QuantizeCelebi (pixel_array , 128 )
240284 selected_color = Score .score (colors )[0 ]
241- elif not selected_color :
242- logger ("Using defined color '{}'" .format (fallback_color ))
243- selected_color = fallback_color
285+ logger (f"Got dominant colors - " f"{ default_timer () - timer_start } sec." )
244286
245287 return (
246- SCHEMES [fallback_scheme ](
247- Hct .from_int (selected_color ),
248- dark_mode ,
249- contrast ,
288+ (
289+ SCHEMES [fallback_scheme_name ](
290+ Hct .from_int (selected_color ),
291+ dark_mode ,
292+ contrast ,
293+ )
294+ if selected_color
295+ else None
250296 )
251- if not dynamic_scheme
252- else dynamic_scheme
297+ if not selected_scheme
298+ else selected_scheme
253299 )
0 commit comments