/*
=====================================================================================
[PPUS 1.6.2] This file is a replacement for the python-for-android (p4a) base 
file of the same name.  It has been modified to address quality issues.


USAGE
=====

Copy this file in its entirety into the following buildozer build-tree 
location, replacing its original version there:

  ./.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/
             usbsync/src/main/java/org/kivy/android/PythonService.java

For app builds, this has to be swapped in just once, after which the saved 
.buildozer/ zip will retain the mod permanently.  This may be also be useful
if source is ever run on Android (unlikely, given its permissions and rules).


CHANGES
=======

A number of related changes that address quality issues were made in both 
1.6.2 and 1.6.1 and in both this file and others:

[1.6.2] Zombie services
  In the Python service code of run_script_as_service.py, call stopSelf() of 
  the class here before exiting, to ensure proper service teardown.  This 
  avoids zombie services and their zombie notifications caused sporadically by
  an exit() in the C code that runs the Python service code.  See ZOMBIES FIX.

[1.6.2] Post-timeout crashes
  In this file, catch uncaught exceptions during service start-up and signal 
  them to Python's main.py with a broadcast message that invokes a popup.
  This avoids app crashes and ANRs if service starts fail after a 6-hour timeout.

[1.6.1] Notifications delays
  In this file, add code to opt-out of the Android 12+ 10-second delay for 
  notification displays.  Because this delay strikes most users as a glitch, 
  notifications are now made to appear immediately on service start.

[1.6.1] 6-hour timeouts
  In this file, add a method to catch Android 6-hour timeouts for data-sync 
  apps.  In the new method, tear down the service properly, and signal Python 
  code in main.py to issue an info popup.  This avoids default silent kills.

Additional 1.6.2 mods here and in main.py that attempted zombie workarounds 
(e.g., exception handlers here and the main.py reaper described ahead) became
moot and were disabled but retained for reference and possible future use, 
after finding the glitch's true cause described in the next section.


ZOMBIES FIX
===========

TL:DR: 1.6.2 fixes sporadic zombie services and notifications by adding a 
stopSelf() call in the Python service code, because an exit() call in its C
wrapper makes a stopSelf() here unreachable and does not by itself tear down 
services reliably on all phones today.  Unrelated code handles errors post 
timeouts to avoid app crashes on new starts.
 
THE GLITCH

Some forensics is required to understand this fix.  p4a's service support for
this app spans this PythonService.java file, a generated ServiceRunscript.java
subclass of it, a start.c wrapper, and settings in a generated AndroidManifest.xml.

At runtime, p4a runs this app's action's foreground service in a new process, 
which spawns a thread to run the service's Python code so that code here remains
active.  The process is implied by syntax in the generated AndroidMainfest.xml that
originates in buildozer.spec, and the thread is spawned in onStartCommand() here.

This kicks off a three-language chain in the service process: Java code runs C code 
that runs Python code.  Specifically, the Java thread's run() in this p4a file 
invokes the C nativeStart() in p4a's start.c using Java's JNI, which in turn launches
the Python code of the service using Python's embedding tool PyRun_SimpleFile().

The "smoking gun" of the zombie-services glitch lies in the middle of this chain: 
the C nativeStart() calls the C stdlib's exit() after the Python code has finished,
and before it returns to Java.  (It runs exit() directly in newer versions and by 
Python sys.exit() proxy in older.)  This exit() operating-system call bluntly kills 
the service's process - which means that the stopService() call after nativeStart() 
in the Java run() here is never reached if a service's Python code finishes normally.

In the past, exit() was relied on to tear down the service properly.  Today, that
assumption is no longer valid: some phones no longer tear down services reliably on
exit() and occasionally leave behind zombie services and their notifications that 
can cause new service starts to fail.  The service lingers in Developer options'
Running Services and the notification remains stuck-on in the status bar.

The scope of this glitch is unclear.  It has been seen to happen sporadically on 
recent Samsung phones, but it's unknown if it reflects a change in Samsung's One UI
or is part of the common Android base that One UI wraps.  Even if it's just Samsung, 
though, it's widespread enough to be critical.

THE FIX

To work around this, Android's stopSelf() is now invoked directly from the Python
service code in run_script_as_service.py via pyjnius - before this code exits and 
reaches the exit() call in the C wrapper.  This ensures that service teardown 
is run properly.  It suffices to avoid zombie services and their notifications,
and allows any number of actions to be started without errors on all phones.

Calling stopSelf() from Python relies on the fact that the thread's run() here 
saves the Java class instance 'this' in the static 'mService' attribute, and 
pyjnius's autoclass() preserves it.  Otherwise, there would be no way for Python 
code to run methods of the service-class instance running in the service process. 

In the end, this a p4a service-support glitch: the C exit() should not be run 
without stopSelf().  Ideally, this would be fixed in p4a's start.c.  Minimally, 
the need to run stopSelf() in Python service code should be documented by p4a,
and clearly so.  As is, this was stumbled onto only because an added exception 
handler in this file for service exits produced no logcat output on forced throws,
and only after days of following multiple rabbit holes that proved to be dead ends.

THE DEFUNCT REAPER

Most promising among those rabbit holes, a failsafe in main.py run on action-exit
broadcast message receipt terminated zombie services forcibly and externally with 
Android's context.stopService(intent) via pyjnius.

This sufficed to remove the zombie and its notification, but could not do so while
the app was paused in the background: its kills were delayed to avoid race condition
risks and that meant that they would not take effect until the Kivy event loop 
resumed when the app was moved to the foreground.  

And really, this workaround worked on;y because the service was never shut down
properly with stopSelf() in the first place; it simply obscured the glitchy state
created by the C exit() call - the true root cause.  The reaper was disabled
but saved in case the zombies rise again.

TIMEOUTS CONFLATION

Failed service starts after a 6-hour timeout proved to be a disjoint issue that 
is not addressed by the zombies fix.  There is no zombie service or notification
in this case, and those failures arise because the Android timer is not being 
reset by app interaction as advertised - and perhaps only in testing contexts.  

To avoid crashes on the failed starts after timeouts, though, separate code added 
here catches start-up exceptions and sends a broadcast message to main.py Python 
code to post a popup suggesting phone restart.


ETCETERA
========

See also main.py's service_in_progress_state() for this story's other half, and 
run_script_as_service.py's signal_script_exit() for "Normal" broadcast exits.
Testing excs here: throw new RuntimeException("Forced fail on Java service stop");
(but Java flags an error if any code after this! - comment-out or wrap in an 'if').

Some PPUS mods here might work in ServiceRunScript.java in dists' com/ instead,
but that file is generated from one of 5 Service.tmpl.java templates.  Ideally, 
some of these patches should also be generalized and added to p4a's own version
of this class, but waiting for this to happen is not an option, and upgrading 
the entire stack is a non-starter (too many subtle workarounds have been applied).

Perspective: this required substantial Java mods by a Python programmer - despite
the Kivy stack's underlying goal of insulating Python programmers from Android's 
Java bias apart from limited tactical tasks run in Python via pyjnius.  Readers will 
have to judge whether this proves that all-Python Android apps is a pipe dream.
=====================================================================================
*/


