Home | History | Annotate | Download | only in service
      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