ラベル java の投稿を表示しています。 すべての投稿を表示
ラベル java の投稿を表示しています。 すべての投稿を表示

2025年6月9日月曜日

android ProgressBar が deprecated

ProgressBar が deprecated になっていた。
ダイアログを表示すると、他の操作ができないから埋込レイアウトにしとけ
というのがGoogle先生の指示なんだけど、アプリケーションのステート的には、このタスクが終了しないと、どのみち次のオペレーションはできないので、埋込にする意味がないと思う。
処理の途中でアプリを切り替えられると、onDestroyが呼ばれるので、それも困る
そんな時はやり直しですしおすし。
それにダイアログを表示していてもアプリは切り替えられる
埋め込んだところで、大差がないと思うのである。
で、いちいち埋込の画面を作成するのは、大変なのでプログレスダイアログは欲しい。
package com.foo.Helper;

import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.widget.ProgressBar;

import com.foo.helper.R;

public class CommonProgressDialog extends DialogFragment {

	private final Context context_;

	private final String title_;

	private final String message_;

	private AlertDialog.Builder builder_;

	private AlertDialog dialog_;

	private ProgressBar progressBar_;

	@NonNull
	@Override
	public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
		dialog_ = builder_.create();
		return dialog_;
	}

	private void BuildBase(final Context context, final String title, final String message) {
		builder_ = new AlertDialog.Builder(context)
				.setView(R.layout.progress)
				.setTitle(title)
				.setCancelable(false)
				//.setCanceledOnTouchOutside(false)
				.setMessage(message);
	}

	public CommonProgressDialog(final Context context, final String title, final String message) {
		this.context_ = context;
		this.title_ = title;
		this.message_ = message;
		BuildBase(context, title, message);
	}

	/// 中止ボタンを設置する
	public void setNegative(final DialogInterface.OnClickListener l) {
		if( dialog_ == null ) return;
		dialog_.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), l);
	}

	public void setMax(int maxValue) {
		if( dialog_ == null) return;
		ProgressBar pb = dialog_.findViewById(R.id.progress_bar);
		if( pb != null ) {
			pb.setMax(maxValue);
		}
	}

	public void setProgress(int progress) {
		if( dialog_ == null) return;
		ProgressBar pb = dialog_.findViewById(R.id.progress_bar);
		if( pb != null ) {
			pb.setProgress(progress);
		}
	}


	public void show() {
		if( dialog_ == null) {
			dialog_ = builder_.create();
		}
		dialog_.show();
	}

	public void dismiss() {
		dialog_.dismiss();
		dialog_ = null;
	}

	/*
	public CommonProgressDialog(Context context) {
		super(context);
		setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
	}
	public CommonProgressDialog(Context context, int theme) {
		super(context,theme);
		setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
	}

	 */
}


progress.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="20dp">
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="4"
        android:gravity="center"
        android:text="Please wait! This may take a moment." />
</LinearLayout>

2022年7月29日金曜日

apache maven で Jni DLL を梱包・備忘録

肝は、resources セクションの resource に対して、filtering=false を指定する事。
この filtering を true にすると、対象リソースをテキストファイルと見なして、文字列の置換が実行されてしまう。
dll ファイルは、バイナリで、utf-8の文字コード規則に収まらないから、false にしないとエラーになってしまう。

後は、Bundle-NativeCode に dll ファイルを resource セクションで指定したディレクトリからの相対パスで ; により列挙すればOK

  <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
      <resource>
        <directory>src/main/extlib</directory>
        <filtering>false</filtering>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven.compiler.plugin.version}</version>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <version>${maven.bundle.plugin.version}</version>
        <extensions>true</extensions>
        <configuration>
          <instructions>
            <Build-Number>${buildId}</Build-Number>
            <Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
            <Bundle-ContactAddress>${sdk.contact.address}</Bundle-ContactAddress>
            <Bundle-Version>${project.version}</Bundle-Version>
            <Bundle-NativeCode>native/myJni.dll;native/libssl-1_1-x64.dll;native/zlib1.dll</Bundle-NativeCode>
            <Export-Package/>
            <Private-Package>jp.co.foo.bar</Private-Package>
          </instructions>
        </configuration>
      </plugin>
    </plugins>
  </build>

java のコードで、loadLibraryを記述する


  private static boolean tryLoadLibrary(String libname) {
    try {
      System.loadLibrary(libname);
      return true;
    } catch(Exception err) {
      err.printStackTrace();
    }
    return false;
  }

  // Bundle-NativeCode の dll を読み込む。Platformにより拡張子が異なるため
  // Java のコードでは、.dll .so といった名称まで指定しなくても良い。
  public static void tryLoadLibraries() {
    tryLoadLibrary("native/libssl-1_1-x64");
    tryLoadLibrary("native/zlib1");
    tryLoadLibrary("native/myJni");
  }
  
  // Load JNI
  static {
    // 自動でライブラリがロードされるはずなのだが、
    // この初期化が行われるより先に、JNI の関数が呼ばれる事態が
    // 発生する事がある。よって、クリティカルな場所から重複して
    // tryLoadLibraries をコールしても大丈夫なように実装している。
    tryLoadLibraries();
  }

