AsyncTaskを使ったIllegalArgumentExceptionを防いだダイアログ表示



ボタンをクリックすると、次のスクリーンショットのようにプログレスバー付きの ProgressDialog が表示さるプログラムのソースコードです。

undefined

非同期処理であるAsyncTask を利用していますが、ネットのサンプルだと

などでエラーが起きたり好ましくない動作をしていたので、その辺りを修正してみました。

スポンサードリンク

ダイアログの表示

    // Progress Dialog を表示
    public void showDialog() {
        mDialog = new ProgressDialog(mContext);
        mDialog.setTitle("Please wait");
        mDialog.setMessage("Downloading data...");
        mDialog.setCancelable(true);
        mDialog.setCanceledOnTouchOutside(false);  
        mDialog.setIndeterminate(false);
        mDialog.setProgressNumberFormat("%1$s / %2$s bytes");
        mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mDialog.setMax(0);
        mDialog.setProgress(0);
        mDialog.show();
    }

    // Progress Dialog を消す  
    private void dismissDialog() {
        mDialog.dismiss();
        mDialog = null;
    }

ProgressDialogの代表的なメソッドは、次のとおりです。

メソッド 説明
setTitle タイトルを設定する
setMessage メッセージを設定する
setProgressNumberFormat進捗表示のフォーマットを設定する
setProgressStyle スタイルを設定する(STYLE_HORIZONTAL:水平スタイル、STYLE_SPINNER:円スタイル)
setCancelable プログレスダイアログのキャンセルが可能かどうかを設定
setCanceledOnTouchOutsideダイアログの外部をタッチしてダイアログを閉じる(true)/閉じない(false)を設定
setIndeterminate プログレスダイアログの確定(false)/不確定(true)を設定
setProgress 開始値を設定する
setMax 最大値を設定する
setButton プログレスダイアログにボダンを設置する
show プログレスダイアログを表示する
dismiss プログレスダイアログを閉じる

AsyncTaskを利用した非同期処理

処理をキャンセルする場合は、 AsyncTask の cancel メソッドを呼びます。

すると isCancelled が true になるとか、今回の例のように sleep が InterruptedException となるので、 適切な終了処理をした後で doInBackground を抜けることができます。

キャンセルした状態で doInBackground を抜けると onPostExecute ではなく、 onCancelled が呼ばれます。

メソッド名 内容
onPreExecute() 事前準備の処理
doInBackground(Params...) バックグラウンドで行う処理
onProgressUpdate(Progress...) 進捗状況をUIスレッドで表示する処理
onPostExecute(Result) バックグラウンド処理が完了し、UIスレッドに反映する処理

AsyncTaskは使い捨て

ProgressDialog がキャンセルイベントを捕まえたところで、 AsyncTask の cancel を呼び出します。

New を行わないで、インスタンスを使いまわすと 次のような IllegalStateException となります。

Cannot execute task: the task has already been executed (a task can be executed only once)

タスクは1回しか実行できないので、毎回インスタンスを生成する必要があります。

IllegalArgumentException 対策

AsyncTaskの処理が終了した時点でIllegalArgumentExceptionが発生してしまいます。

これはダイアログの dismissメソッドを呼び出す時に、ダイアログを表示したアクティビティが破棄されているのが原因です。

E/WindowManager(16629): android.view.WindowLeaked: Activity org.example.demomode.DemoMode has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{41c43ef0 V.E..... R.....I. 0,0-310,215} that was originally added here
E/WindowManager(16629):         at android.view.ViewRootImpl.(ViewRootImpl.java:354)
E/WindowManager(16629):         at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:216)
E/WindowManager(16629):         at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
E/WindowManager(16629):         at android.app.Dialog.show(Dialog.java:281)
E/WindowManager(16629):         at org.example.demomode.AsyncDemoTask.showDialog(AsyncDemoTask.java:43)
E/WindowManager(16629):         at org.example.demomode.AsyncDemoTask.onPreExecute(AsyncDemoTask.java:50)
E/WindowManager(16629):         at android.os.AsyncTask.executeOnExecutor(AsyncTask.java:586)
E/WindowManager(16629):         at android.os.AsyncTask.execute(AsyncTask.java:534)
E/WindowManager(16629):         at org.example.demomode.DemoMode$1.onClick(DemoMode.java:37)
E/WindowManager(16629):         at android.view.View.performClick(View.java:4202)
E/WindowManager(16629):         at android.view.View$PerformClick.run(View.java:17340)

対策方法は次のようになります。

画面の回転時にはActivityが再起動(onDestroy~onCreate)します。

上記対応により、ダイアログ表示したままでActivityが再起動しても、Activityと一緒に自動的にダイアログも再表示されます。

ホームボタン押、他のアプリが起動した場合の対策

ホームボタンを押してアプリがバックグラウンドに回る時や、他のアプリに遷移した時は処理を終了することなく、処理を継続させます。

それには「onUserLeaveHint」を呼び出します。

    @Override
    public void onUserLeaveHint() {
        if (mTask != null && mTask.isInProcess()) {
            Log.v("", "dismissDialog called");
            mTask.dismissDialog();
        }
        // トーストを表示する
        Toast.makeText(getApplicationContext(), "Good bye" , Toast.LENGTH_SHORT).show();
    }

