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