• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * 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.Build;
28 import android.os.StrictMode;
29 import android.preference.PreferenceManager;
30 import android.util.Log;
31 
32 import com.googlecode.android_scripting.AndroidProxy;
33 import com.googlecode.android_scripting.BaseApplication;
34 import com.googlecode.android_scripting.Constants;
35 import com.googlecode.android_scripting.ForegroundService;
36 import com.googlecode.android_scripting.NotificationIdFactory;
37 import com.googlecode.android_scripting.R;
38 import com.googlecode.android_scripting.ScriptLauncher;
39 import com.googlecode.android_scripting.ScriptProcess;
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  * @author Damon Kohler (damonkohler@gmail.com)
59  */
60 public class ScriptingLayerService extends ForegroundService {
61   private static final int NOTIFICATION_ID = NotificationIdFactory.create();
62 
63   private final IBinder mBinder;
64   private final Map<Integer, InterpreterProcess> mProcessMap;
65   private final String LOG_TAG = "sl4a";
66   private volatile int mModCount = 0;
67   private NotificationManager mNotificationManager;
68   private Notification mNotification;
69   private PendingIntent mNotificationPendingIntent;
70   private InterpreterConfiguration mInterpreterConfiguration;
71 
72   private volatile WeakReference<InterpreterProcess> mRecentlyKilledProcess;
73 
74   private TerminalManager mTerminalManager;
75 
76   private SharedPreferences mPreferences = null;
77   private boolean mHide;
78 
79   public class LocalBinder extends Binder {
getService()80     public ScriptingLayerService getService() {
81       return ScriptingLayerService.this;
82     }
83   }
84 
85   @Override
onBind(Intent intent)86   public IBinder onBind(Intent intent) {
87     return mBinder;
88   }
89 
ScriptingLayerService()90   public ScriptingLayerService() {
91     super(NOTIFICATION_ID);
92     mProcessMap = new ConcurrentHashMap<Integer, InterpreterProcess>();
93     mBinder = new LocalBinder();
94   }
95 
96   @Override
onCreate()97   public void onCreate() {
98     super.onCreate();
99     mInterpreterConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
100     mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
101     mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(null);
102     mTerminalManager = new TerminalManager(this);
103     mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
104     mHide = mPreferences.getBoolean(Constants.HIDE_NOTIFY, false);
105   }
106 
107   @Override
createNotification()108   protected Notification createNotification() {
109     Intent notificationIntent = new Intent(this, ScriptingLayerService.class);
110     notificationIntent.setAction(Constants.ACTION_SHOW_RUNNING_SCRIPTS);
111     mNotificationPendingIntent = PendingIntent.getService(this, 0, notificationIntent, 0);
112 
113     Notification.Builder builder = new Notification.Builder(this);
114     builder.setSmallIcon(R.drawable.sl4a_notification_logo)
115            .setTicker(null)
116            .setWhen(System.currentTimeMillis())
117            .setContentTitle("SL4A Service")
118            .setContentText("Tap to view running scripts")
119            .setContentIntent(mNotificationPendingIntent);
120     mNotification = builder.build();
121     mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
122     return mNotification;
123   }
124 
updateNotification(String tickerText)125   private void updateNotification(String tickerText) {
126     if (tickerText.equals(mNotification.tickerText)) {
127       // Consequent notifications with the same ticker-text are displayed without any ticker-text.
128       // This is a way around. Alternatively, we can display process name and port.
129       tickerText = tickerText + " ";
130     }
131     String msg;
132     if (mProcessMap.size() <= 1) {
133       msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running script";
134     } else {
135       msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running scripts";
136     }
137     Notification.Builder builder = new Notification.Builder(this);
138     builder.setContentTitle("SL4A Service")
139            .setContentText(msg)
140            .setContentIntent(mNotificationPendingIntent)
141            .setSmallIcon(mNotification.icon, mProcessMap.size())
142            .setWhen(mNotification.when)
143            .setTicker(tickerText);
144 
145     mNotification = builder.build();
146     mNotificationManager.notify(NOTIFICATION_ID, mNotification);
147   }
148 
149   @Override
onStartCommand(Intent intent, int flags, int startId)150   public int onStartCommand(Intent intent, int flags, int startId) {
151     super.onStartCommand(intent, flags, startId);
152     String errmsg = null;
153     if (intent == null) {
154       return START_REDELIVER_INTENT;
155     }
156     if (intent.getAction().equals(Constants.ACTION_KILL_ALL)) {
157       killAll();
158       stopSelf(startId);
159       return START_REDELIVER_INTENT;
160     }
161 
162     if (intent.getAction().equals(Constants.ACTION_KILL_PROCESS)) {
163       killProcess(intent);
164       if (mProcessMap.isEmpty()) {
165         stopSelf(startId);
166       }
167       return START_REDELIVER_INTENT;
168     }
169 
170     if (intent.getAction().equals(Constants.ACTION_SHOW_RUNNING_SCRIPTS)) {
171       showRunningScripts();
172       return START_REDELIVER_INTENT;
173     }
174 
175     //TODO: b/26538940 We need to go back to a strict policy and fix the problems
176     StrictMode.ThreadPolicy sl4aPolicy = new StrictMode.ThreadPolicy.Builder()
177         .detectAll()
178         .penaltyLog()
179         .build();
180 
181     StrictMode.setThreadPolicy(sl4aPolicy);
182 
183     AndroidProxy proxy = null;
184     InterpreterProcess interpreterProcess = null;
185     if (intent.getAction().equals(Constants.ACTION_LAUNCH_SERVER)) {
186       proxy = launchServer(intent, false);
187       // TODO(damonkohler): This is just to make things easier. Really, we shouldn't need to start
188       // an interpreter when all we want is a server.
189       interpreterProcess = new InterpreterProcess(new ShellInterpreter(), proxy);
190       interpreterProcess.setName("Server");
191     } else {
192       proxy = launchServer(intent, true);
193       if (intent.getAction().equals(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT)) {
194         launchTerminal(proxy.getAddress());
195         try {
196           interpreterProcess = launchScript(intent, proxy);
197         } catch (RuntimeException e) {
198           errmsg =
199               "Unable to run " + intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH) + "\n"
200                   + e.getMessage();
201           interpreterProcess = null;
202         }
203       } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT)) {
204         interpreterProcess = launchScript(intent, proxy);
205       } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_INTERPRETER)) {
206         launchTerminal(proxy.getAddress());
207         interpreterProcess = launchInterpreter(intent, proxy);
208       }
209     }
210     if (errmsg != null) {
211       updateNotification(errmsg);
212     } else if (interpreterProcess == null) {
213       updateNotification("Action not implemented: " + intent.getAction());
214     } else {
215       addProcess(interpreterProcess);
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     mProcessMap.put(process.getPort(), process);
289     mModCount++;
290     if (!mHide) {
291       updateNotification(process.getName() + " started.");
292     }
293   }
294 
295   private InterpreterProcess removeProcess(int port) {
296     InterpreterProcess process = mProcessMap.remove(port);
297     if (process == null) {
298       return null;
299     }
300     mModCount++;
301     if (!mHide) {
302       updateNotification(process.getName() + " exited.");
303     }
304     return process;
305   }
306 
307   private void killProcess(Intent intent) {
308     int processId = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, 0);
309     InterpreterProcess process = removeProcess(processId);
310     if (process != null) {
311       process.kill();
312       mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(process);
313     }
314   }
315 
316   public int getModCount() {
317     return mModCount;
318   }
319 
320   private void killAll() {
321     for (InterpreterProcess process : getScriptProcessesList()) {
322       process = removeProcess(process.getPort());
323       if (process != null) {
324         process.kill();
325       }
326     }
327   }
328 
329   public List<InterpreterProcess> getScriptProcessesList() {
330     ArrayList<InterpreterProcess> result = new ArrayList<InterpreterProcess>();
331     result.addAll(mProcessMap.values());
332     return result;
333   }
334 
335   public InterpreterProcess getProcess(int port) {
336     InterpreterProcess p = mProcessMap.get(port);
337     if (p == null) {
338       return mRecentlyKilledProcess.get();
339     }
340     return p;
341   }
342 
343   public TerminalManager getTerminalManager() {
344     return mTerminalManager;
345   }
346 }
347