Thursday, May 24, 2012

Android System Service/API 實作

最近有在碰system service的部分,身為一個系統開發者,學會刷機的主要目的當然就是要自己新增一些系統API或系統服務,這篇文章希望可以幫助初學者.

為什麼要開發系統服務(system service)或Java API ?

有時候我們會需要修改手機作業系統的運作邏輯,甚至是取得只有system有權限可以存取的資訊,在這篇文章中我們就以一個實際的例子來說明我們要做的事情. 手機APP的啟動總是可能會花費許多的等待時間,這段時間我們稱之為launch time, 根據現有的Android API, 這個資訊是無從取得的,我們就把目標放在launch time上, 從系統中擷取每一隻APP的launch time並記錄在一隻system service中, 並且讓APP開發者能夠透過一套新的API與此service溝通並取得任一隻APP最近一次的launch time.

第一章 system service的撰寫與編譯

在這段裡面,我們撰寫一支空的service,並且在其建構子中印出一行log訊息以確定是否正常被執行.

/*
 *   framework/base/services/java/com/android/server/MyService.java
 */

package com.android.server;

import android.util.Log;
import android.os.IMyManager;

class MyService extends IMyManager.Stub
{
        final String TAG = "MyService";

        public MyService()
        {
                Log.i(TAG,"MyService is constructed!");
        }

}

為了要讓這個service註冊進開機流程中,我們必須在新增幾行指令在以下檔案:

/*
 *   framework/base/services/java/com/android/server/SystemServer.java
 */

power = new PowerManagerService();
ServiceManager.addService(Context.POWER_SERVICE, power);

//Add two lines
MyService myservice = new MyService();
ServiceManager.addService("myservice", myservice);


其中myservice字串代表這個系統服務的代號,未來要使用到此service都會使用到這個字串,為了避免未來混淆,建議可以在framework/base/core/java/android/content/Context.java定義一個此字串對應的值.

此外,手動新增一個檔案,IMyManager.aidl讓MyService可以extends,至於此aidl用途後續會敘述.為了讓此aidl檔案能夠被編譯,在makefile中加入一行:

/*
 *  framework/base/core/java/android/os/IMyManager.aidl
 */

package android.os;

interface IMyManager
{

}

/*
 *  framework/base/Android.mk
 */

core/java/android/os/IPowerManager.aidl \

#Add the following one line:

core/java/android/os/IMyManager.aidl \

到此為止第一步驟完成了,你已經完成了一隻system service,接下來照以下步驟就可以編譯出系統映像檔(system image),把此映像檔用模擬器啟動即可在ddms中看到你的service在被初始化時所產生的訊息了:
~/android$ . build/envsetup.sh
~/android$ lunch full-eng
~/android$ make -j4

第二章 ServiceManager的API實作與編譯

接下來我們先修改一下MyService讓他能夠提供一些簡單的運算服務,以一個最簡單的例子,我們希望這個service可以提供app一個加法的服務,讓app可以透過一套API來存取此加法服務,以下為修改過的MyService.java

/*
 *  framework/base/services/java/com/android/server/MyService.java
 */

package com.android.server;

import android.util.Log;
import android.os.IMyManager;

class MyService extends IMyManager.Stub
{
        final String TAG = "MyService";

        public MyService()
        {
                Log.i(TAG,"MyService is constructed!");
        }

        //A functionality of the service
        public int add(int a, int b)
        {
                Log.i(TAG,"MyService performs the add()");
                return a+b;
        }
}

由於app本身無法直接跟system service傳遞資料,因此我們需要透過Android Interface Defination Language來作為兩個processes之間溝通的介面(Inter-Process Communication),詳細的IPC機制是透過IBinder來實作的,但這裡我就簡單帶過了.

首先修改上一步驟中幾乎為空的IMyManager.aidl:
/*
 *  framework/base/core/java/android/os/IMyManager.aidl
 */

package android.os;

interface IMyManager
{
    int add(int a, int b);
}

為了讓app開發者可以很輕鬆的呼叫這個介面上所定義的函式,我們需要提供一個service manager讓app開發者可以import,因此新增以下檔案:
/*
 *  framework/base/core/java/android/os/MyManager.java
 */

