#14652: export CallManager services to sflphone-service,
        bind service from home activity
        separate service thread from UI

Signed-off-by: Emeric Vigier <emeric.vigier@savoirfairelinux.com>
diff --git a/src/com/savoirfairelinux/sflphone/client/Manager.java b/src/com/savoirfairelinux/sflphone/client/Manager.java
index 0a7b6c9..85bb795 100644
--- a/src/com/savoirfairelinux/sflphone/client/Manager.java
+++ b/src/com/savoirfairelinux/sflphone/client/Manager.java
@@ -24,23 +24,29 @@
 	static SFLPhoneHome uiThread;
 	static ImageButton buttonCall;
 	public static ManagerImpl managerImpl;
-	public CallManagerJNI callmanagerJNI;
+	public static CallManagerJNI callmanagerJNI;
 	public CallManagerCallBack callManagerCallBack;
-	
+
 	static {
+	    /* return unique instance */
 	    managerImpl = SFLPhoneservice.instance();
-		Log.i(TAG, "ManagerImpl::instance() = " + managerImpl);
+	    Log.i(TAG, "ManagerImpl::instance() = " + managerImpl);
+	    callmanagerJNI = new CallManagerJNI();
 	}
 
-	public Manager() {}
-	
+	public Manager() {
+	    Log.i(TAG, "Manager.callManagerJNI = " + callmanagerJNI);
+	    callManagerCallBack = new CallManagerCallBack();
+	    SFLPhoneservice.setCallbackObject(callManagerCallBack);
+	    Log.i(TAG, "callManagerCallBack = " + callManagerCallBack);
+	}
+
 	public Manager(Handler h) {
-	    
-		Manager.handler = h;
-		callmanagerJNI = new CallManagerJNI();
-		callManagerCallBack = new CallManagerCallBack();
-		SFLPhoneservice.setCallbackObject(callManagerCallBack);
-        Log.i(TAG, "callManagerCallBack = " + callManagerCallBack);
+	    Manager.handler = h;
+	    Log.i(TAG, "Manager.callManagerJNI = " + callmanagerJNI);
+	    callManagerCallBack = new CallManagerCallBack();
+	    SFLPhoneservice.setCallbackObject(callManagerCallBack);
+	    Log.i(TAG, "callManagerCallBack = " + callManagerCallBack);
 	}
 	
 	public static void callBack(String s) {
diff --git a/src/com/savoirfairelinux/sflphone/client/SFLPhoneHome.java b/src/com/savoirfairelinux/sflphone/client/SFLPhoneHome.java
index 19bf392..1b2aaac 100644
--- a/src/com/savoirfairelinux/sflphone/client/SFLPhoneHome.java
+++ b/src/com/savoirfairelinux/sflphone/client/SFLPhoneHome.java
@@ -38,13 +38,15 @@
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.ServiceConnection;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Message;
+import android.os.RemoteException;
 import android.support.v13.app.FragmentStatePagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.util.Log;
@@ -64,7 +66,7 @@
 import android.widget.TextView;
 
 import com.savoirfairelinux.sflphone.R;
-import com.savoirfairelinux.sflphone.service.ServiceConstants;
+import com.savoirfairelinux.sflphone.service.ISipService;
 import com.savoirfairelinux.sflphone.service.SipService;
 
 public class SFLPhoneHome extends Activity implements ActionBar.TabListener, OnClickListener
@@ -73,7 +75,6 @@
     static final String TAG = "SFLPhoneHome";
     private ButtonSectionFragment buttonFragment;
     Handler callbackHandler;
-    private Manager manager;
     /* default callID */
     static String callID = "007";
     static boolean callOnGoing = false;
@@ -85,6 +86,8 @@
     static Animation animation;
     ContactListFragment mContactListFragment;
     CallElementList mCallElementList;
+    private boolean mBound = false;
+    private ISipService service;
 
     /**
      * The {@link ViewPager} that will host the section contents.
@@ -160,64 +163,80 @@
         animation.setRepeatCount(Animation.INFINITE);
         // Reverse
         animation.setRepeatMode(Animation.REVERSE);
-    }
 
-    // FIXME
-    static {
-        System.loadLibrary("gnustl_shared");
-        System.loadLibrary("expat");
-        System.loadLibrary("yaml");
-        System.loadLibrary("ccgnu2");
-        System.loadLibrary("crypto");
-        System.loadLibrary("ssl");
-        System.loadLibrary("ccrtp1");
-        System.loadLibrary("dbus");
-        System.loadLibrary("dbus-c++-1");
-        System.loadLibrary("samplerate");
-        System.loadLibrary("codec_ulaw");
-        System.loadLibrary("codec_alaw");
-        System.loadLibrary("speexresampler");
-        System.loadLibrary("sflphone");
+        /* startService() can be called any number of times without harm */
+        Log.i(TAG, "starting SipService");
+        startSipService();
     }
 
     @Override
     protected void onStart() {
         Log.i(TAG, "onStart");
         super.onStart();
+        // Bind to LocalService
+        if (!mBound) {
+            Log.d(TAG, "Binding service...");
+            Intent intent = new Intent(this, SipService.class);
+            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        }
     }
 