2022年1月7日金曜日

JNI GetMethodID に指定するタイプ文字列 忘備録

JNI で Java method id を取得する GetMethodID 関数に渡す文字列のドキュメントが見つからないので、忘備録として記す事に

まず、Primitive Type の表

タイプ文字列Primitive Type
V void
Z boolean
B byte
C char
S short
I int
J long
F float
D double


クラスのタイプ文字列は、
 Ljava/lang/String;
というように L で始まり、class URIを書いて ; で閉じる

配列の場合は、
 [I
というように [ で始まり、タイプ文字列を書く
上記の場合 int [] に対応する

関数表記は
()V
([Ljava/lang/String;)I
というように、 ( 引数 ) 返り値 というように表記する
以上

2019年9月25日水曜日

遅まきながら Android の permission コードの実装をした。

 レガシーシステムの保守もあるので、なかなかAndroidの新しいpermissionの方法に移行できないでいました。
とあるアプリが、きっかけで、ようやく permission コードを実装する機会を得ました。

参考にしたのは、実行時のパーミッション リクエスト(Runtime Permission) 色々まとめ

たくさん権限が必要な場合、煩わしいので、こんなコードにしました。
import android.Manifest;
import android.content.pm.PackageManager;
// 古い場合
//import android.support.v4.app.ActivityCompat;
//import android.support.v4.content.ContextCompat;
//import android.support.v7.app.AppCompatActivity;
// 新しい場合
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
//
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;

public class FooActivity extends AppCompatActivity {

    public static final int REQUEST_CODE = 1001;
    private static final String [] needPermissions = new String[] {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.CAMERA,
            Manifest.permission.INTERNET
    };

    /*!
      @brief 許可の得られていない権限を取得する
     */
    ArrayList getNeededPermissions() {
        ArrayList<String> result = new ArrayList<String>();
        for( String s: needPermissions ) {
            if (ContextCompat.checkSelfPermission(this, s) != PackageManager.PERMISSION_GRANTED) {
                result.add(s);
            }
        }
        return result;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_foo);
        ArrayList<String> permissions = getNeededPermissions();
        if( permissions.size() > 0) {
            // パーミッションの許可をリクエスト
            ActivityCompat.requestPermissions(this, permissions.toArray(new String[permissions.size()]), REQUEST_CODE);
        } else {
            // 許可が必要なコード
            processAny();
        }
    }

    // requestPermissionsのコールバック
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE:
                ArrayList<String> needed = getNeededPermissions();
                if( needed.size() > 0 ) {
                    // 必要な権限の許可が足りない
                    Toast.makeText(this, "本アプリを実行するための権限の許可が取れませんでした", Toast.LENGTH_LONG).show();
                    finish();
                } else {
                    // 必要な権限の許可がされた
                    processAny();
                }
                break;
            default:
                break;
        }
    }

    private void processAny() {
      // ...
    }
    
    // ...

}

2015年12月30日水曜日

Android Library の生産性の悪さ

年末に小ネタを Android 開発、生産性が悪いですね〜。自分は Java を生産性の低い言語だと考えてるのもあって、イライラもマックスですね。 Androidライブラリにリソースを組み込めないのも、生産性を下げる大きな要因だと思いませんか? DRY の原則を適応できない。 小ネタで、ログイン・ダイアログをライブラリ用として書いてみました。 Activity からは、下のような感じで呼出ます。
  showLoginDialog(this, new OnLoginListener() {
    @override
    public void onLogin(String name, String pass, int which) {
       if( DialogInterface.BUTTON_POSITIVE == which ) {
          if( validPassword(name, pass ) {
          } else {
          }
       } else {
          finish();
       }
    }
  });
import を略してすみませんが、以下の感じです。
  public interface OnLoginListener {
    public void onLogin(String name, String pass, final int which);
  }

  public static AlertDialog.Builder createLoginDialog(final Context context, final OnLoginListener l ) {
    final LinearLayout ll = new LinearLayout(context);
    final EditText userEditView = new EditText(context);
    final EditText passEditView = new EditText(context);
    final TextView tv1 = new TextView(context);
    final TextView tv2 = new TextView(context);
    final DialogInterface.OnClickListener ocl = new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialogInterface, int which) {
        l.onLogin(userEditView.getText().toString(), passEditView.getText().toString(), which);
      }
    };
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT);
    ll.setLayoutParams(params);
    ll.setOrientation(LinearLayout.VERTICAL);
    tv1.setText("USER:");
    userEditView.setInputType(android.text.InputType.TYPE_CLASS_TEXT);
    tv2.setText("PASSWORD:");
    passEditView.setInputType(android.text.InputType.TYPE_CLASS_TEXT);
    ll.addView(tv1);
    ll.addView(userEditView);
    ll.addView(tv2);
    ll.addView(passEditView);
    return new AlertDialog.Builder(context)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setTitle("login")
            .setView(ll)
            .setPositiveButton(android.R.string.ok, ocl)
            .setNegativeButton(android.R.string.cancel, ocl);
  }

  public static void showLoginDialog(final Context context, final OnLoginListener l) {
    createLoginDialog(context,l).show();
  }
  
  