package android.os;

import android.util.Log;

public class MyManager
{
        final String TAG = "MyManger";
        private IMyManager mService;

        public MyManager(IMyManager service)
        {
                mService = service;
        }

        public int getSum(int a, int b)
        {
                try{
                        Log.i(TAG, "Try to call the add function of MyService...");
                        return  mService.add(a,b);
                }
                catch(RemoteException e)
                {
                        e.printStackTrace();
                        return 0;
                }
        }

}

MyManager提供app開發者一套API,讓開發者可以直接透過此manager來呼叫遠端service的sum函式,以此程式為例子,app透過MyManager的getSum API直接呼叫MyService的sum功能.

然而,此manager尚未定義其被建構之處,以及如何取得IMyManager的reference,因此我們還需要註冊此Manager讓他能夠透過AIDL與service來溝通,修改如下檔案:

/*
 *  framework/base/core/java/android/app/ContextImpl.java
 */

//add the packages

import android.os.IMyManager;
import android.os.MyManager;

registerService(POWER_SERVICE, new ServiceFetcher() {
    public Object createService(ContextImpl ctx) {
        IBinder b = ServiceManager.getService(POWER_SERVICE);
        IPowerManager service = IPowerManager.Stub.asInterface(b);
        return new PowerManager(service, ctx.mMainThread.getHandler());
        }});

//Add the following codes

registerService("myservice", new ServiceFetcher() {
    public Object createService(ContextImpl ctx) {
        IBinder b = ServiceManager.getService("myservice");
        IMyManager service = IMyManager.Stub.asInterface(b);
        return new MyManager(service);
        }});

到此為止已經成功把service manager的API新增了,接下來就要編譯出兩樣產出,分別是模擬器用的系統映像檔,以及修改過API的android.jar,透過以下指令來編譯:

~/android$ . build/envsetup.sh
~/android$ make update-api
~/android$ lunch sdk-eng
~/android$ make sdk -j4


這裡有幾點需要說明的,由於我們有動到framework的API,因此編譯之前必須先下make update-api, 此外,在4.0以後若是要編譯SDK,則必須透過新的lunch來達成(這點讓我相當不習慣,舊版直接下make sdk就可以了),經過make sdk後,android.jar和system.img都會被編譯產生!!但建議sdk編譯出來後就改回用make -j4了,避免odex開機失敗,要make兩次

編譯完成後,會產生out/host/linux-x86/sdk/android-sdk/platforms/android-4.0/android.jar,我們將可以替換舊的android.jar,如此一來便能在APP中呼叫MyManager並透過他向MyService進行呼叫,以下為一個Android APP,呼叫了我們定義的加法函式:


package cs.nthu;

import android.util.Log;
import android.os.MyManager;//Our inserted package
import android.app.Activity;
import android.os.Bundle;

public class MyApp extends Activity {
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        //Access the MyManager
        MyManager mm = (MyManager)this.getSystemService("myservice");
        
        //Call MyService through MyManager
        Log.i("MyApp", "1+3=" + mm.getSum(1, 3));
       
    }
}


第三章  郵包(parcel)與IPC

到目前為止,我們已經擁有了撰寫基本系統服務的能力了,但是只會這樣其實還不夠,由於Android採用郵包來傳遞不同process之間的Java物件,只有實作了(implements)可郵包化(parcelable)的物件才可以定義在AIDL當中,亦即只有可郵包化的物件才能夠進行IPC.然而我們在實作一個service時,往往有必要傳遞一些自定義型別的物件,因此接下來的練習中,我們將示範如何實作一個可郵包化的類別.

首先我們先訂出一個MyType,並且會讓此型別的物件在AIDL中作為參數傳遞的型別:

/*
 *  framework/base/core/java/android/os/MyType.java
 */

package android.os;

public class MyType
{
        private int value = 0;

        public MyType(int n)
        {
                value = n;
        }

        public int getValue()
        {
                return value;
        }

        public void setValue(int n)
        {
                value = n;
        }
}

接下來我們在MyService當中新增一個函式addMyType, 在此函式中會將兩個自訂類別的物件的value相加,並回傳一個加總後的自訂類別物件:

/*
 *  framework/base/services/java/com/android/server/MyService.java
 */

package com.android.server;

import android.util.Log;
import android.os.IMyManager;
import android.os.MyType;

class MyService extends IMyManager.Stub
{
        final String TAG = "MyService";

        public MyService()
        {
                Log.i(TAG,"MyService is constructed!");
        }

        //A functionality of the service
        public int add(int a, int b)
        {
                Log.i(TAG,"MyService performs the add()");
                return a+b;
        }

        //Self-defined type functions
        public MyType addMyType(MyType a, MyType b)
        {
                Log.i(TAG, "MyService performs the addMyType()");
                return  new MyType(a.getValue() + b.getValue());
        }
}

為了可以遠端呼叫此函式,我們當然要修改AIDL讓mananger可以看的見此函式:

/*
 *  framework/base/core/java/android/os/IMyManager.aidl
 */

package android.os;

import android.os.MyType;

interface IMyManager
{
    int add(int a, int b);
    MyType addMyType(in MyType a, in MyType b);
}

再傳入參數的地方有一個in,這個意思代表說傳入的物件不需要回傳,相對的如果設定成out的話,代表此物件只須被傳出不需傳入,如果要同時傳入傳出的話就要用inout.(這個地方很重要,像這個例子其實可以把a或b設定成inout,然後就把a+b後的值透過a回傳)

接下來再service manager當中新增此函式的API,以便讓APP開發者可以呼叫此函式:
/*
 *  framework/base/core/java/android/os/MyManager.java
 */


public MyType getMyTypeSum(MyType a, MyType b)
{
      try{
            Log.i(TAG, "Try to call the addMyType function of MyService...");
                        return mService.addMyType(a,b);
      }
      catch(RemoteException e)
      {
            e.printStackTrace();
            return new MyType(0);
      }
}

到目前為止看似很順利,但當你實際編譯的時候會出錯原因就在於MyType這個型別並未implements parcelable,所以是無法順利完成IPC的,因此我們新增幾個method於MyType當中:

/*
 *  framework/base/core/java/android/os/MyType.java
 */

package android.os;

import android.os.Parcelable;

public class MyType implements Parcelable
{
        private int value = 0;

        public MyType(int n)
        {
                value = n;
        }

        public int getValue()
        {
                return value;
        }

        public void setValue(int n)
        {
                value = n;
        }

        //Implements the "unpacking" procedure
        public static final Parcelable.Creator<MyType> CREATOR
            = new Parcelable.Creator<MyType>(){

                public MyType createFromParcel(Parcel in){
                        return new MyType(in);
                }
                public MyType[] newArray(int size){
                        return new MyType[size];
                }

        };
        //Implements the "packing" procedure
        public void writeToParcel(Parcel out, int flags)
        {
                out.writeInt(value);
        }

        //Implements the private constructor for "unpacking"
        private MyType(Parcel in)
        {
                value = in.readInt();
        }

        public int describeContents(){
                return 0;
        }
}

後面新增的四個method都是為了要implements parcelable所增加的,主要是透過parcel.write和parcel.read來寫入(讀取)郵包,藉以達成IPC的需求,這裡要特別注意的是讀寫郵包的時候,先寫入的要先讀(FIFO),這個例子只有包入一個int,實際上各種primarily type都是可以直接讀寫郵包的.

最後為了要讓IMyManager.aidl可以認識我們自定義的類別,所以必須額外寫一個檔案讓他辨認:

/*
 *  framework/base/core/java/android/os/MyType.aidl
 */

package android.os;
parcelable MyType;

到目前為止已經完成了新的函式功能,這個章節最後我們就以一個APP來做結尾吧:

package cs.nthu;

import android.util.Log;
import android.app.Activity;
import android.os.Bundle;
//Our inserted package
import android.os.MyManager;
import android.os.MyType;

public class MyApp extends Activity {
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        //Access the MyManager
        MyManager mm = (MyManager)this.getSystemService("myservice");
      
        MyType ret = mm.getMyTypeSum(new MyType(1), new MyType(3));
        
        Log.i("MyApp", "MyType(1) + MyType(3) = " + ret.getValue());
        
    }
}


第四章 範例: 實作launch time 服務 (待補)