+    /* user gets back to the activity, e.g. through task manager */
     @Override
     protected void onRestart() {
         super.onRestart();
     }
 
+    /* activity gets back to the foreground and user input */
     @Override
     protected void onResume() {
         Log.i(TAG, "onResume");
         super.onResume();
-
-        Log.i(TAG, "starting SipService");
-        startSipService();
     }
 
+    /* activity no more in foreground */
     @Override
     protected void onPause() {
-        /* stop the service, no need to check if it is running */
-        stopService(new Intent(this, SipService.class));
-        serviceIsOn = false;
         super.onPause();
     }
 
+    /* activity is no longer visible */
     @Override
     protected void onStop() {
         super.onStop();
+        /* stop the service, if no other bound user, no need to check if it is running */
+        if (mBound) {
+            Log.d(TAG, "Unbinding service...");
+            unbindService(mConnection);
+            mBound = false;
+        }
     }
 
+    /* activity finishes itself or is being killed by the system */
     @Override
     protected void onDestroy() {
+        serviceIsOn = false;
         super.onDestroy();
     }
 
+    /** Defines callbacks for service binding, passed to bindService() */
+    private ServiceConnection mConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName className,
+                IBinder binder) {
+            service = ISipService.Stub.asInterface(binder);
+            mBound = true;
+            Log.d(TAG, "Service connected");
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mBound = false;
+            Log.d(TAG, "Service disconnected");
+        }
+    };
+
     private void startSipService() {
         Thread thread = new Thread("StartSFLphoneService") {
             public void run() {
@@ -252,13 +271,6 @@
                 Log.i(TAG, "handleMessage: " + b.getString("callback_string"));
             }
         };
-
-        manager = new Manager(callbackHandler);
-        Log.i(TAG, "ManagerImpl::instance() = " + Manager.managerImpl);
-        Manager.setActivity(this);
-        /* set static AppPath before calling manager.init */
-        Manager.managerImpl.setPath(getAppPath());
-        Log.i(TAG, "manager created with callbackHandler " + callbackHandler);
     }
 
     @Override
@@ -403,26 +415,12 @@
 		}
 	}
 
