Rubotoを使い、AndroidでAsyncTask + ProgressDialogを使う

Rubotoで AsyncTaskとProgressDialogを使ったところ、いろいろとハマったのでメモ。


■環境

Platform JDK ant Ruby ruboto jruby-jars Device API level
Windows7 x64 1.7.0_25 1.9.1 RubyInstaller 1.9.3-p448 0.13.0 1.7.4 Nexus7 2012 android-17

■生成

ruboto gen app --package com.example.async_task_progress_dialog --target android-17 --with-jruby

■調べたこと

AsyncTaskとProgressDialogを使う

ProgressDialogの表示やキャンセル、キャンセルボタンの実装など、ProgressDialog全般について以下のページが参考になりました。


この時点でのソースは以下のGistです。
ただ、ProgressDialog表示中に端末を回転させた場合、ProgressDialogが消えてしまう不具合があります。

ProgressDialog表示中に端末を回転させた際、ProgressDialogが消えないようにする

以下のページが参考になりました。
なお、dialog#dismissをするイベントはいくつかありましたが、今回はいずれの場合でも通過する「onPause」にて実装しました。
ちなみに、参考ページのうち、後者では「onKeyDown」や「onUserLeaveHint」で実装しています。


他に、回転時にTextViewが初期化されてしまう問題もありました。
回転時にActivityの再構築が行われるようでしたので、以下を参考に、Bundleを介してTextViewの値をやりとりするようにしました。


この時点でのソースは以下のGistです。
この時点でも、ProgressDialog表示中に端末を回転させたときにTextViewの値が更新されないという不具合が残っています。

ProgressDialog表示中に端末を回転させた際、TextViewの値が更新されるようにする

この問題は有名なようで、stackoverflowでも多くのコメントが寄せられていました。


今回は、以下を参考に、API14からサポートされた「ActivityLifecycleCallbacks」を利用してActivityを管理する方法を取りました。


なお、「ActivityLifecycleCallbacks」については、以下が参考になりました。


他には、AndroidManifest.xmlに以下のタグを追加し、再構築を防ぐ方法もあるようです。

android:configChanges="orientation|keyboardHidden"


最終的なソースコードは以下のGistです。他に、AndroidManifest.xmlへの記載も追加する必要があります。
一応、末尾にもソースコード全体を載せておきます。

JRuby関連

Javaのクラスにある引数なしのコンストラクタに加え、JRuby側で引数ありコンストラクタオーバーロードを記述する

以下を参考に「super()」としました。なお、「super」とカッコなしで呼び出すとエラーとなります。

JavaのインタフェースをJRubyから利用する

現在のJRubyのバージョンでは、include の記載も不要で、メソッドを定義してあげれば良いようです。

JRubyでのキャスト

一時期、JRuby側でApplicationを継承したAsyncHelperApplicationを実装しようと考えてたため、AsyncHelperApplicationへのキャストする場合はどうすればよいか悩みました。
その時に調べた結果のリンクを残しておきます。


なお、最終的には、AndroidManifest.xmlへの記載が必要なことからJavaのコードである必要があると分かり、JRubyで実装することは諦めました。
さらに、JRuby側でうまいこと型変換をしてくれるので、キャストは基本不要だというところに落ち着きました。



Javaのsynchronizedに相当する、Rubyのメソッドなど

AsyncHelperApplicationを読むとsynchronizedを使っていたので、その時に調べたMutexやMonitorのリンクを残します。上記の通り、結局使うことはありませんでした。

■悩んだこと

doInBackgroundで例外をrescueしないと、onCancelledが発生しなかった

最初は手を抜いてrescueしていなかったため、onCancelledが発生しないのはなぜだろうと思いましたが、rescueしたら発生しました。手抜きはよくない。



ProgressDialogが存在するかどうかを判定するためのメソッドで使われる、bool変数の名前

メソッド名は末尾に「?」を付ける「showing_dialog?」としました。
一方、そのメソッドの中で使うbool変数の名前は、

のどちらがいいのかを調べてみましたが、よく分かりませんでした。
現時点では前者を使い「is_showing」としましたが、一般的にはどうするのだろう?



