2015/04/09

Android技術社認定試験

「Androidアプリケーション技術者認定試験ベーシック」と言うのを受けてきました。
仕事先の壁にポスターが張ってあって存在を知りました。
プロメトリックの試験は何回かやっているので大体分かっていましたが案の定、暗記オンリーのクソテストです。
しかも、実際の開発ではほぼ役に立たない API やDOSコマンドの丸暗記でまったく楽しくない苦痛な作業でした。

購入した本は以下2点

これ丸暗記すれば誰でも受かります。
Android開発やったことの無い人でもw


2015/03/08

ループする HorizontalScrollView

Android の widget に有りそうで無いのが左右が繋がって循環ループできる View。
横スクロールする HorizontalScrollView は有るけど端っこで止まってしまう。

ググっても質問ばかりでまともな回答が無いようなので汎用的な widget を作ってみた。

ソース

200 行程のクラス1つに収まった。

LoopHScrollView.java:

// Copyright (c) 2015 kotemaru.org  /  License is APL-2.0
package org.kotemaru.android.fw.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

public class LoopHScrollView extends HorizontalScrollView {
    private InnerLayout mInnerLayout;
    private AnimeManager mAnimeManager;
    private GestureDetector mGestureDetector;
    private CsutomOnGestureListener mOnGestureListener = new CsutomOnGestureListener();

    public LoopHScrollView(Context context) {
        this(context, null);
    }
    public LoopHScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public LoopHScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mAnimeManager = new AnimeManager();
        mGestureDetector = new GestureDetector(context, mOnGestureListener);

        mInnerLayout = new InnerLayout(context);
        mInnerLayout.setOrientation(LinearLayout.HORIZONTAL);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mInnerLayout.setLayoutParams(params);
        this.addView(mInnerLayout);
    }

    public void setChildViewGroup(ViewGroup child) {
        CloneView clone1 = new CloneView(getContext(), child);
        CloneView clone2 = new CloneView(getContext(), child);

        LinearLayout.LayoutParams childParams = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mInnerLayout.removeAllViews();
        mInnerLayout.addView(clone1, childParams);
        mInnerLayout.addView(child, childParams);
        mInnerLayout.addView(clone2, childParams);
    }

    public class InnerLayout extends LinearLayout {
        public InnerLayout(Context context) {
            super(context);
        }
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            resizing();
        }
        @Override
        public void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            resizing();
        }
        private void resizing() {
            if (getChildCount() == 3) {
                View clone1 = this.getChildAt(0);
                View origin = this.getChildAt(1);
                View clone2 = this.getChildAt(2);
                int width = origin.getMeasuredWidth();
                if (width < LoopHScrollView.this.getMeasuredWidth()) {
                    // TODO: Can not scroll small child view.
                    mInnerLayout.removeView(clone1);
                    mInnerLayout.removeView(clone2);
                } else {
                    clone1.setMinimumWidth(width);
                    clone2.setMinimumWidth(width);
                }
            }
        }
    }

    public static class CloneView extends ViewGroup {
        ViewGroup mOrigin;

        public CloneView(Context context, ViewGroup origin) {
            super(context);
            mOrigin = origin;
        }
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
        }
        @Override
        protected void dispatchDraw(Canvas canvas) {
            mOrigin.draw(canvas);
        }
        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            return mOrigin.dispatchKeyEvent(event);
        }
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean result = mOrigin.dispatchTouchEvent(ev);
            this.invalidate();
            return result;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) mOnGestureListener.onDown(ev);
        return super.onInterceptTouchEvent(ev);
    }
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return mGestureDetector.onTouchEvent(ev);
    }

    private class CsutomOnGestureListener extends GestureDetector.SimpleOnGestureListener {
        boolean mIsFirstScroll = true; // Note: 子要素のACTION_DOWNが届かず誤動作するので初回を無視する。

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (!mIsFirstScroll) mAnimeManager.startScroll(velocityX);
            mIsFirstScroll = false;
            return true;
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            mAnimeManager.startFling(-velocityX / 50);
            return true;
        }
        @Override
        public boolean onDown(MotionEvent ev) {
            mAnimeManager.stopFling();
            mIsFirstScroll = true;
            return true;
        }
    };

    private class AnimeManager {
        private static final int INTERVAL = 50; // ms
        private static final float ATTENUAION_RATE = 0.90F;
        private float mDelta = 0;
        private final Runnable mUpdateRunner = new Runnable() {
            @Override
            public void run() {
                onUpdate();
            }
        };
        public void update() {
            Handler handler = getHandler();
            if (handler == null) return;
            handler.postDelayed(mUpdateRunner, INTERVAL);
        }

        public void startScroll(float delta) {
            stopFling();
            if (!loopScrollPosition()) {
                scrollBy((int) delta, 0);
            }
        }
        public void startFling(float delta) {
            mDelta = delta;
            update();
        }
        public void stopFling() {
            mDelta = 0.0F;
        }
        private void onUpdate() {
            if (Math.abs(mDelta) > 1.0F) {
                scrollBy((int) mDelta, 0);
                mDelta = mDelta * ATTENUAION_RATE;
                loopScrollPosition();
                update();
            } else {
                stopFling();
            }
        }
        private boolean loopScrollPosition() {
            int curX = computeHorizontalScrollOffset();
            int unitWidth = computeHorizontalScrollRange() / 3;
            if (curX > unitWidth * 1.8F) {
                scrollTo(curX - unitWidth, 0);
                return true;
            } else if (curX < unitWidth * 0.2F) {
                scrollTo(curX + unitWidth, 0);
                return true;
            }
            return false;
        }
    }
}
  • やっていることは同じ 子View を3つ横に並べてスクロールさせているだけ。
    • 端に来ると真ん中にジャンプする。
  • 肝は指定された子Viewの表示をコピーする CloneView 内部クラス。
    • dispatch{Draw|KeyEvent|MotionEvent}() メソッドをオリジナルに転送するだけだがほぼ同じ動きをさせる事ができている。