package org.kivy.android;

import android.os.Build;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import android.app.Service;
import android.os.IBinder;
import android.os.Bundle;
import android.content.Intent;
import android.content.Context;
import android.util.Log;
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Process;
import java.io.File;

//imports for channel definition
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.graphics.Color;

// [PPUS 1.6.1] no longer used
// like py "from", own folder's files implicit
// import android.content.pm.ServiceInfo;            // for failed attempts #3 and #9 ahead
// import com.quixotely.usbsync.ServiceRunscript;    // subclass generated from Service.tmpl.java


public class PythonService extends Service implements Runnable {

    // Thread for Python code
    private Thread pythonThread = null;

    // Python environment variables
    private String androidPrivate;
    private String androidArgument;
    private String pythonName;
    private String pythonHome;
    private String pythonPath;
    private String serviceEntrypoint;
    // Argument to pass to Python code,
    private String pythonServiceArgument;


    public static PythonService mService = null;
    private Intent startIntent = null;

    private boolean autoRestartService = false;

    public void setAutoRestartService(boolean restart) {
        autoRestartService = restart;
    }

    public int startType() {
        // default, possibly overriden in generated subclass via buildozer.spec [PPUS docs]
        return START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }


    @Override
    public void onCreate() {
        super.onCreate();
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
 
        // [PPUS 1.6.2] Wrap entire startup in a 'try' to catch any
        // exceptions in this file's startup code and signal them to the 
        // app's GUI process so it can reset its state and inform the user.  

        // This may be moot if notifications are no longer stuck on due to 
        // other mods here, but it will apply if users try actions sans phone 
        // restarts after 6-hour timeouts.  Exceptions in the subthread are  
        // caught separately via Java tools.  TBD: should startup exceptions 
        // be rethrown for Android cleanup? - moot if require phone restart.

        // Now seen: uncaught timout timer-expired exceptions are caught
        // here if a new action is started after a timeout was caught and 
        // the phone is not restarted (in artificial testing, at least).
        // Oddly, exc delivered to onStartCommand(), not while thread runs.

        trace("onStartCommand enter");
        try {
            // the original p4a bit
            return _onStartCommand(intent, flags, startId);
        } 
        catch (Exception e) {
            // on any normal exception: consume, signal Python app code
            String javaexc = e.getMessage();                       // javaexc: String or null
            trace("onStartCommand error: " + javaexc);             // null => "null"
            signalGUIProcess("StartFail", javaexc);                // Py: null => None, 'None'
            return startType();                                    // return is moot if exc?
        }
    }


