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
17package com.googlecode.android_scripting.service;
18
19import android.app.Notification;
20import android.app.NotificationChannel;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.os.Binder;
26import android.os.IBinder;
27import android.os.StrictMode;
28import android.preference.PreferenceManager;
29
30import com.googlecode.android_scripting.AndroidProxy;
31import com.googlecode.android_scripting.BaseApplication;
32import com.googlecode.android_scripting.Constants;
33import com.googlecode.android_scripting.ForegroundService;
34import com.googlecode.android_scripting.Log;
35import com.googlecode.android_scripting.NotificationIdFactory;
36import com.googlecode.android_scripting.R;
37import com.googlecode.android_scripting.ScriptLauncher;
38import com.googlecode.android_scripting.ScriptProcess;
39import com.googlecode.android_scripting.activity.ScriptProcessMonitor;
40import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
41import com.googlecode.android_scripting.interpreter.InterpreterProcess;
42import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter;
43
44import org.connectbot.ConsoleActivity;
45import org.connectbot.service.TerminalManager;
46
47import java.io.File;
48import java.lang.ref.WeakReference;
49import java.net.InetSocketAddress;
50import java.util.ArrayList;
51import java.util.List;
52import java.util.Map;
53import java.util.concurrent.ConcurrentHashMap;
54
55/**
56 * A service that allows scripts and the RPC server to run in the background.
57 *
58 */
59public 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