ページ

2013/10/23

[Android]いまどきの開発検証用端末

事の始まり

デバッグ用端末の選定

まずはDashboards | Android DevelopersでOSのバージョン毎/画面密度(dpi)毎のシェアを確認します。
現在で言えば、Android4.0以上(2年近く前の端末もサポートしたかったらAndroid2.3以上)で、
画面サイズのシェアを見て Xlarge (タブレット)をサポートするか判断すると言った感じでしょうか。
画面サイズや画面密度のサポートの考え方を知りたい場合は以下のページが参考になります。
Supporting Multiple Screens | Android Developers

上記を考慮してサポート範囲を網羅するようなOSのバージョンと画面密度(dpi)の端末を選定します。

Nexus系Phone枠

Nexus 4
OS: Android4.3
DPI: xhdpi
特記事項: BLE対応
Nexus 4 - Wikipedia

一昔前なら Galaxy Nexus のイメージですが、今なら日本でボッタクリと噂の Nexus 4 でしょうか。
もう少し待てるなら Nexus 5 を待つのが良いと思います。

追記: 2013/11/02

Nexus 5
OS: Android4.4
DPI: ?
Nexus 5 - Google
Nexus 4 より安くて OS も最新の Android4.4 です。Nexus 5 を選ばない理由はないでしょう。
追記おわり。

Nexus系タブレット・7inchタブレット枠

Nexus 7(2012)
OS: Android4.3
DPI: tvdpi
特記事項: インカメラのみ
Nexus 7 (2012) - Wikipedia
Nexus 7(2013)
OS: Android4.3
DPI: xhdpi
特記事項: BLE対応
Nexus 7 (2013) - Wikipedia

カメラアプリを作りたいならインカメラのみの Nexus 7(2012)、
BLE を使ったアプリを作りたいなら Nexus 7(2013) を選べば良いと思います。
ところでインカメラのみの端末って Nexus 7(2012) 以外にあるんですかね?
なければインカメラのみの端末を考慮して Nexus 7(2012) を選択する意味がなさそうです。
tvdpi も対象機種が少なそう(というか Nexus 7(2012) だけ?)ですし、
hdpi から自動で引き伸ばされるのでなんとなく動くだろって感じでサポート対象から外してもいいかも?
特にこだわりがなければ Nexus 7(2013) を選んでおけば良いような気がします。

Xperia枠

Xperia A SO-04E
OS: Android4.2.2
DPI: xhdpi
SO-04E - Wikipedia
SO-04E (4.2.2)|NTTドコモ 端末・ブラウザスペック

シェアの大きい Xperia 枠です。
だいたい良い感じに動いてくれるので他の機種で動かないというバグを生みやすいですが
進捗確認とかデモ用とかで見せる際はザクッと作って動くので便利かもしれません。

Galaxy枠

Galaxy S4 SC-04E
OS: Android4.2.2
DPI: xxhdpi
SC-04E - Wikipedia
SC-04E (4.2.2)|NTTドコモ 端末・ブラウザスペック

シェアの大きい Galaxy 枠です。
Xperia で動くようになったら Galaxy でも動作に問題がないか確認するのに役立ちます。
Galaxy でバグがなくなれば他の機種シリーズの機種依存バグとかはサポート範囲外だと言って切りたいですね。

10inchタブレット枠

Xperia Tablet Z(グローバル版)
OS: Android4.1.2
DPI: hdpi
Xperia Tablet Z - Wikipedia
SO-03E (4.1.2)|NTTドコモ 端末・ブラウザスペック

横向きがデフォルトな感じになっている 10inch タブレットはサポートするなら実機は必ず欲しいですね。
カメラアプリ作ってて色々と想定外なことが起きて対応が必要でした。
当たり前かもしれないけどグローバル版はマナーモードでシャッター音鳴らなくてビックリしました。

Android2.3枠(Optional)

Xperia arc SO-01C
OS: Android2.3.4
DPI: hdpi
特記事項: アウトカメラのみ
SO-01C - Wikipedia

Androider がサポートしたがらない Android2.3 枠です。
合言葉は「もうAndroid4.0未満は切ってもいいよね」。
2年以上前の端末をサポートしなくてはならない悲しみを背負って開発する場合は
Xperia arc は機種依存バグが少ない素直な良い子なので(※使用者の感想です)オススメだと思います。
Android2.3 をサポート対象に含めた途端に機種依存バグを持つ端末がガンガン入ってくるイメージなので、
「動作保証するのは Xperia arc のみで他の Android2.3 機での機種依存は対応できません」で逃げ切りたい所。

カメラがアウトカメラのみなのでカメラアプリが作りたかったら動作確認に欲しいかもしれません。
でもアウトカメラのみの機種なんて最近の機種にはないような気がするのでやっぱりあんまりいらないかも。

まとめ