使い方

使い方は layout.xml に LoopHScrollView を指定して Activity.onCreate() からループする内容となる子ViewGroupを設定するだけ。
ループする内容は実装上の都合により別の layout に定義する必要がある。

Activity の main_activity.xml の抜粋

    <org.kotemaru.android.fw.widget.LoopHScrollView
        android:id="@+id/loopHScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </org.kotemaru.android.fw.widget.LoopHScrollView>
  • LoopHScrollView を好きな所へ埋め込む。

ループする内容 の loop_scroll_item.xml の例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content" android:layout_height="match_parent"
    android:orientation="vertical" >
    <LinearLayout  android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:orientation="horizontal" >
        <CheckBox android:layout_width="100dp" android:layout_height="wrap_content"
            android:background="#ff8888" android:text="Red" android:textSize="24dp" />
        <CheckBox android:layout_width="100dp" android:layout_height="wrap_content"
            android:background="#ffff88" android:text="Yellow" android:textSize="20dp" />
        <CheckBox android:layout_width="100dp" android:layout_height="wrap_content"
            android:background="#88ff88" android:text="Green" android:textSize="24dp" />
        <CheckBox android:layout_width="100dp" android:layout_height="wrap_content"
            android:background="#88ffff" android:text="Cyan" android:textSize="24dp" />
        <CheckBox android:layout_width="100dp" android:layout_height="wrap_content"
            android:background="#8888ff" android:text="Blue" android:textSize="24dp" />
    </LinearLayout>
</LinearLayout>
  • 横幅は LoopHScrollView より大きい幅が必要。
    • 小さいとスクロールできない。
  • トップ要素は ViewGroup が必須。

onCreate() の実装

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);

    LoopHScrollView scrollView = (LoopHScrollView) findViewById(R.id.loopHScrollView);
    LayoutInflater inflater = LayoutInflater.from(this);
    ViewGroup child = (ViewGroup) inflater.inflate(R.layout.loop_scroll_item, null, false);
    scrollView.setChildViewGroup(child);
}
  • 子要素を生成して設定しているだけ。
    • 子要素は ViewGroup なら何でも良い。
  • イベントは KeyKevent と MotionEvent は拾う。
    • 但し、ドラッグ系の操作はできない。(HorizontalScrollViewと同じ)

実行結果

ちゃんとループ出来てます。
セレクタ系 UI では使えそうな気がする。

追記(2016/6/4)

