/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.tools.sdkcontroller.service; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.util.Log; import com.android.tools.sdkcontroller.R; import com.android.tools.sdkcontroller.activities.MainActivity; import com.android.tools.sdkcontroller.handlers.BaseHandler; import com.android.tools.sdkcontroller.handlers.BaseHandler.HandlerType; import com.android.tools.sdkcontroller.handlers.MultiTouchHandler; import com.android.tools.sdkcontroller.handlers.SensorsHandler; import com.android.tools.sdkcontroller.lib.EmulatorConnection; import com.android.tools.sdkcontroller.lib.EmulatorConnection.EmulatorConnectionType; import com.android.tools.sdkcontroller.lib.EmulatorListener; /** * The background service of the SdkController. * There can be only one instance of this. *

* The service manages a number of action "handlers" which can be seen as individual tasks * that the user might want to accomplish, for example "sending sensor data to the emulator" * or "sending multi-touch data and displaying an emulator screen". *

* Each handler currently has its own emulator connection associated to it (cf class * {@code EmuCnxHandler} below. However our goal is to later move to a single connection channel * with all data multiplexed on top of it. *

* All the handlers are created when the service starts, and whether the emulator connection * is successful or not, and whether there's any UI to control it. It's up to the handlers * to deal with these specific details.
* For example the {@link SensorsHandler} initializes its sensor list as soon as created * and then tries to send data as soon as there's an emulator connection. * On the other hand the {@link MultiTouchHandler} lays dormant till there's an UI interacting * with it. */ public class ControllerService extends Service { /* * Implementation reference: * http://developer.android.com/reference/android/app/Service.html#LocalServiceSample */ public static String TAG = ControllerService.class.getSimpleName(); private static boolean DEBUG = true; /** Identifier for the notification. */ private static int NOTIF_ID = 'S' << 24 + 'd' << 16 + 'k' << 8 + 'C' << 0; private final IBinder mBinder = new ControllerBinder(); private List mListeners = new ArrayList(); /** * Whether the service is running. Set to true in onCreate, false in onDestroy. */ private static volatile boolean gServiceIsRunning = false; /** Internal error reported by the service. */ private String mServiceError = ""; private final Set mHandlers = new HashSet(); /** * Interface that the service uses to notify binded activities. *

* As a design rule, implementations of this listener should be aware that most calls * will NOT happen on the UI thread. Any access to the UI should be properly protected * by using {@link Activity#runOnUiThread(Runnable)}. */ public interface ControllerListener { /** * The error string reported by the service has changed.
* Note this may be called from a thread different than the UI thread. */ void onErrorChanged(); /** * The service status has changed (emulator connected/disconnected.) */ void onStatusChanged(); } /** Interface that callers can use to access the service. */ public class ControllerBinder extends Binder { /** * Adds a new listener that will be notified when the service state changes. * * @param listener A non-null listener. Ignored if already listed. */ public void addControllerListener(ControllerListener listener) { assert listener != null; if (listener != null) { synchronized(mListeners) { if (!mListeners.contains(listener)) { mListeners.add(listener); } } } } /** * Removes a listener. * * @param listener A listener to remove. Can be null. */ public void removeControllerListener(ControllerListener listener) { assert listener != null; synchronized(mListeners) { mListeners.remove(listener); } } /** * Returns the error string accumulated by the service. * Typically these would relate to failures to establish the communication * channel(s) with the emulator, or unexpected disconnections. */ public String getServiceError() { return mServiceError; } /** * Indicates when all the communication channels for all handlers * are properly connected. * * @return True if all the handler's communication channels are connected. */ public boolean isEmuConnected() { for (EmuCnxHandler handler : mHandlers) { if (!handler.isConnected()) { return false; } } return true; } /** * Returns the handler for the given type. * * @param type One of the {@link HandlerType}s. Must not be null. * @return Null if the type is not found, otherwise the handler's unique instance. */ public BaseHandler getHandler(HandlerType type) { for (EmuCnxHandler handler : mHandlers) { BaseHandler h = handler.getHandler(); if (h.getType() == type) { return h; } } return null; } } /** * Whether the service is running. Set to true in onCreate, false in onDestroy. */ public static boolean isServiceIsRunning() { return gServiceIsRunning; } @Override public void onCreate() { super.onCreate(); if (DEBUG) Log.d(TAG, "Service onCreate"); gServiceIsRunning = true; showNotification(); onServiceStarted(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // We want this service to continue running until it is explicitly // stopped, so return sticky. if (DEBUG) Log.d(TAG, "Service onStartCommand"); return START_STICKY; } @Override public IBinder onBind(Intent intent) { if (DEBUG) Log.d(TAG, "Service onBind"); return mBinder; } @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "Service onDestroy"); gServiceIsRunning = false; removeNotification(); resetError(); onServiceStopped(); super.onDestroy(); } // ------ /** * Wrapper that associates one {@link EmulatorConnection} with * one {@link BaseHandler}. Ideally we would not need this if all * the action handlers were using the same port, so this wrapper * is just temporary. */ private class EmuCnxHandler implements EmulatorListener { private EmulatorConnection mCnx; private boolean mConnected; private final BaseHandler mHandler; public EmuCnxHandler(BaseHandler handler) { mHandler = handler; } @Override public void onEmulatorConnected() { mConnected = true; notifyStatusChanged(); } @Override public void onEmulatorDisconnected() { mConnected = false; notifyStatusChanged(); } @Override public String onEmulatorQuery(String query, String param) { if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " Query " + query); return mHandler.onEmulatorQuery(query, param); } @Override public String onEmulatorBlobQuery(byte[] array) { if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " BlobQuery " + array.length); return mHandler.onEmulatorBlobQuery(array); } EmuCnxHandler connect() { assert mCnx == null; mCnx = new EmulatorConnection(this); // Apps targeting Honeycomb SDK can't do network IO on their main UI // thread. So just start the connection from a thread. Thread t = new Thread(new Runnable() { @Override public void run() { // This will call onEmulatorBindResult with the result. mCnx.connect(mHandler.getPort(), EmulatorConnectionType.SYNC_CONNECTION); } }, "EmuCnxH.connect-" + mHandler.getType().toString()); t.start(); return this; } @Override public void onEmulatorBindResult(boolean success, Exception e) { if (success) { mHandler.onStart(mCnx, ControllerService.this /*context*/); } else { Log.e(TAG, "EmuCnx failed for " + mHandler.getType(), e); String msg = mHandler.getType().toString() + " failed: " + (e == null ? "n/a" : e.toString()); addError(msg); } } void disconnect() { if (mCnx != null) { mHandler.onStop(); mCnx.disconnect(); mCnx = null; } } boolean isConnected() { return mConnected; } public BaseHandler getHandler() { return mHandler; } } private void disconnectAll() { for(EmuCnxHandler handler : mHandlers) { handler.disconnect(); } mHandlers.clear(); } /** * Called when the service has been created. */ private void onServiceStarted() { try { disconnectAll(); assert mHandlers.isEmpty(); mHandlers.add(new EmuCnxHandler(new MultiTouchHandler()).connect()); mHandlers.add(new EmuCnxHandler(new SensorsHandler()).connect()); } catch (Exception e) { addError("Connection failed: " + e.toString()); } } /** * Called when the service is being destroyed. */ private void onServiceStopped() { disconnectAll(); } private void notifyErrorChanged() { synchronized(mListeners) { for (ControllerListener listener : mListeners) { listener.onErrorChanged(); } } } private void notifyStatusChanged() { synchronized(mListeners) { for (ControllerListener listener : mListeners) { listener.onStatusChanged(); } } } /** * Resets the error string and notify listeners. */ private void resetError() { mServiceError = ""; notifyErrorChanged(); } /** * An internal utility method to add a line to the error string and notify listeners. * @param error A non-null non-empty error line. \n will be added automatically. */ private void addError(String error) { Log.e(TAG, error); if (mServiceError.length() > 0) { mServiceError += "\n"; } mServiceError += error; notifyErrorChanged(); } /** * Displays a notification showing that the service is running. * When the user touches the notification, it opens the main activity * which allows the user to stop this service. */ @SuppressWarnings("deprecated") private void showNotification() { NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); String text = getString(R.string.service_notif_title); // Note: Notification is marked as deprecated -- in API 11+ there's a new Builder class // but we need to have API 7 compatibility so we ignore that warning. Notification n = new Notification(R.drawable.ic_launcher, text, System.currentTimeMillis()); n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pi = PendingIntent.getActivity( this, //context 0, //requestCode intent, //intent 0 // pending intent flags ); n.setLatestEventInfo(this, text, text, pi); nm.notify(NOTIF_ID, n); } private void removeNotification() { NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(NOTIF_ID); } }