kakueki61's dev history

備忘録的に記録を残しています

Android Studioでのパッケージ名変更方法

Eclipseの時の大変でしたが、Android Studioではより大変でした。 テキスト検索で全文字列置換とかが簡単そうだけどその方法もすぐに見つからなかったので手探りでなんとかできました。

  1. 適当なファイルでパッケージ名"xxx.yyy.zzz"を選択
  2. リファクタ機能でリネーム
  3. yyyにかかわる変更を聞かれる→Refactor
  4. パッケージを選択してリネーム(zzzのみ対象)
  5. Manifestのパッケージ名を手動で修正
  6. clean -> rebuild
  7. .idea内のファイルに前のパッケージ名が残ってる
  8. 手動修正→clearn→rebuild→.idea内のファイルは元に戻ってしまう
  9. プロジェクト閉じる
  10. 再度import
  11. 一応ビルドできた

まだ検証不十分ですが、ひとまず備忘録です。

Androidでサーバーに画像データをPOSTする

画像に限らず、ファイルをサーバー側には送りたい時にはmultipart/form-data形式でデータを送ります。

こちらの記事が大変参考になりました↓
[Androidアプリ開発] ネットワーク通信ライブラリ Volley | まるーんの開発備忘録 Volleyでmultipart/form-dataを送信する - fly1tkg blog

ただ、少しはまった箇所があったのでそこに関してだけ書いておこうと思います。

基本的な流れとしては、

  1. multipart/form-data送信用にVolleyのRequestクラスを継承した独自Request(MultipartRequest)を定義する。
  2. VolleyのRequestQueueにmultipart/form-data用のRequestが詰められた時に、HttpClientでファイルデータを送信するような処理を行う。VolleyのHurlStackを継承した独自HttpStackを定義する。

詳細は上記リンクを参照して下さい。

さて、問題になったのは 複数の画像データを同時にPOSTしたい時 でした。

私の場合、以下のようなMultipartJsonRequestというクラスを、multipart/form-data送信用Requestクラスとして定義しました。バイナリデータとして、byte[], File, InputStreamが渡せるようになっています。

public class MultipartJsonRequest extends JsonRequest<JSONObject> {
    private static final String TAG = MultipartJsonRequest.class.getSimpleName();

    private Map<String, String> mStringParams;
    private Map<String, ?> mBinaryParams;
    private MultipartEntityBuilder mMultipartEntityBuilder = MultipartEntityBuilder.create();

    public MultipartJsonRequest(String url, Map<String, String> stringParams, Map<String, ?> bynaryParams,
                                Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, null, listener, errorListener);

        mStringParams = stringParams;
        mBinaryParams = bynaryParams;
        buildMultipartEntity();
    }

    private void buildMultipartEntity() {
        mMultipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        mMultipartEntityBuilder.setBoundary("___________________" + Long.toString(System.currentTimeMillis()));
        mMultipartEntityBuilder.setCharset(Consts.UTF_8);

        for (Map.Entry<String, String> entry : mStringParams.entrySet()) {
            mMultipartEntityBuilder.addTextBody(entry.getKey(), entry.getValue());
        }

        for (Map.Entry<String, ?> entry : mBinaryParams.entrySet()) {
            ContentType imageContentType = ContentType.create("image/jpeg");

            if (entry.getValue() instanceof byte[]) {
                Log.d(TAG, "entry.getValue() => byte[]");
                mMultipartEntityBuilder.addBinaryBody("uploadFiles", (byte[]) entry.getValue(), imageContentType, entry.getKey());
            } else if (entry.getValue() instanceof File) {
                Log.d(TAG, "entry.getValue() => File");
                mMultipartEntityBuilder.addBinaryBody("uploadFiles", (File) entry.getValue(), imageContentType, entry.getKey());
            } else if (entry.getValue() instanceof InputStream) {
                Log.d(TAG, "entry.getValue() => InputStream");
                Log.d(TAG, "key: " + entry.getKey());
                mMultipartEntityBuilder.addBinaryBody("uploadFiles[]", (InputStream) entry.getValue(), imageContentType, entry.getKey());
            }
        }
    }

    @Override
    public String getBodyContentType() {
        return mMultipartEntityBuilder.build().getContentType().getValue();
    }

    public HttpEntity getEntity() {

        return mMultipartEntityBuilder.build();
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        byte[] responseData = response.data;
        try {
            String jsonString = new String(responseData, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return Response.error(new ParseError(e));
        } catch (JSONException e) {
            e.printStackTrace();
            Log.d(TAG, new String(responseData));
            return Response.error(new ParseError(e));
        }
    }
}

ポイントとなったのは、buildMultipartEntity()の中でMultipartEntityBuilderにaddBinaryBodyしている箇所です。
POSTされたファイルをPHPで処理することを想定していたのですが、始めはうまくいきませんでした。
addBinaryBodyの第1引数を見ていただきますと、"uploadFiles"あるいは"uploadFiles[]"となっているかと思います。 これは、HTMLで書かれたWebページからファイルをアップロードさせる場合に対応させると、