-	public String getAppPath() {
-		PackageManager pkgMng = getPackageManager();
-		String pkgName = getPackageName();
-
-		try {
-			PackageInfo pkgInfo = pkgMng.getPackageInfo(pkgName, 0);
-			pkgName = pkgInfo.applicationInfo.dataDir;
-		} catch (NameNotFoundException e) {
-			Log.w(TAG, "Error Package name not found ", e);
-		}
-
-		Log.d(TAG, "Application path: " + pkgName);
-		return pkgName;
-	}
-
 	@Override
     public void onClick(View view)
     {
         buttonService = (Button) findViewById(R.id.buttonService);
-        
+
+        try {
     	switch (view.getId()) {
     	case R.id.buttonCall:
     		TextView textView = (TextView) findViewById(R.id.editAccountID);
@@ -432,8 +430,7 @@
 
     		if (incomingCallID != "") {
     			buttonCall.clearAnimation();
-//    			manager.managerImpl.answerCall(incomingCallID);
-				manager.callmanagerJNI.accept(incomingCallID);
+    			service.accept(incomingCallID);
     			callID = incomingCallID;
     			incomingCallID="";
     			callOnGoing = true;
@@ -450,9 +447,8 @@
 
     				callID = Integer.toString(random.nextInt());
 
-    				Log.d(TAG, "manager.managerImpl.placeCall(" + accountID + ", " + callID + ", " + to + ");");
-//    				manager.managerImpl.outgoingCall(accountID, callID, to);
-    				manager.callmanagerJNI.placeCall(accountID, callID, to);
+    				Log.d(TAG, "service.placeCall(" + accountID + ", " + callID + ", " + to + ");");
+    				service.placeCall(accountID, callID, to);
     				callOnGoing = true;
     				buttonCall.setEnabled(false);
     				buttonHangup.setEnabled(true);
@@ -461,17 +457,16 @@
         	break;
     	case R.id.buttonHangUp:
     		if (incomingCallID != "") {
-    			buttonCall.clearAnimation();
-//    			manager.managerImpl.refuseCall(incomingCallID);
-				manager.callmanagerJNI.refuse(incomingCallID);
-    			incomingCallID="";
+    		    buttonCall.clearAnimation();
+    		    Log.d(TAG, "service.refuse(" + incomingCallID + ");");
+    		    service.refuse(incomingCallID);
+    		    incomingCallID="";
 				buttonCall.setEnabled(true);
 				buttonHangup.setEnabled(true);
     		} else {
     			if (callOnGoing == true) {
-    				Log.d(TAG, "manager.managerImpl.hangUp(" + callID + ");");
-//    				manager.managerImpl.hangupCall(callID);
-    				manager.callmanagerJNI.hangUp(callID);
+    				Log.d(TAG, "service.hangUp(" + callID + ");");
+    				service.hangUp(callID);
     				callOnGoing = false;
     				buttonCall.setEnabled(true);
     				buttonHangup.setEnabled(false);
@@ -481,8 +476,7 @@
 			buttonCall.setImageResource(R.drawable.ic_call);
     		break;
     	case R.id.buttonInit:
-    		Manager.managerImpl.setPath("");
-    		Manager.managerImpl.init("");
+    		Log.i(TAG, "R.id.buttonInit");
     		break;
     	case R.id.buttonService:
     	    if (!serviceIsOn) {
@@ -515,5 +509,8 @@
     		Log.w(TAG, "unknown button " + view.getId());
         	break;
     	}
+        } catch (RemoteException e) {
+            Log.e(TAG, "Cannot call service method", e);
+        }
 	}
 }
diff --git a/src/com/savoirfairelinux/sflphone/client/SFLphoneApplication.java b/src/com/savoirfairelinux/sflphone/client/SFLphoneApplication.java
index 3c15062..1dc2d99 100644
--- a/src/com/savoirfairelinux/sflphone/client/SFLphoneApplication.java
+++ b/src/com/savoirfairelinux/sflphone/client/SFLphoneApplication.java
@@ -1,30 +1,59 @@
 package com.savoirfairelinux.sflphone.client;
 
 import android.app.Application;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.util.Log;
 
+import com.savoirfairelinux.sflphone.service.SipService;
+
 public class SFLphoneApplication extends Application {
-    
+
     static final String TAG = "SFLphoneApplication";
     private boolean serviceRunning;
+    private SipService sipService;
 
     @Override
     public void onCreate() {
         super.onCreate();
         Log.i(TAG, "onCreate");
     }
-    
+
     @Override
     public void onTerminate() {
         super.onTerminate();
         Log.i(TAG, "onTerminate");
     }
-    
+
     public boolean isServiceRunning() {
         return serviceRunning;
     }
-    
+
     public void setServiceRunning(boolean r) {
         this.serviceRunning = r;
     }
+
+    public SipService getSipService() {
+        return sipService;
+    }
+
+    public void setSipService(SipService service) {
+        sipService = service;
+    }
+
+    public String getAppPath() {
+        PackageManager pkgMng = getPackageManager();
+        String pkgName = getPackageName();
+
+        try {
+            PackageInfo pkgInfo = pkgMng.getPackageInfo(pkgName, 0);
+            pkgName = pkgInfo.applicationInfo.dataDir;
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Error Package name not found ", e);
+        }
+
+        Log.d(TAG, "Application path: " + pkgName);
+        return pkgName;
+    }
 }
diff --git a/src/com/savoirfairelinux/sflphone/service/ISipService.aidl b/src/com/savoirfairelinux/sflphone/service/ISipService.aidl
new file mode 100644
index 0000000..cf6e7e5
--- /dev/null
+++ b/src/com/savoirfairelinux/sflphone/service/ISipService.aidl
@@ -0,0 +1,8 @@
+package com.savoirfairelinux.sflphone.service;
+
+interface ISipService {
+    void placeCall(String accountID, in String callID, in String to);
+    void refuse(in String callID);
+    void accept(in String callID);
+    void hangUp(in String callID);
+}
\ No newline at end of file
diff --git a/src/com/savoirfairelinux/sflphone/service/SipService.java b/src/com/savoirfairelinux/sflphone/service/SipService.java
index c371ada..9ba6223 100644
--- a/src/com/savoirfairelinux/sflphone/service/SipService.java
+++ b/src/com/savoirfairelinux/sflphone/service/SipService.java
@@ -1,13 +1,44 @@
+/**
+ * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
+ * Copyright (C) 2004-2012 Savoir-Faire Linux Inc.
+ *
+ *  Author: Regis Montoya <r3gis.3R@gmail.com>
+ *  Author: Emeric Vigier <emeric.vigier@savoirfairelinux.com>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *  If you own a pjsip commercial license you can also redistribute it
+ *  and/or modify it under the terms of the GNU Lesser General Public License
+ *  as an android library.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
 package com.savoirfairelinux.sflphone.service;
 
+import java.lang.ref.WeakReference;
+
 import android.app.Service;
 import android.content.Intent;
 import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
 import android.util.Log;
 import android.widget.Toast;
 
+import com.savoirfairelinux.sflphone.client.Manager;
 import com.savoirfairelinux.sflphone.client.SFLphoneApplication;
+import com.savoirfairelinux.sflphone.service.ISipService;
 
 public class SipService extends Service {
 
@@ -16,16 +47,79 @@
     private boolean runFlag = false;
     private SipServiceThread sipServiceThread;
     private SFLphoneApplication sflphoneApp;
-    private final IBinder mBinder = new LocalBinder();
+    private SipServiceExecutor mExecutor;
+    private static HandlerThread executorThread;
+    private CallManagerJNI callManagerJNI;
+    private Manager manager;
+    private boolean isPjSipStackStarted = false;
+
+    /* Implement public interface for the service */
+    private final ISipService.Stub mBinder = new ISipService.Stub() {
+
+        @Override
+        public void placeCall(final String accountID, final String callID, final String to) {
+            getExecutor().execute(new SipRunnable() {
+                @Override
+                protected void doRun() throws SameThreadException {
+                    Log.i(TAG, "SipService.placeCall() thread running...");
+                    callManagerJNI.placeCall(accountID, callID, to);
+                }
+            });
+        }
+
+        @Override
+        public void refuse(final String callID) {
+            getExecutor().execute(new SipRunnable() {
+                @Override
+                protected void doRun() throws SameThreadException {
+                    Log.i(TAG, "SipService.refuse() thread running...");
+                    callManagerJNI.refuse(callID);
+                }
+            });
+        }
+
+        @Override
+        public void accept(final String callID) {
+            getExecutor().execute(new SipRunnable() {
+                @Override
+                protected void doRun() throws SameThreadException {
+                    Log.i(TAG, "SipService.placeCall() thread running...");
+                    callManagerJNI.accept(callID);
+                }
+            });
+        }
+
+        @Override
+        public void hangUp(final String callID) {
+            getExecutor().execute(new SipRunnable() {
+                @Override
+                protected void doRun() throws SameThreadException {
+                    Log.i(TAG, "SipService.hangUp() thread running...");
+                    callManagerJNI.hangUp(callID);
+                }
+            });
+        }
+    };
+
+    /**
+     * Class used for the client Binder.  Because we know this service always
+     * runs in the same process as its clients, we don't need to deal with IPC.
+     */
+    public class LocalBinder extends Binder {
+        public SipService getService() {
+            // Return this instance of LocalService so clients can call public methods
+            return SipService.this;
+        }
+    }
 
     /* called once by startService() */
     @Override
     public void onCreate() {
         Log.i(TAG, "onCreated");
         super.onCreate();
-        this.sflphoneApp = (SFLphoneApplication) getApplication();
-        this.sipServiceThread = new SipServiceThread();
-        Log.i(TAG, "onCreated");
+        sflphoneApp = (SFLphoneApplication) getApplication();
+        sipServiceThread = new SipServiceThread();
+        getExecutor().execute(new StartRunnable());
     }
 
     /* called for each startService() */
@@ -33,15 +127,12 @@
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log.i(TAG, "onStarted");
         super.onStartCommand(intent, flags, startId);
-//        if(intent != null) {
-//            Parcelable p = intent.getParcelableExtra(ServiceConstants.EXTRA_OUTGOING_ACTIVITY);
-//            Log.i(TAG, "unmarshalled outgoing_activity");
-//        }
-        this.runFlag = true;
-        this.sipServiceThread.start();
-        this.sflphoneApp.setServiceRunning(true);
+
+        runFlag = true;
+        sipServiceThread.start();
+        sflphoneApp.setServiceRunning(true);
         Toast.makeText(this, "Sflphone Service started", Toast.LENGTH_SHORT).show();
-        
+
         Log.i(TAG, "onStarted");
         return START_STICKY; /* started and stopped explicitly */
     }
@@ -50,10 +141,10 @@
     public void onDestroy() {
         /* called once by stopService() */
         super.onDestroy();
-        this.runFlag = false;
-        this.sipServiceThread.interrupt();
-        this.sipServiceThread = null;
-        this.sflphoneApp.setServiceRunning(false);
+        runFlag = false;
+        sipServiceThread.interrupt();
+        sipServiceThread = null;
+        sflphoneApp.setServiceRunning(false);
         Toast.makeText(this, "Sflphone Service stopped", Toast.LENGTH_SHORT).show();
         
         Log.i(TAG, "onDestroyed");
@@ -65,14 +156,120 @@
         return mBinder;
     }
 
-    /**
-     * Class used for the client Binder.  Because we know this service always
-     * runs in the same process as its clients, we don't need to deal with IPC.
-     */
-    public class LocalBinder extends Binder {
-        SipService getService() {
-            // Return this instance of LocalService so clients can call public methods
-            return SipService.this;
+    private static Looper createLooper() {
+        if(executorThread == null) {
+            Log.d(TAG, "Creating new handler thread");
+            // ADT gives a fake warning due to bad parse rule.
+            executorThread = new HandlerThread("SipService.Executor");
+            executorThread.start();
+        }
+        return executorThread.getLooper();
+    }
+
+    public SipServiceExecutor getExecutor() {
+        // create mExecutor lazily
+        if (mExecutor == null) {
+            mExecutor = new SipServiceExecutor(this);
+        }
+        return mExecutor;
+    }
+
+    // Executes immediate tasks in a single executorThread.
+    public static class SipServiceExecutor extends Handler {
+        WeakReference<SipService> handlerService;
+
+        SipServiceExecutor(SipService s) {
+            super(createLooper());
+            handlerService = new WeakReference<SipService>(s);
+        }
+
+        public void execute(Runnable task) {
+            // TODO: add wakelock
+            Message.obtain(this, 0/* don't care */, task).sendToTarget();
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.obj instanceof Runnable) {
+                executeInternal((Runnable) msg.obj);
+            } else {
+                Log.w(TAG, "can't handle msg: " + msg);
+            }
+        }
+
+        private void executeInternal(Runnable task) {
+            try {
+                task.run();
+            } catch (Throwable t) {
+                Log.e(TAG, "run task: " + task, t);
+            }
+        }
+    }
+
+    private void startPjSipStack() throws SameThreadException {
+        if (isPjSipStackStarted)
+            return;
+
+        try {
+            System.loadLibrary("gnustl_shared");
+            System.loadLibrary("expat");
+            System.loadLibrary("yaml");
+            System.loadLibrary("ccgnu2");
+            System.loadLibrary("crypto");
+            System.loadLibrary("ssl");
+            System.loadLibrary("ccrtp1");
+            System.loadLibrary("dbus");
+            System.loadLibrary("dbus-c++-1");
+            System.loadLibrary("samplerate");
+            System.loadLibrary("codec_ulaw");
+            System.loadLibrary("codec_alaw");
+            System.loadLibrary("speexresampler");
+            System.loadLibrary("sflphone");
+            isPjSipStackStarted = true;
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "Problem with the current Pj stack...", e);
+            isPjSipStackStarted = false;
+            return;
+        } catch (Exception e) {
+            Log.e(TAG, "Problem with the current Pj stack...", e);
+        }
+
+        manager = new Manager();
+        Log.i(TAG, "SipService.ManagerImpl::instance() = " + Manager.managerImpl);
+        /* set static AppPath before calling manager.init */
+        Manager.managerImpl.setPath(sflphoneApp.getAppPath());
+        callManagerJNI = Manager.callmanagerJNI;
+        Log.i(TAG, "startPjSipStack() callManagerJNI = " + callManagerJNI);
+
+        Manager.managerImpl.init("");
+        return;
+    }
+
+    // Enforce same thread contract to ensure we do not call from somewhere else
+    public class SameThreadException extends Exception {
+        private static final long serialVersionUID = -905639124232613768L;
+
+        public SameThreadException() {
+            super("Should be launched from a single worker thread");
+        }
+    }
+
+    public abstract static class SipRunnable implements Runnable {
+        protected abstract void doRun() throws SameThreadException;
+
+        public void run() {
+            try {
+                doRun();
+            }catch(SameThreadException e) {
+                Log.e(TAG, "Not done from same thread");
+            }
+        }
+    }
+
+    class StartRunnable extends SipRunnable {
+        @Override
+        protected void doRun() throws SameThreadException {
+            startPjSipStack();
         }
     }