Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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;

import org.qtproject.qt.android.bindings.QtService;

public class PythonService extends QtService implements Runnable {
private static final String TAG = "PythonQtService";

// 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 setEnvironmentVariable(String key, String value) {
/**
* Sets an environment variable based on key/value.
**/
try {
android.system.Os.setenv(key, value, true);
} catch (Exception e) {
Log.e(TAG, "Unable set environment variable:" + key + "=" + value);
e.printStackTrace();
}
}

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

public int startType() {
return START_NOT_STICKY;
}

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

@Override
public void onCreate() {
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (pythonThread != null) {
Log.v(TAG, "service exists, do not start again");
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);
pythonThread.start();

if (serviceStartAsForeground) {
doStartForeground(extras);
}

return startType();
}

protected int getServiceId() {
return 1;
}

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 != null) {
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);
notification = builder.build();
}
startForeground(getServiceId(), notification);
}

@Override
public void onDestroy() {
super.onDestroy();
pythonThread = null;
if (autoRestartService && startIntent != null) {
Log.v(TAG, "service restart requested");
startService(startIntent);
}
Process.killProcess(Process.myPid());
}

/**
* Stops the task gracefully when killed.
* Calling stopSelf() will trigger a onDestroy() call from the system.
*/
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
//sticky service runtime/restart is managed by the OS. leave it running when app is closed
if (startType() != START_STICKY) {
stopSelf();
}
}

@Override
public void run(){
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;

Log.v(TAG, "Setting env vars for start.c and Python to use");
setEnvironmentVariable("ANDROID_ENTRYPOINT", app_root + "/" + serviceEntrypoint);
setEnvironmentVariable("ANDROID_ARGUMENT", app_root);
setEnvironmentVariable("ANDROID_APP_PATH", app_root);
setEnvironmentVariable("ANDROID_PRIVATE", androidPrivate);
setEnvironmentVariable("ANDROID_UNPACK", app_root);
setEnvironmentVariable("PYTHONHOME", pythonHome);
setEnvironmentVariable("PYTHONPATH", pythonPath + ":" + app_root + ":" + app_root + "/lib");
setEnvironmentVariable("PYTHONOPTIMIZE", "2");

super.onCreate();

stopSelf();
}

// Native part
public static native void nativeStart(
String androidPrivate, String androidArgument,
String serviceEntrypoint, String pythonName,
String pythonHome, String pythonPath,
String pythonServiceArgument);
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@
{% endif %}
{% for name in service_names %}
<service android:name="{{ args.package }}.Service{{ name|capitalize }}"
android:process=":service_{{ name }}" />
android:process=":service_{{ name }}"
android:exported="true">
<meta-data android:name="android.app.lib_name"
android:value="main" />
</service>
{% endfor %}
{% for name in native_services %}
<service android:name="{{ name }}" />
Expand Down