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