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