Android Studio 0.4.0 でAIDLを使ってサービスからUIを非同期更新

バックグラウンド(無限ループ)で重い処理が動き続ける状態で
それにあわせてUIを更新したいと思いました。

  • Activity内でThreadを使うとUIが固まってしまう
  • AsyncTaskは便利だけれどTaskが使い捨てなのが利用を悩む
  • Messengerは非同期では無さそう。

勉強不足なのは重々承知なので誤解は多々あると思いますがご容赦下さい。

AIDLを用いたコールバックでUIを非同期に更新にチャレンジ

環境

OS Windows8Pro 64bit
Java Oracle Java SE 1.7.0u45
IDE Android Studio 0.4.0
Android SDK r22.3
Gradle 1.9

ファイルリスト

以下のファイルを生成・編集します。

  • IMyService.aidl
  • IMyCallback.aidl
  • MyService.java
  • fragment_main.xml
  • MainActivity.java
  • AndroidManifest.xml

Android Studio 0.4.0 でウィザードが使えるところは使っています。


新規プロジェクトの作成


ウィザードをすすめます


デモなのでシンプルなのを選びます


fragment_main.xmlを今回は使います。


gradle でプロジェクトがつくられていきます


プロジェクトが起動した直後


AIDLを格納するフォルダをjavaと同階層につくります


javaと階層を揃えるたpackageをつくります


新規作成でFileを選択


拡張子まで含めて空ファイルを作成


ドロイド君アイコンの空ファイルが出来ます
これでAIDLの記述準備が出来ました。
※コードに色は付きますが入力サポートは受けられません


ServiceはAndroidComponentにあります


ウィザードでServiceを選択します


AndroidManifest.xmlへの登録も完了します
※今回は修正が必要です


AIDLを含めてコンパイルをすると入力サポートが受けられます

IMyService.aidl

package jp.tackn.aidldemo;

import jp.tackn.aidldemo.IMyCallback;

 /**
 * バックグラウンド処理を行うサービス
 * Created by Tackn on 13/12/22.
 */
interface IMyService
 {
    /**
     * コールバック登録。
     * @param callback 登録するコールバック。
     */
    oneway void registerCallback(IMyCallback callback);

    /**
     * コールバック解除。
     * @param callback 解除するコールバック。
     */
    oneway void unregisterCallback(IMyCallback callback);

    /**
     * 非同期の処理スタート
     * @param num 何号線か指定
     */
    void AsyncStart();
}

IMyCallback.aidl

package jp.tackn.aidldemo;

/**
 * UI更新用のコールバック
 * Created by Tackn on 13/12/22.
 */
oneway interface IMyCallback {

    /**
     * バックグラウンド処理
     * @param date UI更新に使うコールバックされる値
     */
    void doInBackground(String date);
}

MyService.java

package jp.tackn.aidldemo;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import java.util.Date;

/**
 * Serviceから非同期コールバックでUIを更新デモ
 */
public class MyService extends Service {
    /** 識別用文字列 */
    private static final String TAG = "AidlDemo";

    /** AIDLのサービス実装 */
    private IMyService.Stub mStub = new IMyService.Stub() {
        /** コールバックリスト */
        private RemoteCallbackList<IMyCallback> mCallbacks = new RemoteCallbackList<IMyCallback>();

        /**
         * コールバック登録
         * @param callback 登録するコールバック。
         * @throws android.os.RemoteException 接続エラー
         */
        @Override
        public void registerCallback(IMyCallback callback) throws RemoteException {
            mCallbacks.register(callback);
        }

        /**
         * コールバック解除
         * @param callback 解除するコールバック。
         * @throws RemoteException 接続エラー
         */
        @Override
        public void unregisterCallback(IMyCallback callback) throws RemoteException {
            mCallbacks.unregister(callback);
        }

        /**
         * 非同期処理開始
         */
        @Override
        public void AsyncStart() throws RemoteException {

            // 無限ループ定義
            new Thread(new Runnable() {

                @Override
                public void run() {
                    while (true){
                        //時間のかかる処理
                        try {
                            Thread.sleep(10000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        //コールバックへ
                        updateUI(new Date().toString());
                    }


                }
             }).start();
        }

        /**
         * コールバックでUIの更新処理を行う
         * @param date 更新用文字列
         */
        private void updateUI(String date){
            int n = mCallbacks.beginBroadcast();
            for (int i = 0; i < n; i++) {
                try {
                    mCallbacks.getBroadcastItem(i).doInBackground(date);
                } catch (RemoteException e) {
                    Log.d(TAG, e.getMessage(), e);
                }
            }
            mCallbacks.finishBroadcast();
        }
    };


    /**
     * コンストラクタ
     */
    public MyService() {
        super();
    }

    /**
     * サービスがバインドされたときのコールバック
     * @param intent 情報コンテナ
     * @return 接続に利用されるサービス
     */
    @Override
    public IBinder onBind(Intent intent) {
        return mStub;
    }
}

fragment_main.xml

findViewById をするためにidを追加しただけです

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="jp.tackn.aidldemo.MainActivity$PlaceholderFragment">

    <TextView
        android:id="@+id/textView"
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

MainActivity.java

メインの実装なのでちょっと長めです

package jp.tackn.aidldemo;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

/**
 * サービスからのコールバックでUI更新デモ
 */
public class MainActivity extends ActionBarActivity {
    /** 識別用文字列 */
    private static final String TAG = "AidlDemo";
    /** 更新対象のUI */
    private TextView textView;
    /** UI更新用 */
    private Handler mHandler;

    /** AIDLで記述されたサービスの参照 */
    private IMyService mService;
    /** AIDLで記述されたコールバックの中身 */
    private IMyCallback mCallback = new IMyCallback.Stub() {
        /**
         * バックグラウンド処理
         * @param date 更新文字列
         */
        @Override
        public void doInBackground(final String date) throws RemoteException {
            mHandler.post(new Runnable() {
                public void run() {
                    Log.d(TAG,"doInBackGroud:"+date);
                    textView.setText(
                            date + "\n" +
                            textView.getText()
                    );
                }
            });

        }


    };

    /**
     * サービスとのコネクション
     */
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        /**
         * サービスが接続された時の処理
         * @param name サービスのクラス名
         * @param service サービスとのバインダ
         */
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = IMyService.Stub.asInterface(service);
            try {
                Log.d(TAG, name.getClassName());
                //コールバックの登録
                mService.registerCallback(mCallback);
                //サービス側のメソッドを実行
                mService.AsyncStart();

            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        /**
         * サービスが切断された時の処理
         * @param name サービスのクラス名
         */
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
        }
    };

    /**
     * アクティビティ生成時の処理
     * サービスの接続処理
     * @param savedInstanceState 保存された状態
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_main);

        mHandler = new Handler();
        textView = (TextView)findViewById(R.id.textView);

        bindService(new Intent(IMyService.class.getName()), mServiceConnection, BIND_AUTO_CREATE);
    }

    /**
     * アクティビティ破棄時の処理
     * サービスの切断処理
     */
    @Override
    protected void onDestroy(){
        unbindService(mServiceConnection);
        super.onDestroy();
    }
}

AndroidManifest.xml

serviceのintent-filterでAIDLへのパスを与えます

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.tackn.aidldemo" >

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="jp.tackn.aidldemo.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name="jp.tackn.aidldemo.MyService"
            android:process=":service" >
            <intent-filter>
                <action android:name="jp.tackn.aidldemo.IMyService" />
            </intent-filter>
        </service>

    </application>

</manifest>

実行

service側のスレッドで実行している10秒毎の呼び出しで、
Activity側のUIが非同期で更新されていきます。