Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.googlecode.android_scripting.service;
     18 
     19 import android.app.Notification;
     20 import android.app.NotificationChannel;
     21 import android.app.NotificationManager;
     22 import android.app.PendingIntent;
     23 import android.content.Intent;
     24 import android.content.SharedPreferences;
     25 import android.os.Binder;
     26 import android.os.IBinder;
     27 import android.os.StrictMode;
     28 import android.preference.PreferenceManager;
     29 
     30 import com.googlecode.android_scripting.AndroidProxy;
     31 import com.googlecode.android_scripting.BaseApplication;
     32 import com.googlecode.android_scripting.Constants;
     33 import com.googlecode.android_scripting.ForegroundService;
     34 import com.googlecode.android_scripting.Log;
     35 import com.googlecode.android_scripting.NotificationIdFactory;
     36 import com.googlecode.android_scripting.R;
     37 import com.googlecode.android_scripting.ScriptLauncher;
     38 import com.googlecode.android_scripting.ScriptProcess;
     39 import com.googlecode.android_scripting.activity.ScriptProcessMonitor;
     40 import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
     41 import com.googlecode.android_scripting.interpreter.InterpreterProcess;
     42 import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter;
     43 
     44 import org.connectbot.ConsoleActivity;
     45 import org.connectbot.service.TerminalManager;
     46 
     47 import java.io.File;
     48 import java.lang.ref.WeakReference;
     49 import java.net.InetSocketAddress;
     50 import java.util.ArrayList;
     51 import java.util.List;
     52 import java.util.Map;
     53 import java.util.concurrent.ConcurrentHashMap;
     54 
     55 /**
     56  * A service that allows scripts and the RPC server to run in the background.
     57  *
     58  */
     59 public class ScriptingLayerService extends ForegroundService {
     60   private static final int NOTIFICATION_ID = NotificationIdFactory.create();
     61 
     62   private final IBinder mBinder;
     63   private final Map<Integer, InterpreterProcess> mProcessMap;
     64   private static final String CHANNEL_ID = "scripting_layer_service_channel";
     65   private final String LOG_TAG = "sl4a";
     66   private volatile int mModCount = 0;
     67   private Notification mNotification;
     68   private PendingIntent mNotificationPendingIntent;
     69   private InterpreterConfiguration mInterpreterConfiguration;
     70 
     71   private volatile WeakReference<InterpreterProcess> mRecentlyKilledProcess;
     72 
     73   private TerminalManager mTerminalManager;
     74 
     75   private SharedPreferences mPreferences = null;
     76   private boolean mHide;
     77 
     78   public class LocalBinder extends Binder {
     79     public ScriptingLayerService getService() {
     80       return ScriptingLayerService.this;
     81     }
     82   }
     83 
     84   @Override
     85   public IBinder onBind(Intent intent) {
     86     return mBinder;
     87   }
     88 
     89   public ScriptingLayerService() {
     90     super(NOTIFICATION_ID);
     91     mProcessMap = new ConcurrentHashMap<Integer, InterpreterProcess>();
     92     mBinder = new LocalBinder();
     93   }
     94 
     95   @Override
     96   public void onCreate() {
     97     super.onCreate();
     98     mInterpreterConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
     99     mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(null);
    100     mTerminalManager = new TerminalManager(this);
    101     mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
    102     mHide = mPreferences.getBoolean(Constants.HIDE_NOTIFY, false);
    103   }
    104 
    105   private void createNotificationChannel() {
    106     NotificationManager notificationManager = getNotificationManager();
    107     CharSequence name = getString(R.string.notification_channel_name);
    108     String description = getString(R.string.notification_channel_description);
    109     int importance = NotificationManager.IMPORTANCE_DEFAULT;
    110     NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
    111     channel.setDescription(description);
    112     channel.enableLights(false);
    113     channel.enableVibration(false);
    114     notificationManager.createNotificationChannel(channel);
    115   }
    116 
    117   @Override
    118   protected Notification createNotification() {
    119     Intent notificationIntent = new Intent(this, ScriptingLayerService.class);
    120     notificationIntent.setAction(Constants.ACTION_SHOW_RUNNING_SCRIPTS);
    121     mNotificationPendingIntent = PendingIntent.getService(this, 0, notificationIntent, 0);
    122 
    123     createNotificationChannel();
    124     Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID);
    125     builder.setSmallIcon(R.drawable.sl4a_notification_logo)
    126            .setTicker(null)
    127            .setWhen(System.currentTimeMillis())
    128            .setContentTitle("SL4A Service")
    129            .setContentText("Tap to view running scripts")
    130            .setContentIntent(mNotificationPendingIntent);
    131     mNotification = builder.build();
    132     mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
    133     return mNotification;
    134   }
    135 
    136   private void updateNotification(String tickerText) {
    137     if (tickerText.equals(mNotification.tickerText)) {
    138       // Consequent notifications with the same ticker-text are displayed without any ticker-text.
    139       // This is a way around. Alternatively, we can display process name and port.
    140       tickerText = tickerText + " ";
    141     }
    142     String msg;
    143     if (mProcessMap.size() <= 1) {
    144       msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running script";
    145     } else {
    146       msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running scripts";
    147     }
    148     Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID);
    149     builder.setContentTitle("SL4A Service")
    150            .setContentText(msg)
    151            .setContentIntent(mNotificationPendingIntent)
    152            .setSmallIcon(R.drawable.sl4a_notification_logo, mProcessMap.size())
    153            .setWhen(mNotification.when)
    154            .setTicker(tickerText);
    155 
    156     mNotification = builder.build();
    157     getNotificationManager().notify(NOTIFICATION_ID, mNotification);
    158   }
    159 
    160   private void startAction(Intent intent, int flags, int startId) {
    161     AndroidProxy proxy = null;
    162     InterpreterProcess interpreterProcess = null;
    163     String errmsg = null;
    164     if (intent == null) {
    165     } else if (intent.getAction().equals(Constants.ACTION_KILL_ALL)) {
    166       killAll();
    167       stopSelf(startId);
    168     } else if (intent.getAction().equals(Constants.ACTION_KILL_PROCESS)) {
    169       killProcess(intent);
    170       if (mProcessMap.isEmpty()) {
    171         stopSelf(startId);
    172       }
    173     } else if (intent.getAction().equals(Constants.ACTION_SHOW_RUNNING_SCRIPTS)) {
    174       showRunningScripts();
    175     } else { //We are launching a script of some kind
    176       if (intent.getAction().equals(Constants.ACTION_LAUNCH_SERVER)) {
    177         proxy = launchServer(intent, false);
    178         // TODO(damonkohler): This is just to make things easier. Really, we shouldn't need to start
    179         // an interpreter when all we want is a server.
    180         interpreterProcess = new InterpreterProcess(new ShellInterpreter(), proxy);
    181         interpreterProcess.setName("Server");
    182       }
    183       else if (intent.getAction().equals(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT)) {
    184         proxy = launchServer(intent, true);
    185         launchTerminal(proxy.getAddress());
    186         try {
    187           interpreterProcess = launchScript(intent, proxy);
    188         } catch (RuntimeException e) {
    189           errmsg =
    190               "Unable to run " + intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH) + "\n"
    191                   + e.getMessage();
    192           interpreterProcess = null;
    193         }
    194       } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT)) {
    195         proxy = launchServer(intent, true);
    196         interpreterProcess = launchScript(intent, proxy);
    197       } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_INTERPRETER)) {
    198         proxy = launchServer(intent, true);
    199         launchTerminal(proxy.getAddress());
    200         interpreterProcess = launchInterpreter(intent, proxy);
    201       }
    202       if (interpreterProcess == null) {
    203         errmsg = "Action not implemented: " + intent.getAction();
    204       } else {
    205         addProcess(interpreterProcess);
    206       }
    207     }
    208     if (errmsg != null) {
    209       updateNotification(errmsg);
    210     }
    211   }
    212 
    213     /**
    214      * {@inheritDoc}
    215      */
    216     @Override
    217     public int onStartCommand(Intent intent, int flags, int startId) {
    218         super.onStartCommand(intent, flags, startId);
    219         StrictMode.ThreadPolicy sl4aPolicy = new StrictMode.ThreadPolicy.Builder()
    220                 .detectAll()
    221                 .penaltyLog()
    222                 .build();
    223         StrictMode.setThreadPolicy(sl4aPolicy);
    224         if ((flags & START_FLAG_REDELIVERY) > 0) {
    225             Log.w("Intent for action " + intent.getAction() + " has been redelivered.");
    226         }
    227         // Do the heavy lifting off of the main thread. Prevents jank.
    228         new Thread(() -> startAction(intent, flags, startId)).start();
    229 
    230         return START_REDELIVER_INTENT;
    231     }
    232 
    233   private boolean tryPort(AndroidProxy androidProxy, boolean usePublicIp, int usePort) {
    234     if (usePublicIp) {
    235       return (androidProxy.startPublic(usePort) != null);
    236     } else {
    237       return (androidProxy.startLocal(usePort) != null);
    238     }
    239   }
    240 
    241   private AndroidProxy launchServer(Intent intent, boolean requiresHandshake) {
    242     AndroidProxy androidProxy = new AndroidProxy(this, intent, requiresHandshake);
    243     boolean usePublicIp = intent.getBooleanExtra(Constants.EXTRA_USE_EXTERNAL_IP, false);
    244     int usePort = intent.getIntExtra(Constants.EXTRA_USE_SERVICE_PORT, 0);
    245     // If port is in use, fall back to default behaviour
    246     if (!tryPort(androidProxy, usePublicIp, usePort)) {
    247       if (usePort != 0) {
    248         tryPort(androidProxy, usePublicIp, 0);
    249       }
    250     }
    251     return androidProxy;
    252   }
    253 
    254   private ScriptProcess launchScript(Intent intent, AndroidProxy proxy) {
    255     final int port = proxy.getAddress().getPort();
    256     File script = new File(intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH));
    257     return ScriptLauncher.launchScript(script, mInterpreterConfiguration, proxy, new Runnable() {
    258       @Override
    259       public void run() {
    260         // TODO(damonkohler): This action actually kills the script rather than notifying the
    261         // service that script exited on its own. We should distinguish between these two cases.
    262         Intent intent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
    263         intent.setAction(Constants.ACTION_KILL_PROCESS);
    264         intent.putExtra(Constants.EXTRA_PROXY_PORT, port);
    265         startService(intent);
    266       }
    267     });
    268   }
    269 
    270   private InterpreterProcess launchInterpreter(Intent intent, AndroidProxy proxy) {
    271     InterpreterConfiguration config =
    272         ((BaseApplication) getApplication()).getInterpreterConfiguration();
    273     final int port = proxy.getAddress().getPort();
    274     return ScriptLauncher.launchInterpreter(proxy, intent, config, new Runnable() {
    275       @Override
    276       public void run() {
    277         // TODO(damonkohler): This action actually kills the script rather than notifying the
    278         // service that script exited on its own. We should distinguish between these two cases.
    279         Intent intent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
    280         intent.setAction(Constants.ACTION_KILL_PROCESS);
    281         intent.putExtra(Constants.EXTRA_PROXY_PORT, port);
    282         startService(intent);
    283       }
    284     });
    285   }
    286 
    287   private void launchTerminal(InetSocketAddress address) {
    288     Intent i = new Intent(this, ConsoleActivity.class);
    289     i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    290     i.putExtra(Constants.EXTRA_PROXY_PORT, address.getPort());
    291     startActivity(i);
    292   }
    293 
    294   private void showRunningScripts() {
    295     Intent i = new Intent(this, ScriptProcessMonitor.class);
    296     i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    297     startActivity(i);
    298   }
    299 
    300   private void addProcess(InterpreterProcess process) {
    301     synchronized(mProcessMap) {
    302         mProcessMap.put(process.getPort(), process);
    303         mModCount++;
    304     }
    305     if (!mHide) {
    306       updateNotification(process.getName() + " started.");
    307     }
    308   }
    309 
    310   private InterpreterProcess removeProcess(int port) {
    311     InterpreterProcess process;
    312     synchronized(mProcessMap) {
    313         process = mProcessMap.remove(port);
    314         if (process == null) {
    315           return null;
    316         }
    317         mModCount++;
    318     }
    319     if (!mHide) {
    320       updateNotification(process.getName() + " exited.");
    321     }
    322     return process;
    323   }
    324 
    325   private void killProcess(Intent intent) {
    326     int processId = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, 0);
    327     InterpreterProcess process = removeProcess(processId);
    328     if (process != null) {
    329       process.kill();
    330       mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(process);
    331     }
    332   }
    333 
    334   public int getModCount() {
    335     return mModCount;
    336   }
    337 
    338   private void killAll() {
    339     for (InterpreterProcess process : getScriptProcessesList()) {
    340       process = removeProcess(process.getPort());
    341       if (process != null) {
    342         process.kill();
    343       }
    344     }
    345   }
    346 
    347   public List<InterpreterProcess> getScriptProcessesList() {
    348     ArrayList<InterpreterProcess> result = new ArrayList<InterpreterProcess>();
    349     result.addAll(mProcessMap.values());
    350     return result;
    351   }
    352 
    353   public InterpreterProcess getProcess(int port) {
    354     InterpreterProcess p = mProcessMap.get(port);
    355     if (p == null) {
    356       return mRecentlyKilledProcess.get();
    357     }
    358     return p;
    359   }
    360 
    361   public TerminalManager getTerminalManager() {
    362     return mTerminalManager;
    363   }
    364 }
    365