あんまり様々な機種のサポートをしたことのない Android 開発者が独断と偏見で選んでみました。
これは他の機種に変えたほうがいいとか、他の機種のほうがオススメとかあれば教えてくれると助かります。
他の Androider の検証端末の選定基準や候補のツイートをまとめたので参考になるかもしれません。
Androidのいまどきの検証端末 - Togetter

追記: 2013/11/02

Nexus 端末への Android 4.4(KitKat)のアップデート - Nexus ヘルプ
Nexus 端末は OS のアップデートが早いので最新の API を使った開発に便利なので欲しいですね。
Nexus 5 を買わなくても Nexus 4、Nexus 7、Nexus 10 は数週間でアップデートが来るので嬉しいです。
Android4.4 KitKat 楽しみデスね。

[Android]Custom View の作成と AttributeSet と declare-styleable

今までカスタム View を作ったことがなかったので作ってみた。
ImageView を丸く切り抜き表示する CircleImageView を作ってみて
謎だった AttributeSet と declare-styleable がちょっとわかったので
忘れないように記事を書いておきます。

参考
Creating Custom Views | Android Developers

まずはコンストラクタ。View のコンストラクタは3種類あることが多いけどちゃんと使い分けられているらしい。
public class CircleImageView extends ImageView {

    /**
     * プログラムから動的に生成する場合に使われる
     * 
     * CircleClipView circleClipView = new CircleClipView(this);
     */
    public CircleImageView(Context context) {
        super(context);
    }

    /**
     * レイアウトファイルから生成する場合に使われる
     * 
     * <at.wada811.view.CircleClipView
     * android:layout_width="MATCH_PARENT"
     * android:layout_height="MATCH_PARENT"
     * />
     * 指定した属性値が attrs に入る
     */
    public CircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * いまいち謎
     */
    public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray attrsArray = getContext().obtainStyledAttributes(attrs, R.styleable.circle_view, defStyle, 0);
        mX = attrsArray.getFloat(R.styleable.circle_view_center_x, mX);
        mY = attrsArray.getFloat(R.styleable.circle_view_center_y, mY);
        mR = attrsArray.getInt(R.styleable.circle_view_radius, mR);
        // StyledAttributes should be recycled! 
        attrsArray.recycle();
    }
}
レイアウトパラメータを指定された場合には、上から2番目のAttributeSetを持つコンストラクタが呼び出されます(layout側main.xmlファイルで定義した場合など)。またスタイルを指定した場合はdefStyleを持つ3つめのコンストラクタが利用されます。
Viewをカスタマイズ(独自実装)する « Tech Booster
レイアウト XML から style などを指定しても 2つ目のコンストラクタが呼び出されて
3つ目のコンストラクタで defStyle に値が入ってこなくてイマイチ謎です。
でも style を指定したらちゃんと TypedArray に style で指定したデフォルト値が入っています。
Context#obtainStyledAttributes(AttributeSet, int[], int, int) | Android Developers
Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) | Android Developers

3つ目のコンストラクタの他の使い方としてプログラム側から呼び出す方法があります。
  1. 適用したい style (@style/circle_image_view_style_demo)を作る
  2. style を指定するための属性(@attr/circle_image_view_style)を作る
  3. Activity のテーマに作成した属性に対して適用したい style を指定する
  4. コンストラクタに作成した属性(@attr/circle_image_view_style)を渡す
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="circle_image_view_style_demo">
        <item name="center_x">50</item>
        <item name="center_y">50</item>
        <item name="radius">100</item>
    </style>
    <style name="circle_image_activity_style" parent="@style/AppTheme">
        <item name="circle_image_view_style">@style/circle_image_view_style_demo</item>
    </style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="circle_image_view_style" format="integer|reference" />
</resources>
mCircleImageView = new CircleImageView(this, null, R.attr.circle_image_view_style);
参考
勉強中: ソースコード内で生成したViewインスタンスにStyleを適用

AttributeSet と declare-styleable について書くつもりが話がそれました。
AttributeSet は2つ目のコンストラクタのコメントにあるようにレイアウト XML の属性値が入っています。
declare-styleable はカスタム属性を定義することができます。
CircleImageView では以下のカスタム属性を定義しています。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="circle_view">
        <attr name="center_x" format="float" />
        <attr name="center_y" format="float" />
        <attr name="radius" format="integer" />
    </declare-styleable>
</resources>
カスタム属性は以下のように使用することができます。
<at.wada811.view.CircleImageView
    xmlns:circle="http://schemas.android.com/apk/res-auto"
    android:id="@+id/circle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/blue_light"
    android:src="@drawable/lena"
    circle:center_x="50"
    circle:center_y="50"
    circle:radius="100" />
ここで指定した値は3つ目のコンストラクタで取得しているように取得することができます。
カスタム View の作成については以上です。