2015年8月7日金曜日

MediaScannerService ファッキュー

  Android プログラミング、難しいですね。地雷の山。

Android のアプリケーションから、ファイルストレージにディレクトリを作成したり、ファイルを作成したりしても、PCからUSBで接続するとファイルが見えません。
これは、Android の ファイルシステムとは別に、MTP 接続でのファイル問い合わせが別管理になっているからなんです。
だから、Android のアプリケーションからファイル操作をファイルストレージに対して行ったら、いちいち MediaScannerService 様にお伺いを立てて登録をお願いする必要があります。
ところが、この MediaScannerService 様、登録をお願いしたものは、ディレクトリも含めて、全部「ファイル」として処理をして下さいます。
途中に登録されていない「ディレクトリ」が存在すれば、ついでに、それは「ディレクトリ」として登録してくださる親切仕様。
あ、存在しないファイルに関しては、いちいち通知しなくても、よしなに計らってくださいます。だから、ファイルやディレクトリの削除を通知してはいけません。

昔は、MEDIA_MOUNTED というのを通知してたんですが、今は、こうするのがベストなんじゃないでしょうか?
  public static void makeFileDiscoverable(File file, Context context){
    if( file.isDirectory() )  return;
    if( !file.exists() )  return;
    MediaScannerConnection.scanFile(context, new String[] { file.getAbsolutePath() }, null, null);
  }