    protected int _onStartCommand(Intent intent, int flags, int startId) {

        if (pythonThread != null) {
            Log.v("python service", "service exists, do not start again");    // multiple starts?
            return startType();
        }

	//intent is null if OS restarts a STICKY service
        if (intent == null) {
            Context context = getApplicationContext();
            intent = getThisDefaultIntent(context, "");
        }

        startIntent = intent;
        Bundle extras = intent.getExtras();
        androidPrivate = extras.getString("androidPrivate");
        androidArgument = extras.getString("androidArgument");
        serviceEntrypoint = extras.getString("serviceEntrypoint");
        pythonName = extras.getString("pythonName");
        pythonHome = extras.getString("pythonHome");
        pythonPath = extras.getString("pythonPath");
        boolean serviceStartAsForeground = (
            extras.getString("serviceStartAsForeground").equals("true")
        );
        pythonServiceArgument = extras.getString("pythonServiceArgument");

        pythonThread = new Thread(this);

        // [PPUS 1.6.2] Catch uncaught excs in subthread run() and stop 
        // service, to avoid zombie services after excs in python thread.
        // NB: This has not been seen to be useful in practice so far.

        // Java lambda parameter types inferred from interface/sole-method.
        // Must run android's default handler too, to avoid zombie states?
        // Alt: a try/catch/finally in run() may have a similar effect and 
        // be simpler ("throws Exception" nonsense vs chained calls here).

        Thread.UncaughtExceptionHandler oldHandler;
        oldHandler = pythonThread.getUncaughtExceptionHandler();

        pythonThread.setUncaughtExceptionHandler((thread, exception) -> {
            // java anonymous interface 'function'

            String javaexc = exception.getMessage();
            trace("Uncaught exc in service thread: " + javaexc);
 
            this.signalGUIProcess("RunFail", javaexc);    // GUI reset and popup  
            this.killNotificationAndShutdown();           // end notif and service
        
            if (oldHandler != null) {
                oldHandler.uncaughtException(thread, exception);
            }
            else {
                // Is this manual chaining really needed? oldHandler is 
                // null only if thread died before get(), else it's thread's
                // ThreadGroup which should forward on to defaultHandler auto,

                Thread.UncaughtExceptionHandler defaultHandler;
                defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
                if (defaultHandler != null) {
                    defaultHandler.uncaughtException(thread, exception);
                }
            }
        });
         
        pythonThread.start();              // triggers this.run() ahead [PPUS docs]

        if (serviceStartAsForeground) {
            doStartForeground(extras);     // escalate, and handle notification [PPUS docs]
        }

        trace("onStartCommand exit");
        return startType();                // sticky iff generated subclass overrides [PPUS docs]
    }