ソースコードはwada811/AndroidLibrary-wada811に含まれています。

2013/10/05

[Android]外部SDカードのパスを取得する

Android の機種依存ってそんなに遭遇したことないなーとかつぶやいたら
外部SDカードのパスの取得があるじゃんという意見があり、
それって機種依存なのかーと思った@wada811です。
Environment#getExternalStorageDirectory() | Android Developersを使えば
内部SDカードのパスの場合もあるけど(というか最近の機種はほとんど内部SDだけど)
SDカードのパスを取得することができるので機種依存扱いするのもなんだかなー。

開発者はなぜ外部SDのパスを取得したがるのだろうか。
ユーザーが内部SDではなく外部SDにデータを保存したいと言っているのを見たことがないけど
あえて外部SDのパスの取得に突っ込んで機種依存対応コードで対応しきれなくて苦しむなんてマゾなの?
Environment#getExternalStorageDirectory() | Android Developersでいいじゃんと思うね。
Android で対症療法的なコードを書いたらずっと機種依存対応しなくちゃいけないからジリ貧だと思うなぁ。


ということで根本的なところから解決できるっぽいコードを見つけたのでそれを使うことにしました。
/**
 * 外部SDカードのパスを取得する
 * 
 * @return external SD card path, or null
 */
public static String getExternalSdCardPath(){
    HashSet<string> paths = new HashSet<string>();
    Scanner scanner = null;
    try{
        // システム設定ファイルを読み込み
        File vold_fstab = new File("/system/etc/vold.fstab");
        scanner = new Scanner(new FileInputStream(vold_fstab));
        while(scanner.hasNextLine()){
            String line = scanner.nextLine();
            // dev_mountまたはfuse_mountで始まる行
            if(line.startsWith("dev_mount") || line.startsWith("fuse_mount")){
                // 半角スペースではなくタブで区切られている機種もあるらしい
                // 半角スペース区切り3つめ(path)を取得
                String path = line.replaceAll("\t", " ").split(" ")[2];
                paths.add(path);
            }
        }
    }catch(FileNotFoundException e){
        e.printStackTrace();
        return null;
    }finally{
        if(scanner != null){
            scanner.close();
        }
    }
    // Environment.getExternalStorageDirectory() が内部SDを返す場合は除外
    if(!Environment.isExternalStorageRemovable()){
        paths.remove(Environment.getExternalStorageDirectory().getPath());
    }
    // マウントされているSDカードのパスを追加 
    List<string> mountSdCardPaths = new ArrayList<string>();
    for(String path : paths){
        if(isMounted(path)){
            mountSdCardPaths.add(path);
        }
    }
    // マウントされているSDカードのパス
    String mountSdCardPath = null;
    if(mountSdCardPaths.size() > 0){
        mountSdCardPath = mountSdCardPaths.get(0);
    }
    return mountSdCardPath;

}

/**
 * パスがマウントされているSDカードのパスかチェックする
 * 
 * @param path SD card path
 * @return true if path is the mounted SD card's path, otherwise false
 */
public static boolean isMounted(String path){
    boolean isMounted = false;
    Scanner scanner = null;
    try{
        // マウントポイントを取得する
        File mounts = new File("/proc/mounts");
        scanner = new Scanner(new FileInputStream(mounts));
        while(scanner.hasNextLine()){
            if(scanner.nextLine().contains(path)){
                isMounted = true;
                break;
            }
        }
    }catch(FileNotFoundException e){
        e.printStackTrace();
        return false;
    }finally{
        if(scanner != null){
            scanner.close();
        }
    }
    return isMounted;
}
参考
Android開発日誌:androidとjava関連 AndroidでSDカードのpathを取得する
【android】 SDカードのpathを取得する方法 | 一番かんたんなJava入門

参考元とはちょっと変えています。例外を投げている部分を null で返したり false 返したり。
例外投げておいてキャッチしないなら止まられても困るし取りあえず動くように変更しました。
あと、パスの除外の処理が for 文の中で remove してて怖かったので
有効なものを別のリストに入れるようにしました。ループの中でコレクションの破壊的操作は怖いです。

それにしてもシステム設定ファイルとかマウントポイントのパスなんてよく分かるなぁ。すごい。
ただし、このシステム設定ファイルなんですが、GalaxyNexusや一部のカスタムROMには存在しないそうです。
Nexus7(2013)もありませんでした。コレも機種依存かと思うと微妙ですね。

これで外部SDのパスが取得できるようになるけど
外部SDはマルチユーザ対応が難しいということを忘れてないけないね。
マルチユーザ対応 Android 4.2以降の内部ストレージと外部ストレージ | Tech Booster
Environment#getExternalStorageDirectory() | Android Developersならいい感じにやってくれるから
あえて外部SDへの保存をするべきなのか考えてないとダメだね。