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