フラグメントから使うと上手くいかないとのコメントが有ったので修正方法。

    public class InnerLayout extends LinearLayout {
        public InnerLayout(Context context) {
            super(context);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            if (getChildCount() == 3) {
                View clone1 = this.getChildAt(0);
                View origin = this.getChildAt(1);
                View clone2 = this.getChildAt(2);
                int width = origin.getMeasuredWidth();
                if (width < LoopHScrollView.this.getMeasuredWidth()) {
                    // TODO: Can not scroll small child view.
                    mInnerLayout.removeView(clone1);
                    mInnerLayout.removeView(clone2);
                } else {
                    clone1.setMinimumWidth(width);
                    clone2.setMinimumWidth(width);
                }
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
  • サイズ補正のタイミングを onMeasure() に移動して解決。

2015/03/05

縦横比固定FrameLayout

Android の画面設計をやっていると縦横比(アスペクト比)を固定したまま 端末の画面サイズ一杯にレイアウトしたい事がよく有るのだけど標準の機能に無い。

ググってみたところ自力で計算して設定するしか無いらしい。

このサイトを参考に汎用的に使える FrameLayout を作ってみた。

FixedAspectFrameLayout.java:

// Copyright (c) 2015 kotemaru.org  / License is APL-2.0
package org.kotemaru.android.fw.widget;

import org.kotemaru.android.fw.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.FrameLayout;

/**
 * 縦横比固定FrameLayout。
 * - 上位のLayoutによって動的に決定された幅(or高さ)に応じて縦横比が一定になるように高さ(or幅)を決定する。
 * - 縦横比はカスタム属性 aspectRate に float (幅÷高さ) で与る。
 * - 参考:http://stackoverflow.com/a/13846628/804479
 * 
 * @author kotemaru.org
 */
public class FixedAspectFrameLayout extends FrameLayout {
    private float mAspectRate;

    public FixedAspectFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.fw_FixedAspectFrameLayout);
        this.mAspectRate = a.getFloat(R.styleable.fw_FixedAspectFrameLayout_aspectRate, 1.0F);
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
            int h = (int) (MeasureSpec.getSize(widthMeasureSpec) / mAspectRate);
            super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY));
        } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            int w = (int) (MeasureSpec.getSize(heightMeasureSpec) * mAspectRate);
            super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), heightMeasureSpec);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
}

attr.xml

<resources>
    <declare-styleable name="fw_FixedAspectFrameLayout">
        <attr name="aspectRate" format="float" />
    </declare-styleable>
</resources>

 

使い方

使い方は layout.xml に FixedAspectFrameLayout を指定してカスタム属性の aspectRate を指定するだけ。

layout.xml

    <org.kotemaru.android.fw.widget.FixedAspectFrameLayout
        xmlns:custom="http://schemas.android.com/apk/res/org.kotemaru.android.fw"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ff00"
        custom:aspectRate="2.35" >
    </org.kotemaru.android.fw.widget.FixedAspectFrameLayout>
  • この例ではシネスコのアスペクト比の FrameLayout になるので内側に適当な View を match_parent で入れれば良い。

実行結果

 

  • 端末も縦横も関係無く縦横比を維持したしたまま画面一杯にレイアウトできている。

2014/12/29

Androidで非同期HTTP通信

Androidフレームワークに対する疑問の一つに HttpClient が有る。

通信は非同期で行えと口をすっぱくして言うくせに標準の HttpClient は同期式しか用意されていない。 しかもこいつは connect 処理に対してはキャンセルが効かない。 タイムアウトしか出来ないと言う腐った実装だったりする。

非同期通信ライブラリくらい有るだろうと思って調べてみたら3つほど出てきた。

Apache HttpAsyncClient 4.0

Android で動かすとエラーになる。
Android に入っている Apache HttpCore が古いため。
最新の HttpCore を使うにはパッケージ名を変更して別のライブラリとしてインストールする必要が有るらしい。

ボツ。

loopj Android Asynchronous Http Client

定番ぽい雰囲気。
でも標準の HttpClient をラップしているだけで NIO は使って無い。
つまりスレッドの占有状態は変わらない。
しかも API がいまいちダサい。

俺的にちょっと違う。

koush AndroidAsync

NIO 使って1スレッドで動くと書いてある。おぉ求めていた物。
でもサンプルコードがちょこっと有るだけでドキュメントが無い。
ソースも見てみたがコメントすら書いてない。orz

作りかけなのかドキュメント書くのが嫌いなのかは分からんけどいずれにせよライブラリとしては使えない。

だったら自分で書いてやらぁ

腹が立って来たので自分で実装してみることにした。

  • NIO + Selector で通信スレッドを1つで済ませる。
  • API は極力標準の HttpClient をパクって。
  • レスポンスは InputStream と ByteBuffer を選べる用に。
  • Keep-Alive と Chunked-Transfer をサポート。

