1616import time
1717import uhashlib
1818import ubinascii
19+ import _thread
20+
21+ # Mode constants
22+ MODE_LAUNCHING = 0
23+ MODE_UNINSTALL = 1
1924
2025
2126class Launcher (Activity ):
@@ -24,6 +29,11 @@ def __init__(self):
2429# Cache of the last app list + a quick hash of the icons
2530self ._last_app_list = None # list of tuples (name, path, icon_hash)
2631self ._last_ui_built = False # was UI built at least once?
32+ self ._current_mode = MODE_LAUNCHING # current launcher mode
33+ self ._mode_button = None # reference to mode toggle button
34+ self ._mode_button_icon = None # reference to mode button icon
35+ self ._mode_button_label = None # reference to mode button label
36+ self ._app_widgets = []# list to store app widget references
2737
2838def onCreate (self ):
2939print ("launcher.py onCreate()" )
@@ -34,8 +44,98 @@ def onCreate(self):
3444main_screen .set_style_pad_hor (mpos .ui .pct_of_display_width (2 ),0 )
3545main_screen .set_style_pad_ver (mpos .ui .topmenu .NOTIFICATION_BAR_HEIGHT ,0 )
3646main_screen .set_flex_flow (lv .FLEX_FLOW .ROW_WRAP )
47+
3748self .setContentView (main_screen )
3849
50+ # ------------------------------------------------------------------
51+ def _create_mode_button (self ,screen ,icon_size ,label_height ,iconcont_width ,iconcont_height ,focusgroup ):
52+ """Create the mode toggle button as an app-like icon in the grid"""
53+ import os
54+
55+ # ----- container (same as regular apps) -------------------------
56+ mode_cont = lv .obj (screen )
57+ mode_cont .set_size (iconcont_width ,iconcont_height )
58+ mode_cont .set_style_border_width (0 ,lv .PART .MAIN )
59+ mode_cont .set_style_pad_all (0 ,0 )
60+ mode_cont .set_style_bg_opa (lv .OPA .TRANSP ,0 )
61+ mode_cont .set_scrollbar_mode (lv .SCROLLBAR_MODE .OFF )
62+
63+ # ----- icon -----------------------------------------------------
64+ mode_icon = lv .image (mode_cont )
65+ mode_icon .align (lv .ALIGN .TOP_MID ,0 ,0 )
66+ mode_icon .set_size (icon_size ,icon_size )
67+
68+ # Load appropriate icon based on mode
69+ if self ._current_mode == MODE_LAUNCHING :
70+ # Show trashcan icon (enter uninstall mode)
71+ try :
72+ app_dir = os .path .dirname (os .path .dirname (__file__ ))
73+ icon_path = app_dir + "/res/mipmap-mdpi/trashcan_icon.png"
74+ icon_dsc = self .create_icon_dsc (icon_path )
75+ mode_icon .set_src (icon_dsc )
76+ except Exception as e :
77+ print (f"Failed to load trashcan icon:{ e } " )
78+ mode_icon .set_src (lv .SYMBOL .TRASH )
79+ label_text = "Uninstall Apps"
80+ else :
81+ # Show exit icon (exit uninstall mode)
82+ try :
83+ app_dir = os .path .dirname (os .path .dirname (__file__ ))
84+ icon_path = app_dir + "/res/mipmap-mdpi/exit_icon.png"
85+ icon_dsc = self .create_icon_dsc (icon_path )
86+ mode_icon .set_src (icon_dsc )
87+ except Exception as e :
88+ print (f"Failed to load exit icon:{ e } " )
89+ mode_icon .set_src (lv .SYMBOL .CLOSE )
90+ label_text = "Exit Uninstall"
91+
92+ # ----- label (same as regular apps) -----------------------------
93+ mode_label = lv .label (mode_cont )
94+ mode_label .set_text (label_text )
95+ mode_label .set_long_mode (lv .label .LONG_MODE .WRAP )
96+ mode_label .set_width (iconcont_width )
97+ mode_label .align (lv .ALIGN .BOTTOM_MID ,0 ,0 )
98+ mode_label .set_style_text_align (lv .TEXT_ALIGN .CENTER ,0 )
99+
100+ # ----- events ---------------------------------------------------
101+ mode_cont .add_event_cb (
102+ lambda e :self ._toggle_mode (),
103+ lv .EVENT .CLICKED ,None )
104+ mode_cont .add_event_cb (
105+ lambda e ,cont = mode_cont :self .focus_app_cont (cont ),
106+ lv .EVENT .FOCUSED ,None )
107+ mode_cont .add_event_cb (
108+ lambda e ,cont = mode_cont :self .defocus_app_cont (cont ),
109+ lv .EVENT .DEFOCUSED ,None )
110+
111+ if focusgroup :
112+ focusgroup .add_obj (mode_cont )
113+
114+ # Store references
115+ self ._mode_button = mode_cont
116+ self ._mode_button_icon = mode_icon
117+ self ._mode_button_label = mode_label
118+
119+ # ------------------------------------------------------------------
120+ def _toggle_mode (self ):
121+ """Toggle between launching and uninstall modes"""
122+ if self ._current_mode == MODE_LAUNCHING :
123+ self ._current_mode = MODE_UNINSTALL
124+ else :
125+ self ._current_mode = MODE_LAUNCHING
126+
127+ # Force UI rebuild to update mode button and app overlays
128+ # self._last_ui_built = False
129+ # # Trigger onResume to rebuild
130+ # screen = self.getContentView()
131+ # Force UI rebuild to update mode button and app overlays
132+ self ._last_app_list = None # Invalidate cache
133+ # Trigger onResume to rebuild with the active screen
134+ screen = lv .screen_active ()
135+ if screen :
136+ self .onResume (screen )
137+
138+
39139# ------------------------------------------------------------------
40140# Helper: compute a cheap hash of a file (or return None if missing)
41141@staticmethod
@@ -83,6 +183,9 @@ def onResume(self, screen):
83183# 3. UI needs (re)building – clear screen and create widgets
84184screen .clean ()
85185
186+ # Clear app widgets list
187+ self ._app_widgets = []
188+
86189focusgroup = lv .group_get_default ()
87190if not focusgroup :
88191print ("WARNING: could not get default focusgroup" )
@@ -129,9 +232,48 @@ def onResume(self, screen):
129232label .align (lv .ALIGN .BOTTOM_MID ,0 ,0 )
130233label .set_style_text_align (lv .TEXT_ALIGN .CENTER ,0 )
131234
235+ # Store widget info
236+ widget_info = {
237+ 'app' :app ,
238+ 'container' :app_cont ,
239+ 'image' :image ,
240+ 'label' :label ,
241+ 'overlay' :None ,
242+ 'x_label' :None
243+ }
244+ self ._app_widgets .append (widget_info )
245+
246+ # ----- Add overlay if in uninstall mode --------------------
247+ if self ._current_mode == MODE_UNINSTALL :
248+ is_builtin = PackageManager .is_builtin_app (app .fullname )
249+
250+ # Create overlay
251+ overlay = lv .obj (app_cont )
252+ overlay .set_size (icon_size ,icon_size )
253+ overlay .align (lv .ALIGN .TOP_MID ,0 ,0 )
254+ overlay .set_style_radius (8 ,0 )
255+ overlay .set_style_border_width (0 ,0 )
256+ widget_info ['overlay' ]= overlay
257+
258+ if is_builtin :
259+ # Grey out builtin apps
260+ overlay .set_style_bg_color (lv .color_hex (0x808080 ),0 )
261+ overlay .set_style_bg_opa (lv .OPA ._60 ,0 )
262+ else :
263+ # Red X for non-builtin apps
264+ overlay .set_style_bg_color (lv .color_hex (0xE74C3C ),0 )
265+ overlay .set_style_bg_opa (lv .OPA ._80 ,0 )
266+ # Draw X
267+ x_label = lv .label (overlay )
268+ x_label .set_text (lv .SYMBOL .CLOSE )
269+ x_label .set_style_text_color (lv .color_hex (0xFFFFFF ),0 )
270+ x_label .set_style_text_font (lv .font_montserrat_32 ,0 )
271+ x_label .center ()
272+ widget_info ['x_label' ]= x_label
273+
132274# ----- events --------------------------------------------------
133275app_cont .add_event_cb (
134- lambda e ,fullname = app . fullname : mpos . apps . start_app ( fullname ),
276+ lambda e ,a = app : self . _handle_app_click ( a ),
135277lv .EVENT .CLICKED ,None )
136278app_cont .add_event_cb (
137279lambda e ,cont = app_cont :self .focus_app_cont (cont ),
@@ -143,6 +285,10 @@ def onResume(self, screen):
143285if focusgroup :
144286focusgroup .add_obj (app_cont )
145287
288+ # ------------------------------------------------------------------
289+ # Add mode toggle button as last item in grid
290+ self ._create_mode_button (screen ,icon_size ,label_height ,iconcont_width ,iconcont_height ,focusgroup )
291+
146292# ------------------------------------------------------------------
147293# 4. Store the new representation for the next resume
148294self ._last_app_list = current_apps
@@ -170,3 +316,150 @@ def focus_app_cont(self, app_cont):
170316
171317def defocus_app_cont (self ,app_cont ):
172318app_cont .set_style_border_width (0 ,lv .PART .MAIN )
319+
320+ # ------------------------------------------------------------------
321+ def _handle_app_click (self ,app ):
322+ """Handle app icon click based on current mode"""
323+ if self ._current_mode == MODE_LAUNCHING :
324+ # Normal launch
325+ mpos .apps .start_app (app .fullname )
326+ elif self ._current_mode == MODE_UNINSTALL :
327+ # Check if builtin
328+ is_builtin = PackageManager .is_builtin_app (app .fullname )
329+ if is_builtin :
330+ self ._show_builtin_info_modal (app )
331+ else :
332+ self ._show_uninstall_confirmation_modal (app )
333+
334+ # ------------------------------------------------------------------
335+ def _show_uninstall_confirmation_modal (self ,app ):
336+ """Show confirmation modal for uninstalling an app"""
337+ # Create modal background on layer_top to ensure it's above everything
338+ try :
339+ parent = lv .layer_top ()
340+ except :
341+ parent = lv .screen_active ()
342+
343+ modal_bg = lv .obj (parent )
344+ modal_bg .set_size (lv .pct (100 ),lv .pct (100 ))
345+ modal_bg .set_style_bg_color (lv .color_hex (0x000000 ),0 )
346+ modal_bg .set_style_bg_opa (lv .OPA ._50 ,0 )
347+ modal_bg .set_style_border_width (0 ,0 )
348+ modal_bg .set_style_radius (0 ,0 )
349+ modal_bg .set_pos (0 ,0 )
350+ modal_bg .remove_flag (lv .obj .FLAG .SCROLLABLE )
351+
352+ # Create modal dialog
353+ modal = lv .obj (modal_bg )
354+ modal .set_size (lv .pct (90 ),lv .pct (90 ))
355+ modal .center ()
356+ modal .set_style_pad_all (20 ,0 )
357+ modal .set_flex_flow (lv .FLEX_FLOW .COLUMN )
358+ modal .set_flex_align (lv .FLEX_ALIGN .CENTER ,lv .FLEX_ALIGN .CENTER ,lv .FLEX_ALIGN .CENTER )
359+
360+ # Title
361+ title = lv .label (modal )
362+ title .set_text ("Uninstall App?" )
363+ title .set_style_text_font (lv .font_montserrat_20 ,0 )
364+
365+ # Message
366+ msg = lv .label (modal )
367+ msg .set_text (f"Are you sure you want to uninstall{ app .name } ?" )
368+ msg .set_style_text_align (lv .TEXT_ALIGN .CENTER ,0 )
369+ msg .set_width (lv .pct (90 ))
370+
371+ # Button container
372+ btn_cont = lv .obj (modal )
373+ btn_cont .set_size (lv .pct (100 ),lv .SIZE_CONTENT )
374+ btn_cont .set_style_border_width (0 ,0 )
375+ btn_cont .set_style_pad_all (10 ,0 )
376+ btn_cont .set_flex_flow (lv .FLEX_FLOW .ROW )
377+ btn_cont .set_flex_align (lv .FLEX_ALIGN .SPACE_EVENLY ,lv .FLEX_ALIGN .CENTER ,lv .FLEX_ALIGN .CENTER )
378+
379+ # Yes button
380+ yes_btn = lv .button (btn_cont )
381+ yes_btn .set_size (lv .pct (40 ),50 )
382+ yes_btn .add_event_cb (lambda e ,a = app ,m = modal_bg :self ._confirm_uninstall (a ,m ),lv .EVENT .CLICKED ,None )
383+ yes_label = lv .label (yes_btn )
384+ yes_label .set_text ("Yes" )
385+ yes_label .center ()
386+
387+ # No button
388+ no_btn = lv .button (btn_cont )
389+ no_btn .set_size (lv .pct (40 ),50 )
390+ no_btn .add_event_cb (lambda e ,m = modal_bg :self ._close_modal (m ),lv .EVENT .CLICKED ,None )
391+ no_label = lv .label (no_btn )
392+ no_label .set_text ("No" )
393+ no_label .center ()
394+
395+ # ------------------------------------------------------------------
396+ def _show_builtin_info_modal (self ,app ):
397+ """Show info modal explaining builtin apps cannot be uninstalled"""
398+ # Create modal background on layer_top to ensure it's above everything
399+ try :
400+ parent = lv .layer_top ()
401+ except :
402+ parent = lv .screen_active ()
403+
404+ modal_bg = lv .obj (parent )
405+ modal_bg .set_size (lv .pct (100 ),lv .pct (100 ))
406+ modal_bg .set_style_bg_color (lv .color_hex (0x000000 ),0 )
407+ modal_bg .set_style_bg_opa (lv .OPA ._50 ,0 )
408+ modal_bg .set_style_border_width (0 ,0 )
409+ modal_bg .set_style_radius (0 ,0 )
410+ modal_bg .set_pos (0 ,0 )
411+ modal_bg .remove_flag (lv .obj .FLAG .SCROLLABLE )
412+
413+ # Create modal dialog
414+ modal = lv .obj (modal_bg )
415+ modal .set_size (lv .pct (90 ),lv .pct (90 ))
416+ modal .center ()
417+ modal .set_style_pad_all (20 ,0 )
418+ modal .set_flex_flow (lv .FLEX_FLOW .COLUMN )
419+ modal .set_flex_align (lv .FLEX_ALIGN .CENTER ,lv .FLEX_ALIGN .CENTER ,lv .FLEX_ALIGN .CENTER )
420+
421+ # Title
422+ title = lv .label (modal )
423+ title .set_text ("Cannot Uninstall" )
424+ title .set_style_text_font (lv .font_montserrat_20 ,0 )
425+
426+ # Message
427+ msg = lv .label (modal )
428+ msg .set_text (f"{ app .name } is a built-in app\n and cannot be uninstalled." )
429+ msg .set_style_text_align (lv .TEXT_ALIGN .CENTER ,0 )
430+ msg .set_width (lv .pct (90 ))
431+
432+ # OK button
433+ ok_btn = lv .button (modal )
434+ ok_btn .set_size (lv .pct (50 ),50 )
435+ ok_btn .add_event_cb (lambda e ,m = modal_bg :self ._close_modal (m ),lv .EVENT .CLICKED ,None )
436+ ok_label = lv .label (ok_btn )
437+ ok_label .set_text ("OK" )
438+ ok_label .center ()
439+
440+ # ------------------------------------------------------------------
441+ def _close_modal (self ,modal_bg ):
442+ """Close and delete modal"""
443+ modal_bg .delete ()
444+
445+ # ------------------------------------------------------------------
446+ def _confirm_uninstall (self ,app ,modal_bg ):
447+ """Actually uninstall the app"""
448+ self ._close_modal (modal_bg )
449+ # Run uninstall in thread to avoid blocking UI
450+ try :
451+ _thread .stack_size (mpos .apps .good_stack_size ())
452+ _thread .start_new_thread (self ._uninstall_app_thread , (app .fullname ,))
453+ except Exception as e :
454+ print (f"Could not start uninstall thread:{ e } " )
455+
456+ # ------------------------------------------------------------------
457+ def _uninstall_app_thread (self ,app_fullname ):
458+ """Thread function to uninstall app"""
459+ print (f"Uninstalling app:{ app_fullname } " )
460+ try :
461+ PackageManager .uninstall_app (app_fullname )
462+ print (f"Successfully uninstalled{ app_fullname } " )
463+ # Note: The app list will be refreshed when launcher resumes
464+ except Exception as e :
465+ print (f"Error uninstalling{ app_fullname } :{ e } " )