• 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.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 public class ScriptingLayerService extends ForegroundService {
59     private static final int NOTIFICATION_ID = NotificationIdFactory.create();
60 
61     private final IBinder mBinder;
62     private final Map<Integer, InterpreterProcess> mProcessMap;
63     private static final String CHANNEL_ID = "scripting_layer_service_channel";
64     private volatile int mModCount = 0;
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     /**
77      * A binder object that contains a reference to the ScriptingLayerService.
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<>();
93         mBinder = new LocalBinder();
94     }
95 
96     @Override
onCreate()97     public void onCreate() {
98         super.onCreate();
99         mInterpreterConfiguration = ((BaseApplication) getApplication())
100                 .getInterpreterConfiguration();
101         mRecentlyKilledProcess = new WeakReference<>(null);
102         mTerminalManager = new TerminalManager(this);
103         mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
104         mHide = mPreferences.getBoolean(Constants.HIDE_NOTIFY, false);
105     }
106 
createNotificationChannel()107     private void createNotificationChannel() {
108         NotificationManager notificationManager = getNotificationManager();
109         CharSequence name = getString(R.string.notification_channel_name);
110         String description = getString(R.string.notification_channel_description);
111         int importance = NotificationManager.IMPORTANCE_DEFAULT;
112         NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
113         channel.setDescription(description);
114         channel.enableLights(false);
115         channel.enableVibration(false);
116         notificationManager.createNotificationChannel(channel);
117     }
118 
119     @Override
createNotification()120     protected Notification createNotification() {
121         Intent notificationIntent = new Intent(this, ScriptingLayerService.class);
122         notificationIntent.setAction(Constants.ACTION_SHOW_RUNNING_SCRIPTS);
123         mNotificationPendingIntent =
124                 PendingIntent.getService(this, 0, notificationIntent,
125                         PendingIntent.FLAG_IMMUTABLE);
126 
127         createNotificationChannel();
128         Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID);
129         builder.setSmallIcon(R.drawable.sl4a_notification_logo)
130                 .setTicker(null)
131                 .setWhen(System.currentTimeMillis())
132                 .setContentTitle("SL4A Service")
133                 .setContentText("Tap to view running scripts")
134                 .setContentIntent(mNotificationPendingIntent);
135         mNotification = builder.build();
136         mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
137         return mNotification;
138     }
139 
updateNotification(String tickerText)140     private void updateNotification(String tickerText) {
141         if (tickerText.equals(mNotification.tickerText)) {
142             // Consequent notifications with the same ticker-text are displayed without any
143             // ticker-text. This is a way around. Alternatively, we can display process name and
144             // port.
145             tickerText = tickerText + " ";
146         }
147         String msg;
148         if (mProcessMap.size() <= 1) {
149             msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running script";
150         } else {
151             msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running scripts";
152         }
153         Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID);
154         builder.setContentTitle("SL4A Service")
155                 .setContentText(msg)
156                 .setContentIntent(mNotificationPendingIntent)
157                 .setSmallIcon(R.drawable.sl4a_notification_logo, mProcessMap.size())
158                 .setWhen(mNotification.when)
159                 .setTicker(tickerText);
160 
161         mNotification = builder.build();
162         getNotificationManager().notify(NOTIFICATION_ID, mNotification);
163     }
164 
startAction(Intent intent, int flags, int startId)165     private void startAction(Intent intent, int flags, int startId) {
166         if (intent == null || intent.getAction() == null) {
167             return;
168         }
169 
170         AndroidProxy proxy;
171         InterpreterProcess interpreterProcess = null;
172         String errmsg = null;
173         Log.d(String.format("Received intent: %s", intent.toUri(0)));
174 
175         if (intent.getAction().equals(Constants.ACTION_KILL_ALL)) {
176             killAll();
177             stopSelf(startId);
178         } else if (intent.getAction().equals(Constants.ACTION_KILL_PROCESS)) {
179             killProcess(intent);
180             if (mProcessMap.isEmpty()) {
181                 stopSelf(startId);
182             }
183         } else if (intent.getAction().equals(Constants.ACTION_SHOW_RUNNING_SCRIPTS)) {
184             showRunningScripts();
185         } else { //We are launching a script of some kind
186             if (intent.getAction().equals(Constants.ACTION_LAUNCH_SERVER)) {
187                 proxy = launchServer(intent, false);
188                 // TODO(damonkohler): This is just to make things easier. Really, we shouldn't need
189                 // to start an interpreter when all we want is a server.
190                 interpreterProcess = new InterpreterProcess(new ShellInterpreter(), proxy);
191                 interpreterProcess.setName("Server");
192             } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT)) {
193                 proxy = launchServer(intent, true);
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)
200                                     + "\n" + e.getMessage();
201                     interpreterProcess = null;
202                 }
203             } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT)) {
204                 proxy = launchServer(intent, true);
205                 interpreterProcess = launchScript(intent, proxy);
206             } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_INTERPRETER)) {
207                 proxy = launchServer(intent, true);
208                 launchTerminal(proxy.getAddress());
209                 interpreterProcess = launchInterpreter(intent, proxy);
210             }
211             if (interpreterProcess == null) {
212                 errmsg = "Action not implemented: " + intent.getAction();
213             } else {
214                 addProcess(interpreterProcess);
215             }
216         }
217         if (errmsg != null) {
218             updateNotification(errmsg);
219         }
220     }
221 
222     /**
223      * {@inheritDoc}
224      */
225     @Override
onStartCommand(Intent intent, int flags, int startId)226     public int onStartCommand(Intent intent, int flags, int startId) {
227         super.onStartCommand(intent, flags, startId);
228         StrictMode.ThreadPolicy sl4aPolicy = new StrictMode.ThreadPolicy.Builder()
229                 .detectAll()
230                 .penaltyLog()
231                 .build();
232         StrictMode.setThreadPolicy(sl4aPolicy);
233         if ((flags & START_FLAG_REDELIVERY) > 0) {
234             Log.w("Intent for action " + intent.getAction() + " has been redelivered.");
235         }
236         // Do the heavy lifting off of the main thread. Prevents jank.
237         new Thread(() -> startAction(intent, flags, startId)).start();
238 
239         return START_REDELIVER_INTENT;
240     }
241 
tryPort(AndroidProxy androidProxy, boolean usePublicIp, int usePort)242     private boolean tryPort(AndroidProxy androidProxy, boolean usePublicIp, int usePort) {
243         if (usePublicIp) {
244             return (androidProxy.startPublic(usePort) != null);
245         } else {
246             return (androidProxy.startLocal(usePort) != null);
247         }
248     }
249 
launchServer(Intent intent, boolean requiresHandshake)250     private AndroidProxy launchServer(Intent intent, boolean requiresHandshake) {
251         AndroidProxy androidProxy = new AndroidProxy(this, intent, requiresHandshake);
252         boolean usePublicIp = intent.getBooleanExtra(Constants.EXTRA_USE_EXTERNAL_IP, false);
253         int usePort = intent.getIntExtra(Constants.EXTRA_USE_SERVICE_PORT, 0);
254         // If port is in use, fall back to default behaviour
255         if (!tryPort(androidProxy, usePublicIp, usePort)) {
256             if (usePort != 0) {
257                 tryPort(androidProxy, usePublicIp, 0);
258             }
259         }
260         return androidProxy;
261     }
262 
launchScript(Intent intent, AndroidProxy proxy)263     private ScriptProcess launchScript(Intent intent, AndroidProxy proxy) {
264         Log.d(String.format("Launching script with intent: %s.",
265                 intent.toUri(0)));
266         final int port = proxy.getAddress().getPort();
267         File script = new File(intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH));
268         return ScriptLauncher.launchScript(script, mInterpreterConfiguration, proxy, () -> {
269             // TODO(damonkohler): This action actually kills the script rather than notifying the
270             // service that script exited on its own. We should distinguish between these two cases.
271             Intent newIntent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
272             newIntent.setAction(Constants.ACTION_KILL_PROCESS);
273             newIntent.putExtra(Constants.EXTRA_PROXY_PORT, port);
274             Log.i(String.format("Killing process from default shutdownHook: %s",
275                     newIntent.toUri(0)));
276             startService(newIntent);
277         });
278     }
279 
launchInterpreter(Intent intent, AndroidProxy proxy)280     private InterpreterProcess launchInterpreter(Intent intent, AndroidProxy proxy) {
281         Log.d(String.format("Launching interpreter with intent: %s.",
282                 intent.toUri(0)));
283         InterpreterConfiguration config =
284                 ((BaseApplication) getApplication()).getInterpreterConfiguration();
285         final int port = proxy.getAddress().getPort();
286         return ScriptLauncher.launchInterpreter(proxy, intent, config, () -> {
287             // TODO(damonkohler): This action actually kills the script rather than notifying the
288             // service that script exited on its own. We should distinguish between these two cases.
289             Intent newIntent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
290             newIntent.setAction(Constants.ACTION_KILL_PROCESS);
291             newIntent.putExtra(Constants.EXTRA_PROXY_PORT, port);
292             Log.i(String.format("Killing process from default shutdownHook: %s",
293                     newIntent.toUri(0)));
294             startService(newIntent);
295         });
296     }
297 
298     private void launchTerminal(InetSocketAddress address) {
299         Intent i = new Intent(this, ConsoleActivity.class);
300         i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
301         i.putExtra(Constants.EXTRA_PROXY_PORT, address.getPort());
302         startActivity(i);
303     }
304 
305     private void showRunningScripts() {
306         Intent i = new Intent(this, ScriptProcessMonitor.class);
307         i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
308         startActivity(i);
309     }
310 
311     private void addProcess(InterpreterProcess process) {
312         synchronized (mProcessMap) {
313             mProcessMap.put(process.getPort(), process);
314             mModCount++;
315         }
316         if (!mHide) {
317             updateNotification(process.getName() + " started.");
318         }
319     }
320 
321     private InterpreterProcess removeProcess(int port) {
322         InterpreterProcess process;
323         synchronized (mProcessMap) {
324             process = mProcessMap.remove(port);
325             if (process == null) {
326                 return null;
327             }
328             mModCount++;
329         }
330         if (!mHide) {
331             updateNotification(process.getName() + " exited.");
332         }
333         return process;
334     }
335 
336     private void killProcess(Intent intent) {
337         int processId = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, 0);
338         InterpreterProcess process = removeProcess(processId);
339         if (process != null) {
340             process.kill();
341             mRecentlyKilledProcess = new WeakReference<>(process);
342         }
343     }
344 
345     public int getModCount() {
346         return mModCount;
347     }
348 
349     private void killAll() {
350         for (InterpreterProcess process : getScriptProcessesList()) {
351             process = removeProcess(process.getPort());
352             if (process != null) {
353                 process.kill();
354             }
355         }
356     }
357 
358     /**
359      * Returns the list of all running InterpreterProcesses. This list includes RPC servers.
360      *
361      * @return a list of all running interpreter processes
362      */
363     public List<InterpreterProcess> getScriptProcessesList() {
364         ArrayList<InterpreterProcess> result = new ArrayList<>();
365         result.addAll(mProcessMap.values());
366         return result;
367     }
368 
369     /**
370      * Returns the process running on the given port, if any.
371      *
372      * @param port the integer value corresponding to the port to find a process on
373      * @return the InterpreterProcess running on that port, or null
374      */
375     public InterpreterProcess getProcess(int port) {
376         InterpreterProcess p = mProcessMap.get(port);
377         if (p == null) {
378             return mRecentlyKilledProcess.get();
379         }
380         return p;
381     }
382 
383     public TerminalManager getTerminalManager() {
384         return mTerminalManager;
385     }
386 }
387