動作確認のためにトースト表示も行なっています。

サンプルソースコード

サンプルソースコードを載せておきます。

package org.example.demomode;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.KeyEvent; 
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;

public class DemoMode extends Activity {

    // AsyncDemoTaskオブジェクトをstatic変数で保持
    private static AsyncDemoTask mTask = null;

    // 存在すれば onCreate, onStart, onResumeの順番で呼ばれる
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.v("", "onCreate called");
        super.onCreate(savedInstanceState);
        // ウィンドウタイトルバーを非表示にする
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.main);

        findViewById(R.id.button_id).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               Log.v("", "mTask.execute called");
               if (mTask != null) {
                   mTask.cancel(true);
               }
               // プログレスダイアログを表示
               mTask = new AsyncDemoTask (DemoMode.this);
               mTask.execute();
            }
        });
    }

    // 存在すれば onCreate, onStart, onResumeの順番で呼ばれる
    @Override
    public void onResume(){
        super.onResume();
        Log.v("", "onResume called");
        // プログレスダイアログの表示開始
        if (mTask != null && mTask.isInProcess()) {
            Log.v("", "showDialog called");
            // プログレスダイアログの再表示
            mTask.showDialog();
        }
    }

    // 戻るボタンが押された
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if(keyCode == KeyEvent.KEYCODE_BACK && mTask != null) {
            Log.v("", "keyCode == KeyEvent.KEYCODE_BACK");
            // AsyncTaskのインスタンは使い回しできないので破棄
            mTask.cancel(true);
            mTask = null;
        }
        return super.onKeyDown(keyCode, event);
    }

    //ホームボタン押、他のアプリが起動した時に呼ばれる
    @Override
    public void onUserLeaveHint() {
        if (mTask != null) {
            Log.v("", "dismissDialog called");
            // プログレスダイアログを閉じる(2重表示防止)
            mTask.dismissDialog();
            // 処理は継続するのでインスタンスの破棄はしない
            // mTask = null;
        }
        // トーストを表示する
        Toast.makeText(this, "Good bye" , Toast.LENGTH_SHORT).show();
    }

}

AsyncTask を使ったProgressDialog表示のソースコードを紹介します。

package org.example.demomode;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.SystemClock;  
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;


public class AsyncDemoTask extends AsyncTask  {
    
    private ProgressDialog mDialog = null;
    private Context mContext = null;
    // onPreExecute ~ onPostExecute までの判別フラグ
    private boolean isInProgress = false;

    // インスタンス生成
    public AsyncDemoTask(Context context) {
        mContext = context;
    }

    // プログレスダイアログを閉じる
    public void dismissDialog() {
        if (mDialog !=  null) {
           mDialog.dismiss();
        }
        mDialog = null;
    }

    // Progress Dialog を表示
    public void showDialog() {
        mDialog = new ProgressDialog(mContext);
        mDialog.setTitle("Please wait");
        mDialog.setMessage("Downloading data...");
        mDialog.setCancelable(true);
        mDialog.setIndeterminate(false);
        // ダイアログの外部をタッチしてもダイアログを閉じない
        mDialog.setCanceledOnTouchOutside(false);  
        mDialog.setProgressNumberFormat("%1$s / %2$s bytes");
        mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mDialog.setMax(0);
        mDialog.setProgress(0);
        mDialog.show();
    }


    // 事前準備の処理
    @Override
    public void onPreExecute() {
        isInProgress = true;
        showDialog();
    }

    // バックグラウンドで行う処理
    @Override
    public Integer doInBackground(Integer...ARGS) {

        for (int i=0; i<10; i++) {  
            SystemClock.sleep(1000);  
            // キャンセルが押された場合
            if (isCancelled() ) {
                return 0;
            }
            publishProgress((i+1) * 10);
        }
        return 1;
    }

    // 進捗状況をUIスレッドで表示する処理
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        if (null != mDialog ) {
           mDialog.setProgress(values[0]);
        }
    }

    // キャンセル処理
    @Override
    public void onCancelled(Integer result) {
        Log.v("", "onCancel() called");
        if (mDialog !=  null && mDialog.isShowing() ) {
            dismissDialog();
        }
        super.onCancelled();
    }

    // バックグラウンド処理が完了し、UIスレッドに反映する処理
    @Override
    public void onPostExecute(Integer result) {
        Log.v("", "onPostExecute() called");
        // ProgressDialog の削除
        if (mDialog !=  null) {
            dismissDialog();
        }
        // AsyncTaskの終了
        isInProgress = false;
        Toast.makeText(mContext, "Download Finished!" , Toast.LENGTH_SHORT).show();
    }

    // 
    public synchronized boolean isInProcess() { return isInProgress; }
}

main.xml は次のようになっています。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
	    <Button
	    android:id="@+id/button_id"
	    android:layout_height="wrap_content" 
	    android:layout_width="wrap_content"     
	    android:text="@string/button_id"
	    />

</LinearLayout>

スポンサードリンク