/*
* 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);
}
}