1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.tools.sdkcontroller.service; 18 19 import java.util.ArrayList; 20 import java.util.HashSet; 21 import java.util.List; 22 import java.util.Set; 23 24 import android.app.Activity; 25 import android.app.Notification; 26 import android.app.NotificationManager; 27 import android.app.PendingIntent; 28 import android.app.Service; 29 import android.content.Intent; 30 import android.os.Binder; 31 import android.os.IBinder; 32 import android.util.Log; 33 34 import com.android.tools.sdkcontroller.R; 35 import com.android.tools.sdkcontroller.activities.MainActivity; 36 import com.android.tools.sdkcontroller.handlers.BaseHandler; 37 import com.android.tools.sdkcontroller.handlers.BaseHandler.HandlerType; 38 import com.android.tools.sdkcontroller.handlers.MultiTouchHandler; 39 import com.android.tools.sdkcontroller.handlers.SensorsHandler; 40 import com.android.tools.sdkcontroller.lib.EmulatorConnection; 41 import com.android.tools.sdkcontroller.lib.EmulatorConnection.EmulatorConnectionType; 42 import com.android.tools.sdkcontroller.lib.EmulatorListener; 43 44 /** 45 * The background service of the SdkController. 46 * There can be only one instance of this. 47 * <p/> 48 * The service manages a number of action "handlers" which can be seen as individual tasks 49 * that the user might want to accomplish, for example "sending sensor data to the emulator" 50 * or "sending multi-touch data and displaying an emulator screen". 51 * <p/> 52 * Each handler currently has its own emulator connection associated to it (cf class 53 * {@code EmuCnxHandler} below. However our goal is to later move to a single connection channel 54 * with all data multiplexed on top of it. 55 * <p/> 56 * All the handlers are created when the service starts, and whether the emulator connection 57 * is successful or not, and whether there's any UI to control it. It's up to the handlers 58 * to deal with these specific details. <br/> 59 * For example the {@link SensorsHandler} initializes its sensor list as soon as created 60 * and then tries to send data as soon as there's an emulator connection. 61 * On the other hand the {@link MultiTouchHandler} lays dormant till there's an UI interacting 62 * with it. 63 */ 64 public class ControllerService extends Service { 65 66 /* 67 * Implementation reference: 68 * http://developer.android.com/reference/android/app/Service.html#LocalServiceSample 69 */ 70 71 public static String TAG = ControllerService.class.getSimpleName(); 72 private static boolean DEBUG = true; 73 74 /** Identifier for the notification. */ 75 private static int NOTIF_ID = 'S' << 24 + 'd' << 16 + 'k' << 8 + 'C' << 0; 76 77 private final IBinder mBinder = new ControllerBinder(); 78 79 private List<ControllerListener> mListeners = new ArrayList<ControllerListener>(); 80 81 /** 82 * Whether the service is running. Set to true in onCreate, false in onDestroy. 83 */ 84 private static volatile boolean gServiceIsRunning = false; 85 86 /** Internal error reported by the service. */ 87 private String mServiceError = ""; 88 89 private final Set<EmuCnxHandler> mHandlers = new HashSet<ControllerService.EmuCnxHandler>(); 90 91 /** 92 * Interface that the service uses to notify binded activities. 93 * <p/> 94 * As a design rule, implementations of this listener should be aware that most calls 95 * will NOT happen on the UI thread. Any access to the UI should be properly protected 96 * by using {@link Activity#runOnUiThread(Runnable)}. 97 */ 98 public interface ControllerListener { 99 /** 100 * The error string reported by the service has changed. <br/> 101 * Note this may be called from a thread different than the UI thread. 102 */ 103 void onErrorChanged(); 104 105 /** 106 * The service status has changed (emulator connected/disconnected.) 107 */ 108 void onStatusChanged(); 109 } 110 111 /** Interface that callers can use to access the service. */ 112 public class ControllerBinder extends Binder { 113 114 /** 115 * Adds a new listener that will be notified when the service state changes. 116 * 117 * @param listener A non-null listener. Ignored if already listed. 118 */ 119 public void addControllerListener(ControllerListener listener) { 120 assert listener != null; 121 if (listener != null) { 122 synchronized(mListeners) { 123 if (!mListeners.contains(listener)) { 124 mListeners.add(listener); 125 } 126 } 127 } 128 } 129 130 /** 131 * Removes a listener. 132 * 133 * @param listener A listener to remove. Can be null. 134 */ 135 public void removeControllerListener(ControllerListener listener) { 136 assert listener != null; 137 synchronized(mListeners) { 138 mListeners.remove(listener); 139 } 140 } 141 142 /** 143 * Returns the error string accumulated by the service. 144 * Typically these would relate to failures to establish the communication 145 * channel(s) with the emulator, or unexpected disconnections. 146 */ 147 public String getServiceError() { 148 return mServiceError; 149 } 150 151 /** 152 * Indicates when <em>all</all> the communication channels for all handlers 153 * are properly connected. 154 * 155 * @return True if all the handler's communication channels are connected. 156 */ 157 public boolean isEmuConnected() { 158 for (EmuCnxHandler handler : mHandlers) { 159 if (!handler.isConnected()) { 160 return false; 161 } 162 } 163 return true; 164 } 165 166 /** 167 * Returns the handler for the given type. 168 * 169 * @param type One of the {@link HandlerType}s. Must not be null. 170 * @return Null if the type is not found, otherwise the handler's unique instance. 171 */ 172 public BaseHandler getHandler(HandlerType type) { 173 for (EmuCnxHandler handler : mHandlers) { 174 BaseHandler h = handler.getHandler(); 175 if (h.getType() == type) { 176 return h; 177 } 178 } 179 return null; 180 } 181 } 182 183 /** 184 * Whether the service is running. Set to true in onCreate, false in onDestroy. 185 */ 186 public static boolean isServiceIsRunning() { 187 return gServiceIsRunning; 188 } 189 190 @Override 191 public void onCreate() { 192 super.onCreate(); 193 if (DEBUG) Log.d(TAG, "Service onCreate"); 194 gServiceIsRunning = true; 195 showNotification(); 196 onServiceStarted(); 197 } 198 199 @Override 200 public int onStartCommand(Intent intent, int flags, int startId) { 201 // We want this service to continue running until it is explicitly 202 // stopped, so return sticky. 203 if (DEBUG) Log.d(TAG, "Service onStartCommand"); 204 return START_STICKY; 205 } 206 207 @Override 208 public IBinder onBind(Intent intent) { 209 if (DEBUG) Log.d(TAG, "Service onBind"); 210 return mBinder; 211 } 212 213 @Override 214 public void onDestroy() { 215 if (DEBUG) Log.d(TAG, "Service onDestroy"); 216 gServiceIsRunning = false; 217 removeNotification(); 218 resetError(); 219 onServiceStopped(); 220 super.onDestroy(); 221 } 222 223 // ------ 224 225 /** 226 * Wrapper that associates one {@link EmulatorConnection} with 227 * one {@link BaseHandler}. Ideally we would not need this if all 228 * the action handlers were using the same port, so this wrapper 229 * is just temporary. 230 */ 231 private class EmuCnxHandler implements EmulatorListener { 232 233 private EmulatorConnection mCnx; 234 private boolean mConnected; 235 private final BaseHandler mHandler; 236 237 public EmuCnxHandler(BaseHandler handler) { 238 mHandler = handler; 239 } 240 241 @Override 242 public void onEmulatorConnected() { 243 mConnected = true; 244 notifyStatusChanged(); 245 } 246 247 @Override 248 public void onEmulatorDisconnected() { 249 mConnected = false; 250 notifyStatusChanged(); 251 } 252 253 @Override 254 public String onEmulatorQuery(String query, String param) { 255 if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " Query " + query); 256 return mHandler.onEmulatorQuery(query, param); 257 } 258 259 @Override 260 public String onEmulatorBlobQuery(byte[] array) { 261 if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " BlobQuery " + array.length); 262 return mHandler.onEmulatorBlobQuery(array); 263 } 264 265 EmuCnxHandler connect() { 266 assert mCnx == null; 267 268 mCnx = new EmulatorConnection(this); 269 270 // Apps targeting Honeycomb SDK can't do network IO on their main UI 271 // thread. So just start the connection from a thread. 272 Thread t = new Thread(new Runnable() { 273 @Override 274 public void run() { 275 // This will call onEmulatorBindResult with the result. 276 mCnx.connect(mHandler.getPort(), EmulatorConnectionType.SYNC_CONNECTION); 277 } 278 }, "EmuCnxH.connect-" + mHandler.getType().toString()); 279 t.start(); 280 281 return this; 282 } 283 284 @Override 285 public void onEmulatorBindResult(boolean success, Exception e) { 286 if (success) { 287 mHandler.onStart(mCnx, ControllerService.this /*context*/); 288 } else { 289 Log.e(TAG, "EmuCnx failed for " + mHandler.getType(), e); 290 String msg = mHandler.getType().toString() + " failed: " + 291 (e == null ? "n/a" : e.toString()); 292 addError(msg); 293 } 294 } 295 296 void disconnect() { 297 if (mCnx != null) { 298 mHandler.onStop(); 299 mCnx.disconnect(); 300 mCnx = null; 301 } 302 } 303 304 boolean isConnected() { 305 return mConnected; 306 } 307 308 public BaseHandler getHandler() { 309 return mHandler; 310 } 311 } 312 313 private void disconnectAll() { 314 for(EmuCnxHandler handler : mHandlers) { 315 handler.disconnect(); 316 } 317 mHandlers.clear(); 318 } 319 320 /** 321 * Called when the service has been created. 322 */ 323 private void onServiceStarted() { 324 try { 325 disconnectAll(); 326 327 assert mHandlers.isEmpty(); 328 mHandlers.add(new EmuCnxHandler(new MultiTouchHandler()).connect()); 329 mHandlers.add(new EmuCnxHandler(new SensorsHandler()).connect()); 330 } catch (Exception e) { 331 addError("Connection failed: " + e.toString()); 332 } 333 } 334 335 /** 336 * Called when the service is being destroyed. 337 */ 338 private void onServiceStopped() { 339 disconnectAll(); 340 } 341 342 private void notifyErrorChanged() { 343 synchronized(mListeners) { 344 for (ControllerListener listener : mListeners) { 345 listener.onErrorChanged(); 346 } 347 } 348 } 349 350 private void notifyStatusChanged() { 351 synchronized(mListeners) { 352 for (ControllerListener listener : mListeners) { 353 listener.onStatusChanged(); 354 } 355 } 356 } 357 358 /** 359 * Resets the error string and notify listeners. 360 */ 361 private void resetError() { 362 mServiceError = ""; 363 364 notifyErrorChanged(); 365 } 366 367 /** 368 * An internal utility method to add a line to the error string and notify listeners. 369 * @param error A non-null non-empty error line. \n will be added automatically. 370 */ 371 private void addError(String error) { 372 Log.e(TAG, error); 373 if (mServiceError.length() > 0) { 374 mServiceError += "\n"; 375 } 376 mServiceError += error; 377 378 notifyErrorChanged(); 379 } 380 381 /** 382 * Displays a notification showing that the service is running. 383 * When the user touches the notification, it opens the main activity 384 * which allows the user to stop this service. 385 */ 386 @SuppressWarnings("deprecated") 387 private void showNotification() { 388 NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 389 390 String text = getString(R.string.service_notif_title); 391 392 // Note: Notification is marked as deprecated -- in API 11+ there's a new Builder class 393 // but we need to have API 7 compatibility so we ignore that warning. 394 395 Notification n = new Notification(R.drawable.ic_launcher, text, System.currentTimeMillis()); 396 n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; 397 Intent intent = new Intent(this, MainActivity.class); 398 intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 399 PendingIntent pi = PendingIntent.getActivity( 400 this, //context 401 0, //requestCode 402 intent, //intent 403 0 // pending intent flags 404 ); 405 n.setLatestEventInfo(this, text, text, pi); 406 407 nm.notify(NOTIF_ID, n); 408 } 409 410 private void removeNotification() { 411 NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 412 nm.cancel(NOTIF_ID); 413 } 414 } 415