■最終的なソースコード

# ver 0.1
# バックグラウンドへ回った時でもProgressDialogが動作するバージョン
# ただし、端末を回転させると、うまく動作しない

# ver 0.2
# 端末を回転させても動作する
# ただし、ProgressDialogが表示されている時に回転させると、うまく動作しない

# ver 0.3
# ProgressDialogが表示されている時に回転させても動作する
# 以下を参考にActivityManager.javaとAsyncHelperApplication.javaを実装し、組み込む
# (AndroidManifest.xml への追記も忘れずに)
# http://blog.kotemaru.org/2013/02/android-asynctask-orientation.html


require 'ruboto/widget'

ruboto_import_widgets :Button, :LinearLayout, :TextView

java_import 'android.os.AsyncTask'
java_import 'java.lang.Thread'      # Thread.sleepを使いたいため追加

# Progressバーまわり
java_import 'android.app.ProgressDialog'
java_import 'android.content.DialogInterface'
java_import 'android.util.Log'


class AsyncTaskProgressDialogActivity
  attr_accessor :status
  @@task = nil

  def onCreate(bundle)
    super

    self.content_view = 
      linear_layout orientation: :vertical do
        @status = text_view text: 'before', text_size: 48
        button text: 'start', on_click_listener: proc { run_async_task }
      end
  end

  def onPause
    super
    @@task.dismiss_dialog if showing_dialog?
  end

  def onResume
    super
    @@task.show_dialog if showing_dialog?
  end

  def onSaveInstanceState(bundle)
    super
    bundle.putString('TEXT_VIEW', @status.text)
  end

  def onRestoreInstanceState(bundle)
    super
    @status.text = bundle.getString('TEXT_VIEW')
  end


  def run_async_task
    @@task = MyAsyncTask.new(self)
    @@task.execute ''
  end

  def showing_dialog?
    !@@task.nil? && @@task.is_showing
  end
end


class MyAsyncTask < AsyncTask
  attr_reader :is_showing

  def initialize(activity)
    # カッコ重要。無しだと、エラーになる
    super()
    
    @activity_manager = activity.getApplication.getActivityManager
    @activity_id = @activity_manager.getActivityId(activity)
  end


  def onPreExecute
    show_dialog
    @is_showing = true
  end

  def doInBackground(params)
    # 例外を捕捉しないとonCancelledが呼ばれない等が発生することに注意
    begin
      (0..9).each do |i|
        return false if isCancelled

        Thread.sleep(1000)
        publishProgress((i + 1) * 10)
      end
    rescue
    end

    true
  end

  def onProgressUpdate(values)
    @dialog.setProgress(values.first) unless @dialog.nil?
  end

  def onCancelled
    dismiss_dialog
    @activity_manager.getActivity(@activity_id).status.text = 'cancel'
    @is_showing = false
  end

  def onPostExecute(result)
    dismiss_dialog
    @activity_manager.getActivity(@activity_id).status.text = 'after'
    @is_showing = false
  end


  def show_dialog
    @dialog = ProgressDialog.new(@activity_manager.getActivity(@activity_id))
    @dialog.setTitle('Please wait')
    @dialog.setMessage('Loading data...')
    @dialog.setProgressStyle(ProgressDialog::STYLE_HORIZONTAL)
    @dialog.setCancelable(true)
    @dialog.setIndeterminate(false)
    @dialog.setMax(100)
    @dialog.setProgress(0)

    # 戻るボタンを押したとき等の処理:これを記載しないと、キャンセルボタン以外でキャンセルした時に、正しくキャンセルが動作しない
    @dialog.setOnCancelListener(proc { cancel_progress_dialog })

    # ProgressDialogのCancelボタンを追加して押したときの処理
    @dialog.setButton(DialogInterface::BUTTON_NEGATIVE, "cancel", proc { cancel_progress_dialog })
    @dialog.show
  end

  def dismiss_dialog
    @dialog.dismiss unless @dialog.nil?
    @dialog = nil
  end

  def cancel_progress_dialog
    @dialog.cancel
    cancel(true)
  end
end