package org.gscript.process;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
import org.gscript.R;
import org.gscript.data.ContentUri;
import org.gscript.data.HistoryProvider;
import org.gscript.data.LibraryProvider;
import org.gscript.data.library.ItemAttributes;
import org.gscript.interop.InteropReceiver;
import org.gscript.jni.NativeSupport;
import org.gscript.settings.ShellProfile;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
public class ProcessService extends Service implements ProcessStateListener {
static final String LOG_TAG = "ProcessService";
public static final String ACTION_START = "gscript.intent.action.START";
public static final String ACTION_EXECUTE = "gscript.intent.action.EXECUTE";
public static final String EXTRA_ATTRIBUTES = "attributes";
public static final String EXTRA_PROFILE = "profile";
ArrayList<ProcessTask> mTasks = new ArrayList<ProcessTask>();
ArrayList<ProcessRegistration> mProcessRegistrations = new ArrayList<ProcessRegistration>();
ArrayList<IServiceCallback> mServiceCallbacks = new ArrayList<IServiceCallback>();
AtomicInteger mCurrentPid = new AtomicInteger(1);
int mForeground = 0;
InteropReceiver mInteropReceiver;
boolean mServiceStarted;
@Override
public void onCreate() {
if (!mServiceStarted)
this.startService(new Intent(this, ProcessService.class));
super.onCreate();
}
@Override
public void onDestroy() {
super.onDestroy();
if(mInteropReceiver != null) {
mInteropReceiver.stop();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!mServiceStarted) {
if (!NativeSupport.prepare(this)) {
Log.e(LOG_TAG,
"failed to prepare native support... additional binaries might be missing");
}
mInteropReceiver = new InteropReceiver(this);
mInteropReceiver.start();
mServiceStarted = true;
}
/* check if this start command has something for us to do */
if (intent != null && ACTION_EXECUTE.equals(intent.getAction())
&& intent.getData() != null) {
/* start process */
Intent processIntent = new Intent(intent);
startProcess(processIntent, 0);
}
if(intent != null && ACTION_START.equals(intent.getAction()) && intent.hasExtra(EXTRA_PROFILE)) {
/* start process */
String profile = intent.getStringExtra(EXTRA_PROFILE);
String profileName = ShellProfile.getName(this, profile);
Intent processIntent = new Intent(intent);
processIntent.setData(Uri.parse(profileName));
startProcess(processIntent, 0);
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mServiceBinder;
}
/* Process service binder implementation */
IProcessService.Stub mServiceBinder = new IProcessService.Stub() {
@Override
public void registerProcessCallback(ProcessDescriptor pd,
IProcessCallback cb) throws RemoteException {
if (pd != null && cb != null) {
ProcessRegistration reg = new ProcessRegistration(cb, pd);
synchronized (mProcessRegistrations) {
mProcessRegistrations.add(reg);
}
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
ProcessDescriptor taskdescriptor = (ProcessDescriptor) task;
if (pd.equals(taskdescriptor)) {
/*
* task is already running send registration
* notification
*/
if (task.getState() != ProcessState.STATE_NONE) {
/* notify here if task is no longer running because else
* the callback will never receive any transcript etc */
cb.OnProcessRegistration(pd, task.getAttributes()
.get(ItemAttributes.ATTRIBUTE_SHELL), task
.getScreenBuffer());
cb.OnProcessStateChanged(pd, task.getState());
}
}
}
}
}
}
@Override
public void unregisterProcessCallback(ProcessDescriptor pd,
IProcessCallback cb) throws RemoteException {
synchronized (mProcessRegistrations) {
/* use an iterator to allow safe removal from the collection */
Iterator<ProcessRegistration> iter = mProcessRegistrations
.iterator();
while (iter.hasNext()) {
ProcessRegistration reg = iter.next();
if (reg.equalsCallback(cb)
&& (reg.equalsDescriptor(pd) || pd == null)) {
iter.remove();
}
}
}
}
@Override
public void registerServiceCallback(IServiceCallback cb)
throws RemoteException {
synchronized (mServiceCallbacks) {
mServiceCallbacks.add(cb);
}
}
@Override
public void unregisterServiceCallback(IServiceCallback cb)
throws RemoteException {
synchronized (mServiceCallbacks) {
mServiceCallbacks.remove(cb);
}
}
@Override
public void requestKillProcess(ProcessDescriptor pd)
throws RemoteException {
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
ProcessDescriptor taskdescriptor = (ProcessDescriptor) task;
if (pd.equals(taskdescriptor))
task.requestKill();
}
}
}
@Override
public void requestKillProcesses(IServiceCallback cb)
throws RemoteException {
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
task.requestKill();
}
}
}
@Override
public ProcessDescriptor[] getProcesses(IServiceCallback cb, int state)
throws RemoteException {
ArrayList<ProcessDescriptor> processes = new ArrayList<ProcessDescriptor>();
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
if (((task.getState() & state) != 0)
|| state == ProcessState.STATE_NONE) {
processes.add(new ProcessDescriptor(task));
}
}
}
ProcessDescriptor[] pds = new ProcessDescriptor[processes.size()];
processes.toArray(pds);
return pds;
}
@Override
public int getProcessState(ProcessDescriptor pd) throws RemoteException {
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
ProcessDescriptor taskdescriptor = (ProcessDescriptor) task;
if (taskdescriptor.equals(pd))
return task.getState();
}
}
return -1;
}
@Override
public void dispatchProcessOutput(ProcessDescriptor pd, byte[] output,
int offset, int length) throws RemoteException {
synchronized (mTasks) {
final ArrayList<ProcessTask> tasks = mTasks;
int cnt = tasks.size();
for (int i=0; i < cnt; ++i) {
final ProcessTask task = tasks.get(i);
ProcessDescriptor taskdescriptor = (ProcessDescriptor) task;
if (taskdescriptor.equals(pd))
task.sendOutput(output, offset, length);
}
}
}
@Override
public void dispatchProcessEvent(ProcessDescriptor pd, int event,
Bundle extras) throws RemoteException {
}
@Override
public void requestWindowSizeChange(ProcessDescriptor pd, int rows,
int cols, int width, int height) throws RemoteException {
synchronized (mTasks) {
for (ProcessTask task : mTasks) {
ProcessDescriptor taskdescriptor = (ProcessDescriptor) task;
if (taskdescriptor.equals(pd))
task.requestWindowSizeChange(rows, cols, width, height);
}
}
}
};
public void startProcess(Intent i, int flags) {
/* create a new process descriptor */
int newPid = mCurrentPid.getAndAdd(1);
ProcessDescriptor pd = new ProcessDescriptor(newPid, i);
/* start new task from descriptor */
ProcessTask task = new ProcessTask(ProcessService.this, pd,
ProcessService.this);
synchronized (mTasks) {
mTasks.add(task);
}
}
/* ProcessStateListener */
@Override
public void OnProcessStateChanged(ProcessTask task, int state) {
ProcessDescriptor pd = (ProcessDescriptor) task;
/* notify all matching ProcessCallbacks */
synchronized (mProcessRegistrations) {
Iterator<ProcessRegistration> iter = mProcessRegistrations
.iterator();
while (iter.hasNext()) {
ProcessRegistration reg = iter.next();
if (reg.equalsDescriptor(pd)) {
try {
if(state == ProcessState.STATE_RUNNING) {
/*
* notify callbacks who registered early while the process was in STATE_NONE
* and the task did not have the needed info to dispatch a complete
* OnProgressRegistration event
* */
reg.callback.OnProcessRegistration(pd, task.getAttributes()
.get(ItemAttributes.ATTRIBUTE_SHELL), task
.getScreenBuffer());
}
/* dispatch process state to callback */
reg.callback.OnProcessStateChanged(pd, state);
} catch (RemoteException e) {
/*
* callback could be gone without unregistering
* correctly... remove it
*/
Log.d(LOG_TAG, "Trying to access dead process callback");
iter.remove();
}
}
}
}
/* notify MonitorCallback(s). normally there should be only one (main) */
synchronized (mServiceCallbacks) {
Iterator<IServiceCallback> iter = mServiceCallbacks.iterator();
while (iter.hasNext()) {
IServiceCallback cb = iter.next();
try {
cb.OnProcessStateChanged(pd, state);
} catch (RemoteException e) {
/*
* callback could be gone without unregistering correctly...
* remove it
*/
Log.d(LOG_TAG, "Trying to access dead service callback");
iter.remove();
}
}
}
if (state == ProcessState.STATE_RUNNING) {
createNotificationForTask(task);
if (!task.getAttributes().containsKey(
ItemAttributes.ATTRIBUTE_UNATTENDED)) {
/* launch a process activity */
startActivity(pd.getActivityIntent(this));
}
}
if (!ProcessState.isActiveState(state)) {
removeNotificationForTask(task);
/* task is no longer running add to history */
/* TODO:
* instead of storing a transcript for history we can also store the actual screenbuffer bytes so
* that we will be able to add color to the history log */
ContentValues values = new ContentValues();
values.put(HistoryProvider.COLUMN_INTENT, pd.mIntent.toUri(0));
values.put(HistoryProvider.COLUMN_TIME_START, pd.mTime);
values.put(HistoryProvider.COLUMN_TIME_END,
System.currentTimeMillis());
values.put(HistoryProvider.COLUMN_STATE, state);
values.put(HistoryProvider.COLUMN_LOG, task.getTranscript());
getContentResolver().insert(ContentUri.URI_HISTORY, values);
}
}
@Override
public void OnProcessEvent(ProcessTask task, int event) {
ProcessDescriptor pd = (ProcessDescriptor) task;
/* notify all matching ProcessCallbacks
* make sure there is no allocation happening here
* as this function might get called very often.
* */
synchronized (mProcessRegistrations) {
final ArrayList<ProcessRegistration> registrations = mProcessRegistrations;
int cnt = registrations.size();
for(int i=0; i < cnt; ++i) {
final ProcessRegistration reg = registrations.get(i);
if (reg.equalsDescriptor(pd)) {
try {
reg.callback.OnProcessEvent(pd, event);
} catch (RemoteException e) {
Log.d(LOG_TAG, "Trying to access dead process callback");
}
}
}
}
}
public void createNotificationForTask(ProcessTask task) {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
task.getPid(), task.getActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
int titleRes = ACTION_EXECUTE.equals(task.getIntent().getAction()) ? R.string.executing_script : R.string.executing_process;
String title = getResources().getString(titleRes);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
this).setContentIntent(pendingIntent).setContentTitle(title)
.setContentText(task.getIntent().getDataString())
.setTicker(title).setSmallIcon(R.drawable.ic_launcher)
.setOngoing(true).setAutoCancel(false);
/* set as foreground if no active task is already set as foreground else just create a normal notification,
* which we might later promote to foreground when needed */
if(mForeground == 0) {
Log.d(LOG_TAG, String.format("Creating foreground notification for task %d", task.getPid()));
mForeground = task.getPid();
startForeground(mForeground, mBuilder.build());
} else {
Log.d(LOG_TAG, String.format("Creating normal notification for task %d", task.getPid()));
nm.notify(task.getPid(), mBuilder.build());
}
}
public void removeNotificationForTask(ProcessTask task) {
if(mForeground == task.getPid()) {
Log.d(LOG_TAG, String.format("Removing foreground notification for task %d", task.getPid()));
stopForeground(true);
/* check if we have other tasks running and promote first active task to foreground */
synchronized (mTasks) {
/* set mForeground to 0 so that any newly created task/notification will set this service
* to a foreground service. If there is still an active task we will promote that to
* a foreground notification */
mForeground = 0;
for (ProcessTask _task : mTasks) {
if(ProcessState.isActiveState(_task.getState())) {
Log.d(LOG_TAG, String.format("Promoting task %d to foreground", _task.getPid()));
createNotificationForTask(_task);
break;
}
}
}
} else {
Log.d(LOG_TAG, String.format("Removing normal notification for task %d", task.getPid()));
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(task.getPid());
}
}
/*
* simple pair to hold a registration between process callback and
* descriptor
*/
class ProcessRegistration {
public IProcessCallback callback;
public ProcessDescriptor descriptor;
public ProcessRegistration(IProcessCallback cb, ProcessDescriptor pd) {
this.callback = cb;
this.descriptor = pd;
}
public boolean equalsDescriptor(ProcessDescriptor pd) {
return descriptor.equals(pd);
}
public boolean equalsCallback(IProcessCallback cb) {
return callback.equals(cb);
}
@Override
public boolean equals(Object o) {
boolean result = false;
if (o instanceof ProcessRegistration) {
ProcessRegistration oreg = (ProcessRegistration) o;
result = (equalsCallback(oreg.callback) && equalsDescriptor(oreg.descriptor));
}
return result;
}
}
}