    protected int getServiceId() {
        return 1;    // also defined in generated subclass in Python GUI process [PPUS docs]
    }

    protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
        return null;
    }


    protected void doStartForeground(Bundle extras) {
        String serviceTitle = extras.getString("serviceTitle");
        String smallIconName = extras.getString("smallIconName");
        String contentTitle = extras.getString("contentTitle");
        String contentText = extras.getString("contentText");

        Notification notification;
        Context context = getApplicationContext();
        Intent contextIntent = new Intent(context, PythonActivity.class);
        PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
            PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

	// Unspecified icon uses default.
	int smallIconId = context.getApplicationInfo().icon;
	if (!smallIconName.equals("")){
	    int resId = getResources().getIdentifier(smallIconName, "mipmap",
						     getPackageName());
	    if (resId ==0) {
		resId = getResources().getIdentifier(smallIconName, "drawable",
						     getPackageName());
	    }
	    if (resId !=0) {
		smallIconId = resId;
	    }
	}
	    
	if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
	    // This constructor is deprecated
            notification = new Notification(
                smallIconId, serviceTitle, System.currentTimeMillis());
            try {
                // prevent using NotificationCompat, this saves 100kb on apk
                Method func = notification.getClass().getMethod(
                    "setLatestEventInfo", Context.class, CharSequence.class,
                    CharSequence.class, PendingIntent.class);
                func.invoke(notification, context, contentTitle, contentText, pIntent);
            } catch (NoSuchMethodException | IllegalAccessException |
                     IllegalArgumentException | InvocationTargetException e) {
            }

        } else {
            // for android 8+ we need to create our own channel
            // https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1
            String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a" + getServiceId();
            String channelName = "Background Service" + getServiceId();
            NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, 
                NotificationManager.IMPORTANCE_NONE);
            
            chan.setLightColor(Color.BLUE);
            chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
            NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            manager.createNotificationChannel(chan);

            Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
            builder.setContentTitle(contentTitle);
            builder.setContentText(contentText);
            builder.setContentIntent(pIntent);
            builder.setSmallIcon(smallIconId);

            // [PPUS 1.6.1] Opt out of the Android 12+ 10-second delay for staus-bar
            // notifications (which seems more bug than feature to most observers). 
            // Host-version bifurcation alert: S == api level 31 == Android 12.
            
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE);
            }

            notification = builder.build();
        }
        startForeground(getServiceId(), notification);
        trace("doStartForeground exit");
    }


    @Override
    public void onDestroy() {

        // [PPUS 1.6.2] NB: called on internal stopSelf() or external stopService()
        // (usually!) but may also be called in other contexts (out-of-memory kills?)
        trace("onDestroy");

        super.onDestroy();
        pythonThread = null;
        if (autoRestartService && startIntent != null) {
            Log.v("python service", "service restart requested");
            startService(startIntent);
        }
        killNotification();                       // [PPUS 1.6.2] added - pid kill may not suffice
        Process.killProcess(Process.myPid());     // kills service process, not app [PPUS docs]
    }


    /*
     * Stops the task gracefully when killed.
     * Calling stopSelf() will trigger a onDestroy() call from the system.
     */

    @Override
    public void onTaskRemoved(Intent rootIntent) {    

        // [PPUS 1.6.2] NB: called on user Recent swipes, etc.
        trace("onTaskRemoved");

        super.onTaskRemoved(rootIntent);
        //sticky service runtime/restart is managed by the OS. leave it running when app is closed
        if (startType() != START_STICKY) {
            killNotificationAndShutdown();        // [PPUS 1.6.2] was stopSelf()
        }
    }


    @Override
    public void run() {

        // [PPUS docs] What the pythonThread Java thread does on start().
        // Why a thread? - the service is also a new process via manifest,
        // but it must converse with Android's API during Python-code run.
        // A try/catch/finally here might == setUncaughtExceptionHandler.

        // After Python code finishes, an exit() in nativeStart() makes the
        // stopSelf() (now in killNotificationAndShutdown()) unreachable - 
        // and occasionally leaves behind a zombie service and notification 
        // behind on some phones.  To fix, stopSelf() now run in Python code. 

        trace("Service-thread run() enter");
        String app_root = getFilesDir().getAbsolutePath() + "/app";
        File app_root_file = new File(app_root);

        PythonUtil.loadLibraries(app_root_file,
            new File(getApplicationInfo().nativeLibraryDir));
        this.mService = this;

        nativeStart(                           // JNI C code that runs Python service code [PPUS]
            androidPrivate, androidArgument,   // Python code sends "Normal" broadcast to main.py
            serviceEntrypoint, pythonName,     // Python code now runs service.stopSelf() [1.6.2]
            pythonHome, pythonPath,
            pythonServiceArgument);

        killNotificationAndShutdown();         // <= NEVER REACHED! normal exit, was stopSelf()
        trace("Service-thread run() exit");    // not run either (clue...) [PPUS]
     }


    // Native part, in a C lib: runs Python service code file
    public static native void nativeStart(
            String androidPrivate, String androidArgument,
            String serviceEntrypoint, String pythonName,
            String pythonHome, String pythonPath,
            String pythonServiceArgument);


    /*
    =====================================================================================
    [PPUS 1.6.2] On Samsung devices, try to ensure that notifications are removed 
    on service exit.  This is a workaround to a possible sporadic timing glitch in 
    Samsung One UI, and coded defensively (random glithces are, well, random),
    and errors on optional calls are skipped; only stopSelf() errors invoke GUI popups.

    UPDATE: some of this code became moot after the real start.c exit() cause of 
    zombies was uncovered, but it's retained for reference and possible future use.
    =====================================================================================
    */


    protected void signalGUIProcess(String cause) {    // emulate py's optional args (sigh)
        signalGUIProcess(cause, null);
    }


    protected void signalGUIProcess(String cause, String javaexc) {

        // [PPUS 1.6.2] Now used by all service exits in this file: signal Python to reset 
        // GUI, show popup, release wakelock, and stop its broadcast receiver.  "Normal" 
        // exits send the same broadcast from file run_script_as_service.py via pyjnius.

        // Python: c in (1,2,3), Java: (c==1||c==2||c=3), Java 9+: List.of(1,2,3).contains(c), 
        // but List.of(1,2,3).contains(c) was not added to Android until Android 11 (api 30);
        // alas, Java seems to suffer the same chronic thrashing as Python... (and Android...);

        trace("signalGUIProcess");

        // causes sent here; cause "Normal" sent sans exc by run_script_as_service.py
        assert (cause == "Timeout"  || cause == "StartFail" || 
                cause == "StopFail" || cause == "RunFail");

        // hardcode broadcast name: statics differ in new process made for service
        timeoutBroadcastName = "com.quixotely.usbsync.SCRIPT_EXIT";

        Intent intent = new Intent(timeoutBroadcastName);
        intent.putExtra("WhyStopped", cause);                // key, value
        intent.putExtra("JavaExc", javaexc);                 // java String|null => py str|None

        sendBroadcast(intent);                               // a Context method: reset gui, popup
    }


    protected void killNotification() {

        /* UNUSED
       
        // UPDATE: disabled after the tue exit() cause of zombies in start.c
        // was found; moot but retained only for reference and future use.

        // [PPUS 1.6.2] Try to work around Samsung One UI stuck-on notifications.
        // These are optional calls: fails show logcat message but not GUI popup.

        // May also be run auto via stopSelf() => onDestroy()... usually. 
        // Per the web: stopForeground() and notificationManager.cancel()
        // (and stopSelf(), not called here) can safely be called more than 
        // once (later re-calls are no-ops); and stopForeground() must be 
        // called first, else the call to notificationManager.cancel() is 
        // ignored because the foreground service is active.

        // TBD: also suggested - kill PendingIntents, and the following coding alt:
        // NotificationManager notificationManager = 
        ///(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // notificationManager.cancel(NOTIFICATION_ID);

        // Now seen: this doesn't seem to have any effect - zombie notification
        // and service still left open.  The zombie-service reaper in main.py 
        // closes both, and is now also used _after_ actions to clean up asap.
        // Later: reaper was disabled after found real zombies cause - exit().

        trace("killNotification");

        try {
            // moves svc from FG to BG, closes its notificaton?
            trace("stopForeground");
            stopForeground(STOP_FOREGROUND_REMOVE);
        }
        catch (Exception e) {
            // on any normal exception: consume, logcat but no popup
            String javaexc = e.getMessage();
            trace("Shutdown stopForeground error: " + javaexc);
        }

        try {
            // force notification to close?
            trace("NotificationManager");
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.cancel(getServiceId());    // null pointer possible?
        }
        catch (Exception e) {
            // on any normal exception: consume, logcat but no popup
            String javaexc = e.getMessage();
            trace("Shutdown NotificationManager error: " + javaexc);
        }

        UNUSED */
    }


    protected void killNotificationAndShutdown() {     // optional arg via overload (also sigh)
        killNotificationAndShutdown(0); 
    }


    protected void killNotificationAndShutdown(int startId) {

        // [PPUS 1.6.2] Work around Samsung One UI stuck-on notifications?
        // Now seen: catching excs here doesn't help, like killNotification()

        // Caveat: exceptions caught here may be never seen as GUI popups, 
        // because this is not run on "Normal" exits in .py, stopSelf() excs  
        // are very unlikely, and if > one exc signal is sent to main.py here
        // it stops its receiver after the _first_ (timeout+this, run()+this).
        // That makes the catch here largely pointless.  Other contexts may
        // reach this method sans exc signals but are iffy: onTaskRemoved()
        // calls this too, though the whole app is then being swiped away,
        // and the call in run() after nativeStart is never reached.  Punt? 

        trace("killNotificationAndShutdown");

        // try optional calls, logcats on excs
        killNotification();    // now a no-op

        try {
            // stopSelf() normally suffices to also close service's notification
            // for 6-hour timeouts, must do this within a "few" seconds to avoid crash
            // see also stop() of generated subclass of this: runs stopService(context)
            trace("stopSelf");

            if (startId == 0) {
                stopSelf();
            }
            else {
                stopSelf(startId);    // id matters iff multiple starts
            }
        } 
        catch (Exception e) {
            // on any normal exception: consume, signal Py app code for popup
            String javaexc = e.getMessage();
            trace("Shutdown stopSelf error: " + javaexc);
            signalGUIProcess("StopFail", javaexc);
        }
    }


    /*
    =====================================================================================
    [PPUS 1.6.1] Catch Android15+ 6-hour data-sync foreground-service timeouts.
    See main.py's service_in_progress_state() in the land of Python for more info.
    =====================================================================================
    */


    private static final String PPUSTAG = "PPUS-Java";         // statics required: no instance
    private static final boolean RELEASE = false;              // true=no tracing here in release
    private static String timeoutBroadcastName = "unset";      // init else null


    public static void trace(String message) {
    
        /*
        ---------------------------------------------------------------------------
        Emulate main.py's tracing system.

        Code: could use BuildConfig.DEBUG to test and Java's 
        horrifically verbose System.out.println("") to print.
        Must be static to be callable from other statics too.

        NB: many calls to this method in this file do _not_ run 
        for normal exits that send a "Normal" broadcast message
        in run_script_as_service.py, and many run only for error 
        cases (e.g., 6-hour timeouts and excs). 
        ---------------------------------------------------------------------------
        */

        if (!RELEASE) {
            Log.d(PPUSTAG, message);
        }
    }


    public static void registerTimeoutBroadcastName(String name) {

        /* UNUSED
        
        ---------------------------------------------------------------------------
        Originally called by Python but no longer used

        This works via pyjnius if statics (there is no instance), but the class-
        level tBN attribute has been reset to "unset" later in onTimeout.  This 
        is most likely because, per the app's manifest, Android runs this class 
        in a new process created for the service that differ's from the app's 
        process and reloads the class.  Hence, changes to statics in the app have 
        no effect in the service.
 
        Could save to an XML file via SharedPreferences, but that seems overkill:
        hardcode PPUS's broadcast name ahead for Intent - and abandon generality.
        Could also pass in an environment variable like p4a does for service args,
        or add to the generated Service* subclass like p4a does for other data, 
        but this app has just one service (generalize me for broader p4a support).

        See also run_script_as_service.py for more on the service process model.
        ---------------------------------------------------------------------------
        

        timeoutBroadcastName = name;                                      // FAIL
        trace("registerTimeoutBroadcastName: " + timeoutBroadcastName);   // ok but reset in svc

        UNUSED */
    }


    @Override                                               // @Override is optional
    public void onTimeout(int startId, int fgsType) {       // java 'this' is optional--usually

        /* 
        ---------------------------------------------------------------------------
        Called by Android on a 6-hour data-sync timeout per rolling 24 hours.

        The timeout occurs only on Android 15+ for apps targeting 15 or later.
        Per docs, moving the app from background to foreground resets the timer.  
        That makes timeouts unlikely, given that users would naturally check
        action status or results, but two timeout crashes have surfaced on Play.

        Also per docs, the app must shutdown the service via this method within
        a "few" (undocumented) seconds or it is killed.  Shut down the service 
        here, and trigger an info_message popup in the GUI by sending a broadcast 
        message that the app is registered to receive.  This same message is used
        for normal action exits, distinguished per its Intent's extra data.

        ----
        Why is a phone restart required?

        This ran into an issue where the app's state was glitchy even after 
        catching the timeout, stopping the BroadcastReceiver, and trying to kill 
        the service with all the options below.  The service would sometimes live
        on in Developer options/Running services, and one new action in the app 
        would work but yield an "app keeps stopping" popup, and a second action 
        would hang while waiting for an action-exit message that never appeared.

        In this state, the service process seems unstoppable or unstartable.  It's
        possible that Android is stuck in a timeout loop, despite app foregrounding 
        that should reset the timer; or is invalidly trying to restart the timed-out
        service on later action runs instead of beginning a new one, even though 
        it is not marked as 'sticky.'  

        Logcat messages were mixed: one suggested the restarts theory, but others
        for double FATAL crashes strongly implied that the service couldn't start 
        anew because the data-sync time limit was exhausted - despite moving 
        the app from/to foreground before the run (do timer resets even work?).  
        Text: "Caused by android.app.ForegroundServiceStartNotAllowedException: 
        Time limited already exhausted for foreground service type dataSync".

        The cause for this state is undocumented and might derive from either 
        broadcast messages, services, or notifications and may originate in p4a's 
        or Android's source code - but this is too deep a rabbit hole for an issue 
        that has impacted just 0.02% of the app's likely 10k active user base to 
        date (and just 0.003% of its 60k total installers so far).

        By comparison, a 2026 user of a 2018 phone with limited memory recently 
        generated three crashes in Python 3.9.9's bytecode optimizer, likely due
        to internal storage corruption; this app can only do so much.

        So: rather than crashing on timeouts, we now at least issue an informative
        popup that explains action resumes and tells users to restart their phone 
        to make app state usable again, and forcibly kill the app to reduce the 
        chances of user reruns before phone restarts.  This isn't as good as full
        app recovery, but it's an improvement over timeout crashes.

        ----
        New info

        The same glitchy 6-hour timeout restart behavior has now been seen on a 
        Google Pixel 9 after flashing it with Android 15.  Hence, it's not a 
        Samsung One UI glitch, but lies in either Android or the Kivy/p4a stack.
 
        A possible Samsung glitch that causes notifications to remain stuck on after 
        prior service exit may also cause service starts to fail and was addressed 
        in 1.6.2, but it seems unrelated: notifications go away on 6-hour timeouts.

        Later: a p4a exit() in C caused general service/notification zombies and was
        fixed in 1.6.2, but it was unrelated to start fails after 6-hour timeouts.
        The latter occur because the timer is not being reset by app taps in testing.
        See top of file for more on notifications and general service-start fails.

        ----
        Perspective 

        These timeouts seem a poison pill for sync apps, but they are also wildly
        unlikely to impact this on-demand app in normal usage, due to both timer
        resets and practicality.  They're not worth the effort, and such annual 
        breakages in Android make supporting it tedious in general.

        An alternative suggested by Android docs, user-initiated data transfer 
        (UIDT) jobs, works on Android 14+ only, may be limited to network transfers
        only, and requires too much Java infrastructure code; FG services have 
        support in p4a for Python programmers and should just work.  Android also 
        has options for NFC and IR but nothing general for long tasks; how ad-hoc!

        https://
            developer.android.com/develop/background-work/background-tasks/uidt

        https://
            android-developers.googleblog.com/2024/09/
            google-maps-improved-download-reliability-user-initiated-data-transfer-api.html

        ----
        Update: 1.6.2 now goes out of its way to catch exceptions; see top of file.
        As one consequence, it now catches and reports startup errors when new runs
        fail after a 6-hour timeout.  Post-timeout runs may still fail, but the new 
        catches prevent app crashes, if users do not restart phones and it matters.

        NB: new run failures after timeouts were seen only in an artificial testing
        context that uses ADB commands to shorten the timeout radically.  Real 6-hour
        timeouts may respect timer resets on app foregrounding, but this is impractical 
        to test given the 6-hour delays.  No timer resets would be an Android bug.
        ---------------------------------------------------------------------------
        */


        trace("Enter onTimeout: " + timeoutBroadcastName);

        // an empty no-op in android source code (despite AI)
        // super.onTimeout(startId, fgsType);

        // signal Py app to reset GUI, show popup, release wakelock, and stop BR
        signalGUIProcess("Timeout");             // [PPUS 1.6.2] no exc

        // stop fg service gracefully
        killNotificationAndShutdown(startId);    // [PPUS 1.6.2] was stopSelf(startId) in 1.6.1

        trace("Exit onTimeout");


        /*
        ---------------------------------------------------------------------------
        (The following is now largely moot - it was exit() in p4a's start.c!)
        Per android source code, the foreground service can be stopped by any of:
          
          - android.app.Service#stopSelf() - which seems most recommended
          - android.content.Context#stopService(android.content.Intent) or their overloads;
            this is what the generated Service class's stop() method does
          - android.app.Service#stopForeground(int) can be used as well, which demotes the
            service to a "background" service that will soon be stopped by the system

        But the following alternatives did not avoid zombie services and all left
        the app in an unusable state (the Android API is riddled with redundancy!):

        1) // most recommended: do this within a "few" seconds to avoid crash
           stopSelf(startId);
        
        2) // moves svc from FG  to BG, closes its notificaton
           stopForeground(STOP_FOREGROUND_REMOVE);

        3) // which calls Context#stopService(android.content.Intent)
           ((ServiceRunscript) this).stop(getApplicationContext());

        4) // kill service process, not app; runs Process.killProcess(Process.myPid())
           onDestroy();

        5) // nuclear option: should run in and kill service process
           System.exit(0);

        6) // manifest-insert add: but seems to be default for swipes today
           android:stopWithTask="true"

        7) // force removal of notification - and any impacts on service
           NotificationManager notificationManager = getSystemService(NotificationManager.class);
           notificationManager.cancel(getServiceId());

        8) // overkill combos: did this in onTimeout(), onTaskRemoved(), and run() 
           stopForeground(STOP_FOREGROUND_REMOVE);    # moot
           stopSelf();                                # also tried #6+#7+#8+#9 combo; argh

        9) // overkill start call in this file; per android docs: 
           // "If foreground service type is not specified in the call, 
           // the type defaults to the values defined in the manifest."
           startForeground(getServiceId(), notification, 
               ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
        ---------------------------------------------------------------------------
        */
    }
}