<form method="post" action="../index.php?action=image/upload" enctype="multipart/form-data">
    <input type="file" name="uploadFiles[]" multiple><br>
    <input type="submit" value="Submit"><br>
</form>

上記のような<input>タグのname属性に当たる部分です。 PHP側では、メインの処理は以下の様な感じです。

if (isset($_FILES[self::POST_FILE_KEY])) {
            $num_files = count($_FILES['uploadFiles']['name']);
            for ($i = 0; $i < $num_files; $i++) {
                $name = $_FILES['uploadFiles']['name'][$i];
                $tmp_name = $_FILES['uploadFiles']['tmp_name'][$i];
                $ext = 'jpg';
       
                // the path of uploaded files are defined according to the rule below.
                // timestamp + sequential number + "_"
                // + MD5 generated by micro seconds, original file name and accessed IP + extension
                $new_file_name = time() . $i . '_'
                    . md5(microtime() . $name . $_SERVER['REMOTE_ADDR'])
                    . '.' . $ext;
                $move_to = IMG_DIR . $new_file_name;
              
                move_uploaded_file($tmp_name, $move_to);
            }

"uploadFiles"でPOSTされたデータを受け取っています。
送信側では"uploadFiles"のように、を付けることで配列としてデータを送ることができます。これを忘れていたことでえらいハマっていまいました。[]を付けていないと一番最後にaddBinaryBodyしたデータのみ送信されます。なので、上記のサンプルコードの場合、InputStreamを詰めた場合のみ複数のデータをPOSTしてくれます。

久々に記事を書いたので、とりあえずこれくらいのネタから。。。


POSTの進捗を管理するための機能を実装しました。↓

Androidでmultipart/form-dataをPOST送信しながら、POSTの進捗をActivityに通知する。 - kakueki61's dev history

Amazon EC2 + Jenkins + bazaarでAndroidアプリを自動ビルド~②起動したEC2インスタンスにログイン~

Management ConsoleでInstancesを選択すると、インスタンスの一覧が表示される。
ここでインスタンスを選択するとその詳細情報が下に表示されます。

上方のInstance ActionsのプルダウンからConnectを選択すると、インスタンスSSH接続するためのガイドが現れます。
"Connect with a standalone SSH Client"のExampleというところにsshのコマンドラインが出てるので、これをコピペして実行すればインスタンスSSHログインすることができます。
ただし、Ubuntu AMIの場合、"ubuntu"ユーザーで再ログインするように求められました。

また、おそらくデフォルトのSecurity Groupの設定だとSSH接続の22番ポートが開いていないようなので、この設定を行う必要があります。

f:id:kakueki61:20120806033256p:plain

Management Consoleの左方からSecurity Groupを選択します。
それからInboundタブを選択し、"Create a new rule"のプルダウンからSSHを選択します。
この状態で"Source"には0.0.0.0/0が設定されています。ひとまずこの値のまま、"Add Rule"を押すと右側の一覧に"22 (SSH)"という項目が追加されます。
これで問題なければ"Apply Rule Changes"で変更を環境に反映させます。
f:id:kakueki61:20120813130840p:plain

インスタンス一覧画面に戻り、SSHログインしたいインスタンスを選択肢、"Instance Actions"のプルダウンから"Connect"を選択します。
さらに出てきたポップアップから"Connect with a standalone SSH Client"を選択すると、SSHログイン用コマンドの例が表示されるので、
これをコピペして実行すればログインすることができます。
失敗する場合には鍵の指定が間違ってるとか、セキュリティグループの設定でアクセス元に制限かけたりしてるとか確認してみて下さい。

ちなみに私がUbuntu(12.04 LTS)インスタンスの場合、rootユーザーでログインするコマンド例が表示されますが、これでそのまま試すと
"ubuntu"ユーザーでログインするよう求められました。

こんな感じでひとまずログインできましたが、SSHサーバには、設定値が適切でないために発生するセキュリティリスクがあります。
Ubuntu(12.04 LTS)インスタンスの/etc/ssh/sshd_configを見てみると、
RSAAuthentication yes
PubkeyAuthentication yes
PasswordAuthentication no
となっているので鍵認証のみが行われるはずだが、念のためIPによるアクセス制限もかけておきたい。

それについてはまた別の記事に書いておきたいと思います。

Amazon EC2 + Jenkins + bazaarでAndroidアプリを自動ビルド~①EC2を用意する~

AWSアカウントをつくる

まずはAmazon EC2の契約をする。
まずはhttp://aws.amazon.com/jp/からサインアップします。

f:id:kakueki61:20120804232329p:plain

サインアップの過程で電話による認証フローが入りますが、登録するときの電話番号と、認証に使う電話番号は違うものでもよさそうな感じでした。
今私は海外にいますが、日本にも数ヶ月一度帰ったりしてる状態だし、一生こっちで暮らすというわけでもないのでこういう時にどこの住所を登録するのがいいのかとても迷う。ひとまず住所は日本のもので、電話番号は海外用の番号を登録しました。

サインアップが完了し、無事にEC2のManagement Consoleが開けるようになりました。

EC2でインスタンスをつくる

f:id:kakueki61:20120805000859p:plain

AWS Management Consoleです。
まずは左上のプルダウンからRegionを選択しておきます。
今回はAsia Pacificを選択してみました。
f:id:kakueki61:20120806014634p:plain
それでは、Launch Instanceからインスタンスを作成していくことにします。

まずはAmazon Machine Image (AMI) の選択から。

f:id:kakueki61:20120805184848p:plain

今回は無料の"Ubuntu Server 12.04 LTS"を選択しました。

次にインスタンスの詳細を決定します。

f:id:kakueki61:20120805190827p:plain

AWSの契約ではいくらかの無料利用範囲が決められていて、マイクロインスタンスであれば1年間無料で利用できるので、とりあえずマイクロインスタンスを選択しました。
また、ストレージサービスのS3も5GBまでは無料で利用できます。
他にもいろいろあるので詳細はここを参照してください。
"Availability Zone"は明示的に決めておいた方がいいらしいので、ここでは"ap-southeast-1a"を選択しておきました。

インスタンス詳細の続きです。
f:id:kakueki61:20120806014634p:plain
Kernel IDとRAM Disk IDはデフォルト設定にしておきました。
ここでインスタンスごとにテキストデータを設定することができ、インスタンス起動後にHTTPリクエストで取得することがきます。

f:id:kakueki61:20120806020323p:plain

key/valueペアのタグを設定することもできます。
ec2-api-toolsで簡単に取得することができます。

f:id:kakueki61:20120806020924p:plain

EC2に接続するためにkey pair作成します。
一度生成してしまえば、また別のインスタンスを起動するときにも同じ鍵を指定して接続することができます。

最後にセキュリティグループを設定します。
f:id:kakueki61:20120806021135p:plain
後でも詳細設定を変更することができるようなので、ひとまずはデフォルト設定にしました。

ここまで設定すれば最後に確認画面がでてくるので、それを確認すれば新しいインスタンスが起動されます。

AndroidプロジェクトをJenkinsで自動ビルドするには

コマンドラインAndroidプロジェクトをビルドする

androidコマンドを使えるように

まずはコマンドライン上でAndroidプロジェクトをビルドできるようにする必要があります。
Androidプロジェクトをコマンドラインでビルドするにはandroidコマンドを使います。
これまで私はadbコマンドしか使っていなかったので、まずはandroidコマンドを使えるようにパスを通しました。

androidコマンドはandroid-sdk/toolsにあるので、私の場合は

の1行を.bash_profileに追加しました。

これで、

と打ち込むと「Android SDK and ADV Manager」というGUIが立ち上がるようになりました。

antでビルド

Antを使ってビルドするためには"build.xml"ファイルが必要です。
コマンドラインでプロジェクトを作成した場合には"build.xml"が自動生成されるようですが、Eclipseでプロジェクトを作成した場合には自動生成されません。
よって以下のコマンドでAnt用の"build.xml"を生成します。

これで準備が整いました。
ビルドにはデバッグ用とリリース用が用意されていて、

$ant debug

$ant release

のように実行します。
今回はデバッグ用として実行した結果、"bin"以下に

  • *-debug-unaligned.apk
  • *-debug-unaligned.apk.d
  • *-debug.apk
  • *.ap_
  • *.ap_.d
  • build.prop
  • classes.dex
  • classes.dex.d

のようにいくつかファイルが生成されました。

Jenkinsで自動化

  • Jenkinsをローカル環境にインストール
  • ローカル環境で動かしてみる
  • Jenkisサーバーを立てて、開発メンバーがアクセスできるように
$lsa /Applications
drwxrwx---  14 TakuyaKodama  staff    476  4  9 16:52 android-sdk-mac_x86
$lsa /Applications/android-sdk-mac_x86/tools/ant/
-rw-r--r--   1 TakuyaKodama  staff  60099  3 20 16:26 build.xml
TakuyaKodama:/Library/LaunchDaemons
$lsa
-rw-r--r--   1 root  wheel   681  3  6 10:54 org.jenkins-ci.plist

TakuyaKodama:/Users/Shared
$lsg
total 0
drwxr-xr-x  3 daemon  102  6 27 04:10 Jenkins
drwxrwxrwx@ 4 wheel   136 11 18  2011 SC Info

TakuyaKodama:/Users/Shared/Jenkins
$lsa
total 0
drwxr-xr-x   3 daemon  daemon  102  6 27 04:10 .
drwxrwxrwt   5 root    wheel   170  6 27 04:10 ..
drwxr-xr-x  27 daemon  daemon  918  7  1 18:13 Home