ぼけーヽ(`Д´)ノウワァァァン!!

2016/2/2 追記:どうも、この static void MediaScannerConnection.scanFile を使用すると、LogCat に ConnectionのLeakが報告されるようです。
 Java エコシステムにおいて、Leak って言われてもね〜。
Intent を使って通知する方が良いかもしれません。
参考: MediaScanner ServiceConnectionLeaked
参考2:MediaScannerに登録を依頼する方法
  Uri contentUri = Uri.fromFile(newFile);
  Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, contentUri);
  context.sendBroadcast(mediaScanIntent);

2013年5月23日木曜日

android ndk と java から sqlite3 でアクセスするとどうなる?備忘録

 ndk からsqlite3 データベースを読みだしている状態で、android sdk の java が、データベース更新時に、時々例外を発生する事がある。どんなカラクリになっているのか気になって調べてみるとtwo connection to sqlite from java application and from ndkの情報が正確そうだ。Android SDK のJavaコードでは、1000mm sec 待っても応答が無ければ例外を送出する仕様であるらしい。言わせてもらえると、例外が発生するようなケースは、レアケースじゃね〜Ze。こんちくしょう。 自分の ndk でのレコードアクセスでは
#pragma once
#ifndef SQLITE_ACCESS_HPP
#define SQLITE_ACCESS_HPP

#include <sqlite3/sqlite3.h>
#include <functional>
#include <memory> // unique_ptr
#include <log.hpp>
#include <unistd.h>

namespace sio {

  template <class Container, class ReadOpe>
  bool read_rows(
    sqlite3*    db, 
    const char* sql, 
    Container&  container,
    ReadOpe     ope,
    const char* target_name
  ) {
    sqlite3_stmt* stmt = nullptr;
 int res = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr );
 if( SQLITE_OK != res ) {
      __android_log_print(ANDROID_LOG_ERROR, target_name, "Fail to read rows: %d", res );
      return false;
 }
    std::unique_ptr<sqlite3_stmt,std::function<void(sqlite3_stmt*)> > stmt_holder( stmt, sqlite3_finalize );
 while(true) {
      while( res = sqlite3_step(stmt), SQLITE_BUSY == res ) { usleep(10); }
   switch( res ) {
   case SQLITE_ROW:  ope( stmt, container );  break;
   case SQLITE_DONE: return true;
      default:
        __android_log_print(ANDROID_LOG_ERROR, target_name, "Fail to get row: %d", res);
        return false;
   }
 }
  }

} // namespace sio

#endif // SQLITE_ACCESS_HPP
こんな感じで処理していて、他のプロセスやスレッドによって書き込みが阻害されている場合には、何mm sec スリープさせれば良いかわからないけど、とりあえず10mm sec 休ませる事にしているのだが、Android SDK は問答無用で1秒タイムアウト。 詰んだ\(^o^)/ まぁ、1秒にでもしとかないとデッドロック等のケースに対処できないから、しょうがないのかもしれない。  多分、例外が発生したら、もう一回同じ更新をJavaの方でかけに行くのが、いいのかなぁ?

2013年5月20日月曜日

android Intent で画像編集、備忘録

 android.net.Uri の作成の仕方によっては、「ファイルが見つかりません」と、つれなくあしらわれるので備忘録。 以下のようなやり方だと、ファイルを認識できない。
  public void editImage( String path, String filename ) {
    Uri imageUri = Uri.parse( path + "/" + filename );
    Intent launchEditor = new Intent();
    launchEditor.setAction(Intent.ACTION_EDIT);
    launchEditor.setDataAndType(imageUri, "image/jpeg");
    activity.startActivityForResult(launchEditor, 0);            
  }
 ちゃんと、File クラスを通じて生成する事
  public void editImage( String path, String filename ) {
    Uri imageUri = Uri.fromFile( new File( path, filename ) );
    Intent launchEditor = new Intent();
    launchEditor.setAction(Intent.ACTION_EDIT);
    launchEditor.setDataAndType(imageUri, "image/jpeg");
    activity.startActivityForResult(launchEditor, 0);            
  }
2014/09/09 追記:編集するアプリケーションによっては、ファイル名を勝手に書き換えて、新しいファイル名の取得方法が用意されていないという問題が発生する場合がある。これに対する処方箋は、見つかっていない。

2013年1月8日火曜日

ADT 20.0.1 で PreCompilerBuilder が NullPointerException

android ndk ADT 20.0.1 にアップデートしたら、急にこんなエラーが起こってコンパイルできなくなったんですわ。
java.lang.NullPointerException
 at com.android.ide.eclipse.adt.internal.build.builders.PreCompilerBuilder.build(PreCompilerBuilder.java:673)
 at org.eclipse.core.internal.events.BuildManager$2.run(BuildManager.java:728)
 at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:42)
 at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:199)
 at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:239)
 at org.eclipse.core.internal.events.BuildManager$1.run(BuildManager.java:292)
 at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:42)
 at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:295)
 at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:351)
 at org.eclipse.core.internal.events.BuildManager.build(BuildManager.java:374)
 at org.eclipse.core.internal.events.AutoBuildJob.doBuild(AutoBuildJob.java:143)
 at org.eclipse.core.internal.events.AutoBuildJob.run(AutoBuildJob.java:241)
 at org.eclipse.core.internal.jobs.Worker.run(Worker.java:54)
.svn が原因とか、いろいろ情報あるけど、自分の環境には当てはまらない。で、どのファイルを追加したら起こるか試しながらコンパイルしていると、
/src/nad/epsg
/src/nad/esri
...
等の proj4 で利用している拡張子が無いテキストファイルが原因でした。(Causes non extension files.) これらに .txt の拡張子有りにリネームすると問題解決。疲れた。

2012年10月26日金曜日

android proguard support.v4 error 対処 備忘録

なんか、久々に apk ファイルを作成しようと思ったら、
Proguard returned with error code 1. See console
というメッセージが出て、失敗する。 エラーの原因は、 android.support.v4 を処理しようとしているからって感じ。 対処法は、プロジェクト直下に作成した proguard.cfg ファイルに以下の2行を追加してやる
-dontwarn android.support.v4.**

-keep class android.support.** { *; }
と、これだけ。 参考:http://stackoverflow.com/questions/8723088/error-when-proguard-android-project

2012年3月29日木曜日

android ADT が更新されたら could not find class (NoClassDefFoundError) 備忘録

eclipse ADT がアップデータされたから、アップデートしたら、eclipse が起動しねぇ。またか?と思ったけど、
eclipse -clean
したら、ちゃんと起動するようになった。

 次にプログラムを実行しようとすると、java.lang.NoClassDefFoundError の例外が飛ぶ。なんじゃコレ(-_-)。イラッとしながら解決策もわからず、右往左往してたら、相棒が解決策を教えてくれた。

  • プロジェクトのプロパティを表示する
  • Java のビルド・パスを選択する
  • 順序およびエクスポートのタブを選択する
  • チェックボックスで必要な jar ファイル等にチェックを入れる
  • プロジェクトをクリーン・ビルドする
 以上。また、無駄な時間を費やしてしまった。

2012年3月4日日曜日

android 開発におけるメモリーリーク 備忘録

いやー、android 開発、難しいですねー。嵌りました。罠に…。 どんな罠か?って言うと、 Androidのソースコードレビュー(メモリリーク)に書いてあるような罠に、はまりました。気が付いたのは、BitmapFactory.decodeResource という関数をコールしている所で、OutOfMemoryException が発生するのが、きっかけでした。色々と探っていくと、自分で確保した Bitmap は、自前で recycle をしないといけないらしい事も、わかってきました。てっきり、GC の対象だと思っていたんですが…。コードまで追ってないので、本当のところは、わかりません。  さて、android のAPIには、Context を引数にとるものが多数存在します。この Context に渡すのは、どの context なのか?気になりますよね? getApplicationContext() で全部処理できれば万歳なんでしょうけど、終了確認のダイアログを出すときに、getApplicationContext()を引数として渡すと、お亡くなりになりました。いやー難しいですねー。  ふと思ったのは、Activity が Context を継承している設計は、失敗だったんじゃないでしょうか?という事です。
 interface ISecurityContext {
  Context getContext();
 }

 class Activity ... implements ISecurityContext {
   private Context mContext_;

   public Context getContext() { return mContext_; }
   ...
 }

こんな風に設計されていれば、循環参照でActivityがメモリリークする事も無かったのではないでしょうか? あと、自前でコードから生成した View には onDetachedFromWindow とかのイベント発生しないようで、クリーンナップのコードも自前でコールしないといけないんでしょうか?  Java の GC は、とっても安全ですね。android 難しいです(>_<)。

2012年2月3日金曜日

android における sqlite3 のスレッドによるアクセス

SQLiteOpenHelper を利用していて、IlligalStateException みたいなものに出くわしたので、スレッドによるアクセスは?データベースのインスタンスは?と、気になり、結局ソースコードを追った。ぐぐって見つけたのは、「SQLiteを使う場合の注意点」で、ここに書いてある事は概ね正しいです。  SQLiteOpenHelper.java にて SQLiteDatabase は、mDatabase というメンバ変数に保持しているので、1クラスにつき、1インスタンスしか扱えません。
データベースに対しては、読み書きを行うでしょうから、getWritableDatabase 関数をコールして、内部で保持しているデータベースのインスタンスを取得する事になります。スレッドで読取り専用であっても、データベースは、DBOpenHelperに対して、ひとつのインスタンスなのでしょうがないです。内部のコードを追っていると、DBOpenHelper::getReadableDatabase 関数の中から DBOpenHelper::getWritableDatabase をコールしている場所があるので、現時点では、あまり違いは無いかもしれません。ただし、現在どっちのモード(読取り専用かどうか)でデータベースを開いているかは、内部で管理しているみたいで、整合性が合わないと、IlligalStateException がスローされます。
カーソルを作成すると、データベースの読取領域がメモリ上に読み込まれる感じになるので、この同じデータベース・インスタンスに対して、別スレッドから同時アクセスしても、そんなに大きな問題にはならないでしょう。ただし、これが可能なのは、1つの書き手と残りは複数の読み手に限られます。書き込みトランザクション中に別スレッドから読取操作が行われながらトランザクションが完了するので、非常に気持ちの悪い動作になります。よって、常識的にコードを書くならば、スレッド1個に対してDBOpenHelperのインスタンスを1つ用意すべきでしょう。そうすれば、読取り専用と書き込みの区別もできます。
 あと、この辺の情報を検索していて、データベースを開いた状態でメンバ変数に保持する事や、カーソルを開いたままメンバ変数に保持するのは、ご法度だみたいな事を書いている人もいるみたいですが、これは間違いです。Android では、デバイスから得られる情報を取得するためのイベント・リスナ中の関数は、別スレッドになるので、ここら辺を意識できないでコードを組む人には、そのような説明をした方が都合が良いからだと思われます。

2012年1月24日火曜日

GeomagneticField に関する疑問

android のセンサーはカオスで、加速度系の方向が機種によって違うとか、いろいろ聞きます。 山をやる人には馴染みの磁北線ですが、国土地理院のページに詳しく書かれています。正確には測量によって算出されるもので、API に落としこむ Google は凄いなと思います。現在は時計回りを正とした場合、日本では反時計回り方向に磁北線があり、西偏X度というような表現をします。日本でも過去には東偏だった事があるようです。 さて、手持ちの機種 IS03, Regza AT3S0, Lenovo Ideapad K1 で検証したところ、方位は時計回りを正として表現されています。従って西偏として考えれば、北海道では9度ちょいなので時計回りを正とすれば、Android SDK の GeomagneticField.getIncrenation() で取得できるべき値は負であるべき気がしますが正。逆に GeomagneticField.getDeclination() で取得できる値は正であるべき気がするのですが負。API で取得できる値は正負が逆のような気がしてならない・・・。  もちろん、センサーで取得できる方位も時計回りが正なので、真北の補正は、
  magneticAngle - declination  // 間違い
か
  magneticAngle + inclination  // 間違い
だと思うのですが、ググッて見つかるのは magneticField + declination というものばかり・・・俺、間違ってるのかな・・・自信なくなってきた・・・。  ちなみに、SensorManager.remapCoordinateSystem から SensorManager.getOrientation の API を使って取得できる方位は単位がラジアンで、GeomagneticField.getDeclination で取得できる偏角は単位が度なので、注意が必要です。
2012/01/25 追記:  どうも、小学校からやり直しですな…。センサが方位0度を示している時、真北の値はX度な訳です。写像にすると、X度を0度にが補正写像な訳ですから、-X度を足し算すれば良い事になります。 従って、
  magneticAngle + declination  // 正解
か
  magneticAngle - inclination  // 正解
で良い事になります。あー、すっきりした。

2012年1月7日土曜日

android 2重起動の禁止

いやー、android 開発、難しいですね~。こんな事は、もう語り尽くされているだろうと思ったかもしれませんが、もうちょい突っ込んだ内容になります。 android 関連の情報では、圧倒的にお世話になっている yanzm さんのところに Android launchMode の違いと、あるんで、基本は、これでオッケーなんです。  ところが、アプリケーションを終了して、直後に、アプリケーションを起動すると、一時的にアプリケーションというか、Activity が2重起動している状態になります。この時に初期化処理と終了処理が前後してしまう事があるんですわ。いやいや、ちゃんと行儀よくActivityを書いていれば、何の問題もないんです。どういう事をやっているかというと、
  
  ...
  class FooActivity extends Activity {
    public static Hoge hoge_ = null;
  ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      hoge_ = new Hoge();
    }
    @Override
    protected void onDestroy() {
      hoge_.close();
      super.onDestroy();
    }
  ...
  }
みたいな事をやってるんです。フッ、static な global 変数ですか?って笑われるかもしれませんが、正直、あちこちの Activity が関連を持って利用しなければならないとなると、こうするより他無いんです。こんなんで共有できてしまうのは、将来的に地雷を踏む可能性もあります。だって、アプリケーション全体でデータを共有するためには、どうしたらいいですか?に対する回答は、「ApplicationBean を使いましょう」だからです。データは、Parcelable で受け渡しすべきです。さて、この android.app.Application ですが、onDestroy に相当するメソッドがありません。正直言って使えません。また、画面の回転でレイアウトが変わる度に onDestroy と onCreate が呼ばれるので、うざくてしょうがありません。だから、私はダミーの BaseActivity を作成して、そこで共通リソースの初期化と破棄を行なってます。そうすれば、MainActivity の onDestroy と onCreate が何回呼ばれようが気にしなくて済みます。  話を巻き戻して、SingleTask なのに一時的に2重起動してもプロセスが別なら問題ないんじゃないですか?と思うやないですか?ところが、違うんです。この2つのアプリケーションから hoge_ が共有できてしまっており、先に終了したアプリの onDestroy よりも先に、後から起動したアプリの onCreate がコールされる状況があるんです。いや、もうビックリですね?しょうがないので、
  ...
  class FooActivity extends Activity {
    public static Hoge hoge_ = null;
    private static int process_count_ = 0;
    ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      if( 0 == process_count_ ) {
        ++process_count_;
        hoge_ = new Hoge();
      } else {
        ++process_count_;
      }
    }
    @Override
    protected void onDestroy() {
      --process_count_;
      if( 0 == process_count_ ) {
        hoge_.close();
      }
      super.onDestroy();
    }
  ...
  }
という感じで、プロセス?カウントをとって、対処してます。ちなみにタイミングがシビアじゃ無いので、カウントの保護は一切行なっていません。 追記:static 変数の記述場所を間違えてたので修正しました。やっぱりカウンタは同期保護した方が良いかもしれません。synchronized(this)ではダメっぽい気がしますが、突っ込んでません。やるとすれば private static Object sync_ = new Object(); しておいて、synchronized(sync_) でしょうか?
訂正: final 修飾子が余計だったのを取った。
2012/01/15 追記: どうも、Activity 再開時の挙動など、singleTask 等で細かく異なるようである。参考になるサイトを見つけた。こちらも目を通した方が良いです。一旦 HOME ボタンを押した後に再開すると、singleTask では BaseActivity の onDestroy がコールされるので思わしくない。そこで、このような事をやる場合は、singleInstance を選択する事にしました。 singleInstance は、Activity のフォーカスが外れる時点で、onDestroy がコールされ破棄されるので、使えない。別に single である必要は無い気がしてきた…。もう少し突っ込んで、整理した後に書く予定。追試する度に、挙動が違って困る…。
 2012/01/17 追記: BaseActivity に Manifest で android:screenOrientation="landscape" と指定するようにした。 BaseActivity -> MainActivity と表示している時にレイアウトの縦横が変更された時、MainActivity だけが変更の影響を受けるわけではなく、BaseActivity にまで変更の影響を受けている事がわかった。そのせいで、BaseActivity の onDestroy と onCreate がコールされる事になる。この辺の挙動は、かなりトリッキーだ。BaseActivity の onCreate 時に MainActivity を起動しているのだが、MainActivity が縦横変更時にどんどん増殖するという恐ろしい事態を引き起こしていた。現在は、singleInstance で実装している。
2012/08/29 追記:Android SDK における sqlite3 のコードが、データベースを開いたプロセスと閉じたプロセスが同一でないと、エラーで動作しないようなチェックが入っております。なので、最終的には、2重起動を自前で禁止するために、process_count_ >= 2 ならば、アプリケーションを終了するようにしました。  一方のプロセスがデータベースを閉じるまで待ってからオープンしようと synchronized メソッドでカウントを検知したら、そこから抜けてThread.sleep(200); とか入れてみたんですが、プロセス|スレッドのコンテキストが全く切り替わりません。このため、2重起動を自前で防ぐコードを選択せざるを得ませんでした。こういうのは、singleInstance を指定しているんだから、アプリがやる仕事じゃ無いと思います。

2011年8月12日金曜日

Android 本日の謎

float[] coords = {
    0.0F, 1,0F,
    1.0F, 1.0F,
    0.0F, 0.0F,
    1.0F, 0.0F,
  };
  //*/
  Log.d( TAG, "" + coords[0] + "," + coords[1] + "," + coords[2] + "," + coords[3] );
  Log.d( TAG, "" + coords[4] + "," + coords[5] + "," + coords[6] + "," + coords[7] );


これに対して、出力されるログが

  0.0,1.0,0.0,1.0
  1.0,0.0,0.0,1.0

なんですか?コレ?

ちなみに、これの代わりに変数の計算式で初期化( x, y + h 等)すれば、正しい値のログが出力される。


float one = 1.0F;
  float zero = 0.0F;
  float [] coords = {
    zero, one,
    one, one,
    zero, zero,
    one, zero,
  };
  Log.d( TAG, "" + coords[0] + "," + coords[1] + "," + coords[2] + "," + coords[3] );
  Log.d( TAG, "" + coords[4] + "," + coords[5] + "," + coords[6] + "," + coords[7] );


これに対して、出力されるログが

  0.0,1.0,1.0,1.0
  0.0,0.0,1.0,0.0


さっぱりわかんねぇ、なんだコレ~?

追記:バグレポート書いた

追記 2011/11/14:バグレポートに返信があった。1行目の「,」バグでしょって、突っ込まれた…。こんな事に気がつかなかったなんて…ちょー恥ずかしい… orz
 この恥ずかしさ、じわじわくるな…

2011年6月17日金曜日

java.util.concurrent.ExecutionException 詰み筋

本日遭遇した、エラー。java.util.concurrent.ExecutionException
android の service で、通信を処理していて、RemoteCallbackList に通知をしていたのだが、通信の速度が速すぎて、通知が間に合わない。よって、2重通知の状態になり、タスクが落ちる。
処理を post する事も考えられるのだが、基本的に処理が間に合わないから、キューが溜まっていくだけと考えられる。という事は、ある程度、通信データを間引くしかない…。つか、受信タスクって、スレッドプールで処理してるって事か…。

追記: RemoteCallbackList::finishBroadcast() をコールし忘れていたのが原因だったようだ…orz

2011年2月19日土曜日

Android TimerHandler

postDelayed の機構が理解できたという訳で、TimerHandler クラスを作成してみました。ネーミングはちょっといまいちかも?

  2011/2/20 に nextPost() に重要な修正が入ってます
  2025/6/4 に API 30 Handler のコンストラクタに変更が入りました。
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

public class TimerHandler {
  private static final String TAG = "TimerHandler";
  private static final boolean DEBUG_ON = true;
  private Handler handler_;
  private Runnable timeRunner_;
  private Runnable intervalRunner_;
  private long intervalMillisecs_;

  private void nextPost() {
    if(DEBUG_ON) Log.d(TAG,"nextPost");
    // ここでの実行する順序は重要。先にポストしておかないと、
    // timeRunner_ 中から stopTimer(); が呼び出せない。
    handler_.postDelayed( intervalRunner_, intervalMillisecs_);
    timeRunner_.run();
  }

  public void startTimer() {
    if(DEBUG_ON) Log.d(TAG,"startTimer");
    handler_.removeCallbacks(intervalRunner_);
    handler_.postDelayed(
    intervalRunner_,
    intervalMillisecs_ );
  }

  public void stopTimer() {
    if(DEBUG_ON) Log.d(TAG,"stopTimer");
    handler_.removeCallbacks(intervalRunner_);
  }

  TimerHandler(
    Runnable timeRunner,
    long intervalMillisecs
  ) {
    if(DEBUG_ON) Log.d(TAG,"TimerHandler");
    if( Build.VERSION.SDK_INT < 30) {
      handler_ = new Handler();
    }
    else {
      handler_ = new Handler(Looper.getMainLooper());
    }
    timeRunner_ = timeRunner;
    intervalMillisecs_ = intervalMillisecs;
    intervalRunner_ = new Runnable() { public void run() { nextPost(); } };
  }
}


使い方は、こんなんです。
class Hoge extends Activity {
  private final String TAG = "Hoge";
  private TimerHandler timerHandler_;
  //...
  boolean bTimer = false;
  private void onTimer() {
    Log.d(TAG,"onTimer");
    bTimer = !bTimer;
    if( bTimer ) Toast.makeText(this, "○",Toast.LENGTH_SHORT).show();
    else Toast.makeText(this, "×",Toast.LENGTH_SHORT).show();
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //...
    timerHandler_ = new TimerHandler( new Runnable() { public void run() { onTimer(); }}, 3000 );
  }

  @Override
  protected void onPause() {
    timerHandler_.stopTimer();
    super.onPause();
  }

  @Override
  protected void onResume() {
    timerHandler_.startTimer();
    super.onResume();
  }

  @Override
  protected void onDestroy() {
    // 念のために呼び出ししておかないと、ハンドラが残ったままゾンビ化する
    // onDestroy で、メッセージキューのクリーンアップ処理とかが
    // まともに実行されていないくさい。
    timerHandler_.stopTimer();
    super.onDestroy();
  }
  //...
}

android Handler による timer 機構

 本日は、Handler の timer 絡みのコードを深堀りしました。

Handler.postAtTime は、Handler.sendMessageAtTime をコールしています。
そして、Handler.sendMessageAtTime は、MessageQueue.enqueueMessage をコールしています。

ここでのトリックは、MessageQueue が、現在取得できる機器の稼働時間 SystemClock.uptimeMillis() と、引数で指定された稼働時間を比較して、指定された稼働時間を過ぎていた場合には、Handler を取り出して実行するというものです。稼働時間が過ぎていない場合には、MessageQueue が queue に溜まっている次の Handler を処理します。
postDelayed の場合は、現在取得できる機器の稼働時間に指定された millisec を足して、sendMessageAtTime しています。

なるほど、これはスマートなやり方だ。ちなみに、だからといって一度に大量の Handler を postDelayed で投入するのは、賢いやり方とは言えない。

機器の稼働時間がオーバーフローする事は無いのかな・・・

2011年2月17日木曜日

android の設計がいろいろ気になる

 PreferenceActivityが、使いでが無いのである。個々の値が表示されないので、ユーザは何が設定されているか脳内に記憶しとけという事らしい。ReadOnly の項目が無いくさいので、結局自前でダイアログを書いた方がマシってもんである。UIに関しても、iOS と比較すると圧倒的なデザイン力の差が出ていると断言できます。

 Activity の onResume と onPause が Activity を切り替える時に発生するので、このタイミングで unbindService したり、bindService してると、onActivityResult のイベント時に、サービスの準備が整っていなくて、どはまりするという…。いやはや、参りますね…。

 こういう事ばかり経験していると、以下のようなバッドノウハウが当たり前になってくる…。
  Handler handler_ = new Handler();
  public void postHoge() {
    handler_.post( new Runnable() {
      public void run() {
         // メインスレッドで実行したいコード
         hoge();
      }
    });
  }

  new Thread(new Runnable() {
    public void run() {
      // 1秒後に、Looper のメッセージキューへハンドラが投入される
      // この1秒という数字には、何の根拠も無い。
      // 9割方は、うまく動作するだろう。しかし、稀にエラーを引き起こすだろう。
      Thread.sleep( 1000 );
      postHoge();
    }
  }).start();

 もちろん、こんな根拠の無いコードは書かない。ただし、何かウェイトを入れなければならない場合には、関数 hoge() のコード内から、状況を確認した上で、Thread.sleep を駆使して、postHoge() するようなコードは、十分考えられる。ただし、状況が改善されるのが、異なるスレッドからによる場合に限定される。