ぐらいで。

分かってたけど簡単じゃ無いね NIO は。
全部のクラスが状態遷移マシンですわ。

何とか最低限のテストコードを通せました。

InputStream を受け取る POST のサンプルコード。

クラスが Async~ になるだけでほぼ標準のHttpClientと同じように使える。
InputStream は全文をオンメモリに持つので大きいデータには使えない。 PipedInputStream を使うオプションが有ってもいいかもしれない。
ちなみに execute() は UIスレッドで実行してもエラーにならない。

AsyncHttpClient mClient = new AsyncHttpClient();
private void doSend() {
    AsyncHttpPost request = new AsyncHttpPost("http://192.168.0.2/cgi-bin/log.sh");
    HttpEntity httpEntity = new StringEntity("Test data");
    request.setHttpEntity(httpEntity);

    mClient.execute(request, new AsyncHttpListenerBase() {
        @Override
        public void onResponseBody(HttpResponse httpResponse) {
            if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                // サーバからエラー
            }
            try {
                InputStream is = httpResponse.getEntity().getContent();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                String line;
                while ((line = br.readLine()) != null) {
                    Log.i("DEBUG", "->" + line);
                }
                br.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

ByteBuffer を受け取る GET のサンプルコード。

ByteBuffer を直接で無く ByteBufferReader で受け取るのは使い終わった ByteBuffer を返して貰うため。(再利用する)
この辺り利便性と実行効率を両立させるのが難しいです。

Google さんは Chunked で応答して来ますがアプリに渡すときには平文に変換しています。ここが実装上の一番面倒なところ。

それと FileChannel って Selector 使えないのね。はじめて知ったわ。

private void doSend2() {
    AsyncHttpGet request = new AsyncHttpGet("http://www.google.co.jp/");
    mClient.execute(request, new AsyncHttpListenerBase() {
        FileChannel mFileChannel;

        @Override
        public boolean isResponseBodyPart() {
            return true;
        }
        @Override
        public void onResponseBodyPart(ByteBufferReader transporter) {
            try {
                if (mFileChannel == null) {
                    @SuppressWarnings("resource")
                    FileOutputStream file = new FileOutputStream(getFilesDir()+"/index.html");
                    mFileChannel = file.getChannel();
                }

                ByteBuffer buffer = transporter.read();
                if (buffer != null) {
                    while (buffer.hasRemaining()) {
                        if (mFileChannel.write(buffer) == -1) break;
                    }
                } else {
                    mFileChannel.close();
                }
                transporter.release(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onResponseBody(HttpResponse httpResponse) {
            // not called.
        }
    });
}

まとめ

結局現状では loopj で妥協するくらいしか無さそう。

今回作ったライブラリはちゃんとテストを行えば実用になりそうな気がする。
興味のある人はこちらからどうぞ

追記

HTTPSも欲しいなと思って作り始めたらどつぼに嵌ったよ。
JSSE のドキュメントがアバウト過ぎて訳分かんねー。
でも何とか Selector + SSLEngine の組み合わせで動かすことに成功。

需要は無いと思うけどソースはこちら


2014/12/06

PureJavaでAndroidのSQLite

Android で SQLite を使うときに結構冗長になるので何とかならないかと考えてみた。

Android 向けの O/R マッパーもぼちぼち出てきているようだけど そもそも Android でそんなに大規模なテーブル構成を使うことも無いので XML とかでテーブル定義書かされるのもウザかったりする。

なので目指すのは標準の Java だけでシンプルに SQLite を扱う方法。

  • 1つのクラスで閉じている。
    • テーブル生成、挿入、更新、削除、検索ができる。
  • カラム名の定義(テーブル定義)は一ヶ所で済む。
  • 定義したカラム名は eclipse の補間やリファクタが効く。
  • コード量が少なく見通しが効く。
  • 将来的に複雑な SQL が使いたくなっても耐えられる。

以上の要件を満たせる用に考えたテンプレがこれ。

package sample;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class SQLiteSample extends SQLiteOpenHelper {
    static final String DB_NAME = "sample.db";
    static final int VERSION = 100;

    public interface Column {
        public String name();
        public String type();
    }

    // テーブル定義
    private static final String SAMPLE_TABLE = "SAMPLE_TABLE";
    public enum SampleCols implements Column {
        _ID("integer primary key autoincrement"),
        FIRST_NAME("text"),
        SECOND_NAME("text");

        // --- 以下、定形 (enumは継承が出来ないので) ---
        private String mType;
        private String mWhere;

        SampleCols(String type) {
            mType = type;
            mWhere = name() + "=?";
        }
        // @formatter:off
        public String type() {return mType;}
        public String where() {return mWhere;}
        public long getLong(Cursor cursor) {return cursor.getLong(cursor.getColumnIndex(name()));}
        public int getInt(Cursor cursor) {return cursor.getInt(cursor.getColumnIndex(name()));}
        public String getString(Cursor cursor) {return cursor.getString(cursor.getColumnIndex(name()));}
        public void put(ContentValues values, long val) {values.put(name(), val);}
        public void put(ContentValues values, int val) {values.put(name(), val);}
        public void put(ContentValues values, String val) {values.put(name(), val);}
        // @formatter:on
    }
    private SampleBean toBean(Cursor cursor) {
        SampleBean data = new SampleBean(
                SampleCols._ID.getLong(cursor),
                SampleCols.FIRST_NAME.getString(cursor),
                SampleCols.SECOND_NAME.getString(cursor)
                );
        return data;
    }
    private ContentValues fromBean(SampleBean bean) {
        ContentValues values = new ContentValues();
        SampleCols._ID.put(values, bean.getId());
        SampleCols.FIRST_NAME.put(values, bean.getFirstName());
        SampleCols.SECOND_NAME.put(values, bean.getSecondName());
        return values;
    }


    SQLiteSample(Context context) {
        super(context, DB_NAME, null, VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(getCreateTableDDL(SAMPLE_TABLE, SampleCols.values()));
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DELETE TABLE " + SAMPLE_TABLE + ";");
        onCreate(db);
    }

    public SampleBean query(long id) {
        SQLiteDatabase db = getReadableDatabase();
        Cursor cursor = db.query(SAMPLE_TABLE, null, SampleCols._ID.where(), toArgument(id), null,null,null);
        if (!cursor.moveToNext()) return null;
        return toBean(cursor);
    }
    public long insert(SampleBean bean) {
        SQLiteDatabase db = getWritableDatabase();
        long id = db.insert(SAMPLE_TABLE, null, fromBean(bean));
        return id;
    }
    public long update(SampleBean bean) {
        SQLiteDatabase db = getWritableDatabase();
        long id = db.update(SAMPLE_TABLE, fromBean(bean), SampleCols._ID.where(), toArgument(bean.getId()));
        return id;
    }
    public long delete(SampleBean bean) {
        SQLiteDatabase db = getWritableDatabase();
        long id = db.delete(SAMPLE_TABLE, SampleCols._ID.where(), toArgument(bean.getId()));
        return id;
    }

    private String[] toArgument(long id) {
        return  new String[] { Long.toString(id) };
    }
    private String getCreateTableDDL(String table, Column[] columns) {
        StringBuilder sbuf = new StringBuilder();
        sbuf.append("CREATE TABLE ").append(table).append('(');
        for (Column column : columns) {
            sbuf.append(column.name()).append(' ').append(column.type()).append(',');
        }
        sbuf.setLength(sbuf.length() - 1);
        sbuf.append(");");
        return sbuf.toString();
    }
}

enum に継承機能が無いのが辛い。

ちなみに ContentProvider にしている例はこちら


2014/11/23

Androidの画像選択で嵌ったのでメモ

Android 5.0(lolipop)がリリースされたのでインストールしてみた。
体感速度で倍ぐらいになってビビったよ。
で、自作の付箋アプリの動作を確認していたら画像選択で落ちやがったのでその対応メモ。

とりあえず、ログを見るとこれで落ちてた。

android.content.ActivityNotFoundException: No Activity found to handle Intent 
{ act=android.intent.action.PICK typ=image/* }

Intent.ACTION_PICK image/* はシステムの画像選択を呼び出すはずだけど無くなったのか?
ググったら KitKat からは Intent.ACTION_OPEN_DOCUMENT に変わったらしい。
でも KitKat 以前は Intent.ACTION_PICK のままである必要がある。うへ

とりあえず、これでどっちでも画像選択が起動するようになったよ。

@TargetApi(Build.VERSION_CODES.KITKAT)
public static void startChoosePicture(Activity context, int code) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        context.startActivityForResult(intent, code);
    } else {
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setAction(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        context.startActivityForResult(intent, code);
    }
}

これでOKかと思ったら甘かったね。
OSを再起動すると選択した画像が表示されない。 ログを見たらこんなエラーが出てた。

java.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider 
from ProcessRecord{2787cc3a 22044:org.kotemaru.android.postit/u0a109} (pid=22044, uid=10109) 
requires android.permission.MANAGE_DOCUMENTS or android.permission.MANAGE_DOCUMENTS

単純に MANAGE_DOCUMENTS をパーミッションに加えても解決しない。
ググったら見つかったよ。

takePersistableUriPermission()で永続的パーミッションを得ないと再起動したら見えなくなるのか。
そんなの見落とすよ、API考えろよGoogle。

画像URI取得にこれをはさんだらうまく行った。

@TargetApi(Build.VERSION_CODES.KITKAT)
public static Uri getResultChoosePictureUri(Context context, int requestCode, int resultCode, Intent returnedIntent) {
    if (resultCode != Activity.RESULT_OK) return null;
    Uri uri = returnedIntent.getData();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        final int takeFlags = returnedIntent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
        context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
    }
    return uri;
}

Androidはバージョン間差異が細かすぎるよ。


2014/11/09

Android SDKでjavadoc生成

Android の開発環境で eclipse から javadoc を生成しようとすると例外が出たり文字化けしたりでうまく行かない。

単純に eclipse のメニューから実行するとこうなる。

java.lang.ClassCastException: com.sun.tools.javadoc.ClassDocImpl cannot be cast to com.sun.javadoc.AnnotationTypeDoc

android.jar を bootclasspath に入れたりする必要が有り結局、 専用の build.xml 作って ant で動かすのが一番面倒が無さそう。

忘れると思うのでメモっとく。

build-javadoc.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project default="javadoc">
    <property file="local.properties" />
    <property file="project.properties" />

    <target name="javadoc">
        <javadoc access="private" 
            additionalparam="-encoding UTF-8 -charset UTF-8 "
            bootclasspath="${sdk.dir}/platforms/${target}/android.jar" 
            classpath="libs/android-support-v4.jar"
            destdir="doc" 
            source="1.6" 
            sourcepath="src" >
            <packageset dir="src" defaultexcludes="yes">
                <include name="{パッケージパス}/**" />
            </packageset>
            <link href="file:/${sdk.dir}/docs/reference" />
        </javadoc>
    </target>
</project>

local.properties:

sdk.dir={android-sdksのパス}

project.properties:

target=android-{APIレベル}

2014/11/03

GooglePlayにAndroidアプリ上げてみた

はじめてアプリを Google Play に上げてみました。
作ったのは 普通 の「付箋」アプリ。 なんで作ったかと言うと以外に 普通 の「付箋」アプリって無いんですよね。

ここで 普通 と言っているのは 忘れちゃいけないことをモニタの横に張っておいて、その件が済んだらゴミ箱へ って言う付箋の使い方の出来るアプリ。

具体的には

  1. ホーム画面の好きな所に貼れる。アイコンの上にも。
  2. アプリを起動しなくてもホーム画面に表示されてる。(嫌でも目に付く)
  3. 不要になったらはがせる。(枠も表示されない)
  4. 当然、他のアプリが起動したら表示されない。

だけなんだけど、これだけの事が出来るアプリが無い。
何で出来ないかは実装してみて技術的な問題が色々有る事が分かったけどね。

そんなこんなで完成したのがこのアプリです。

ホーム画面の上に邪魔臭く陣取ります。
ドラッグ&ドロップでゴミ箱にポイできます。
イメージ通りです。

と言うわけで Google Play に登録しました。 Google Play はこちら

検索してみます。

orz
「付箋」で 132 位、「post it」で 56 位と言う惨憺たる結果に。
インストールしてもらえる気がしないw。ライバル多すぎですね。

雑感

実際 Google Play に登録してみて分かったのは登録画面用の画像リソース等が以外に多いこと。
512x512px の高解像度アイコンとか、1024x500px のバナー画像とかが必須項目になっています。

得にアプリを端的に表す「バナー画像」なんて絵心のないプログラマーにはハードル高すぎます。
本気で白画面でごまかそうかと思ったのですが Web でフリーの写真を見つけて加工の許可を頂いて登録する事ができました。

有料アプリは開発者の住所公開も必須になったようだし個人開発者は追い出したいんでしょうかね。


プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿

リンク

アーカイブ