woshidan's blog

あいとゆうきとITと、とっておきの話。

AWS CLIでAWS Athenaのクエリがデータベースが見つからないためになんども失敗する場合は結果出力先のS3のバケットのregionを確認する

俺の屍を越えていけ、的なメモ。

require 'aws-sdk-athena'
client = Aws::Athena::Client.new

begin 
    start_response = client.start_query_execution({
      query_string: "SELECT * FROM samples limit 10",
      query_execution_context: {
        database: "mydatabase",
      },
      result_configuration: {
        output_location: "s3://sample-woshidan-test-tokyo/athena_query_result"
      },
    })

  sleep(5)

    result_response = client.get_query_results({
        query_execution_id: start_response.query_execution_id
    })
rescue Aws::Athena::Errors::InvalidRequestException => ex
  puts ex.inspect
ensure
  puts "Ensure"
end

上記のコードで aws-sdk-athena でAthena のクエリを飛ばそうとしたら

Query did not finish successfully. Final query state: FAILED (Aws::Athena::Errors::InvalidRequestException)

というエラーが出て失敗する。なんで失敗したのかさっぱりわからないので、CLIで実行し直すと、

$ aws athena start-query-execution \
>  --query-string "SELECT * FROM mydatabase.samples limit 10;" \
>  --result-configuration OutputLocation=s3://sample-woshidan-test/athena_query_result
{
    "QueryExecutionId": "f8e45456-238a-44ba-955a-40f048e5c3b2"
}

$ aws athena get-query-execution --query-execution-id f8e45456-238a-44ba-955a-40f048e5c3b2
{
    "QueryExecution": {
        "Status": {
            "SubmissionDateTime": 1506675885.243, 
            "State": "FAILED", 
            "CompletionDateTime": 1506675885.387, 
            "StateChangeReason": "com.facebook.presto.hive.DataCatalogException: Namespace mydatabase not found. Please check your query."
        }, 
        "Query": "SELECT * FROM mydatabase.logs limit 10", 
        "Statistics": {
            "DataScannedInBytes": 0, 
            "EngineExecutionTimeInMillis": 53
        }, 
        "ResultConfiguration": {
            "OutputLocation": "s3://sample-woshidan-test/athena_query_result/f8e45456-238a-44ba-955a-40f048e5c3b2.csv"
        }, 
        "QueryExecutionId": "f8e45456-238a-44ba-955a-40f048e5c3b2"
    }
}

というメッセージが出て、どうも database が見つからないらしい。

もしかして、 AWS のアカウントの region を間違ったかな、とAWS CLIの設定で使う region を Athenaの管理画面で表示されている region に設定しなおしてみると

f:id:woshidan:20170929195730p:plain

- region = ap-northeast-1
+ region = us-east-2
The S3 location provided to save your query results is invalid. Please check your S3 location is correct and is in the same region and try again. If you continue to see the issue, contact customer support for further assistance.

戻すと

+ region = ap-northeast-1
- region = us-east-2
Query did not finish successfully. Final query state: FAILED (Aws::Athena::Errors::InvalidRequestException)

どっちに設定しても何かしらダメっぽい...

最終的に

自分がAthenaのクライアントとして選んだ aws-athena-client はその実装でAWS CLIを利用しています。

そして、AWS CLIで設定値として使うregion は ~/.aws/config で指定しているためか

  • クエリを投げるAthenaのデータベースが ~/.aws/config で指定したregionに存在すること
  • クエリ結果の出力先のS3のバケットが ~/.aws/config で指定したregionに存在すること

の2点がAWS CLIでAthenaにクエリを投げるためには必要みたいです。そして最終的に結果出力先のS3のバケットのregionをAthenaのデータベースのregionに揃えてことなきを得ました。

AthenaのクエリでスキャンするS3のバケットのregionは、AWS CLIで使うIAMアカウントがアクセス権を持ってさえいればAthenaのデータベースのregionと違ってもクエリが実行できるのもあっていまいち原因がわからず焦りました。

もしかしたらAthena - S3 間だけでなく、複数のAWSのサービスが協調して動く場合、CLIが一個しかregionの値を持たない、みたいなことが原因で似たようなトラブルはあるかもなと思いつつ、現場からは以上です。

iOSにてzlibのdeflate()コマンドでファイルをgzip形式で圧縮する方法について

zlibについてコピペで使うのはいやだなと思ったのでちょっと調べてメモ。

比較的小さいファイルの圧縮を行うなど、圧縮前後のデータを全てメモリ上に展開できる場合は、 compress() を使って一気に圧縮できますが、今回はストリームから少しずつデータを出し入れして圧縮していく deflate() コマンドを利用する場合についてメモします。

zlibとは

まず、zlibが何かをこちらのページから引用します。

zlibとは、圧縮アルゴリズムの一種である Deflate のライブラリであり,C#, Haskell, Java, Perl, Python, Ruby など,主要なプログラミング言語では,軒並み使えるように整備されています.圧縮・伸長が高速なこともあり,ディスク領域の有効利用や通信量の削減を目的として,zlib は気軽に利用できます.

XCodeでのiOSの開発環境への導入について

XCodeでのiOSの開発環境下では、iOS SDKに一部APIが利用できる状態でzlib.hが含まれているため umcompress() はリンクエラーで使えないようでしたが、compress() については zlib.h をimportすれば利用可能でした。

導入しようとしたプロジェクトによっては zlib.h のimportだけでは

Undefined symbols for architecture x86_64:
  "_deflate", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
  "_deflateInit2_", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
  "_deflateEnd", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

のようなエラーメッセージでリンクに失敗することがあります。

その場合は、

f:id:woshidan:20170927212154p:plain

libzを利用するアプリのプロジェクトのターゲットの設定で、 Build Phases > Link Binary With Libraries から libz と入力して出てきたものの中から利用するバージョンの libz を追加してビルドしなおしてください *1

f:id:woshidan:20170927212226p:plain

今回は圧縮の方法についてだけひとまずメモしようと思っているのでこれで進みますが、iOS添付でない zlib.h の導入には以下が参考になりそうです。

https://gist.github.com/dulaccc/75f1f49f53e544cef549

zlibでファイルを圧縮する

z_stream構造体のオブジェクトを生成

zlibの基本的な使い方ですが、 z_stream 構造体のオブジェクトを生成して、 zalloc, zfree, opaque のメンバを設定します。

z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;

この時、設定する値はデフォルト値である Z_NULL で基本的に問題ありません。gzip形式で圧縮する場合は、 delateInit2() で stream の初期化処理を行います。

入力データの設定

z_stream 構造体の avail_in, next_in の設定をします。

stream.next_in = (Bytef *)[inputData bytes]; // 入力バッファへのポインタ
stream.avail_in = (uint)[inputData length];  // 入力データの量

出力データの初期設定

自分の環境のzlibではデフォルト値でもよかったんですが、ライブラリなどにする場合、どのzlibとリンクされるか固定ではないので、 z_stream 構造体の avail_out, total_out の初期値の設定をしておきます。

stream.total_out = 0; // これまでの出力されたデータの合計長
stream.avail_out = 0; // 出力データのバッファ上の残量

ストリームの初期化

gzip圧縮の場合、ストリームの初期化は下記の deflateInit2() コマンドで行います。

// int deflateInit2(z_stream *strm,
//                  int level,  .... 圧縮レベル(数値が小さいほど圧縮時間が小さく、大きいほど圧縮後のサイズが小さい)
//                                   デフォルト   Z_DEFAULT_COMPRESSION(=6)
//                                   圧縮速度最高 Z_BEST_SPEED(=1)
//                                   圧縮率最高   Z_BEST_COMPRESSION(=9)
//                                   圧縮無効     Z_NO_COMPRESSION(=0)
//                  int method, .... 圧縮方法     zlib-1.2.6 で指定できるのは Z_DEFLATED のみ
//                  int windowBits,  ウィンドウ・サイズ gzip 形式では  31 を指定する必要あり
//                  int memLevel, .. メモリの消費量を指定 大きくても特に嬉しいことはない. デフォルト8
//                  int strategy); . 圧縮の方式を指定。デフォルトは Z_DEFAULT_STRATEGY
deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY)

これによって、出力バッファの合計長 stream.total_out や、出力データのバッファ上の残量 stream.avail_out などの出力関連のパラメータが設定されます。

圧縮したデータをstreamに出力する

int result = deflate(&stream, Z_FINISH);
// 戻り値の意味
// Z_OK ... まだ出力できる範囲が残っているのでもう一回deflate()が呼び出せる
// Z_STREAM_END ... もう出力できる範囲がない
// Z_BUF_ERROR ... 入力データ、出力バッファの一時的な不足
// Z_STREAM_ERROR ... next_in, next_outがNULLの時や内部状態が破壊された

deflate を実行するたびに stream.next_out で指定されたポインタのバッファへ圧縮結果が書き込まれます。 この出力先のポインタは書き込むたびに書き込んだ分ずらす必要があります。

stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out;
// ... 出力先のバッファのポインタの位置をこれまで書き込んだ分 stream.total_out だけずらして設定
// [NSMutableData mutableBytes] はNSMutableDataで管理している生のバイト列へのポインタを返してくれる

また、出力を書き込むたびにバッファ残量の値が減少するので、不足するようであれば書き込み先のバイト列の長さを伸ばしたり、次に書き込む前に出力バッファ残量が不足しないよう設定し直してやる必要があります。

if (stream.total_out >= [data length])
{
    data.length += ChunkSize;
}               
stream.avail_out = (uInt)([data length] - stream.total_out);

圧縮処理に使ったメモリの解放

最後に後始末のため deflateEnd(&stream) を呼び出して、使ったメモリを開放します。

deflateEnd(&stream);

最終的に書いたみたコード

以上を踏まえつつ、

のコードを参考にして書くとこういうコードになりました。

#import <Foundation/Foundation.h>
#import <zlib.h>

static const NSUInteger CHUNK_SIZE = 1000; // バイト列を伸ばす処理を確認するために小さめにしている
static const int GZIP_WINDOW_BITS = 31;
static const int DEFAULT_MEMORY_USAGE = 8;

+ (NSData *)compress:(NSData *)sourceData
{
    if ([sourceData length])
    {
        z_stream stream;
        stream.zalloc = Z_NULL;
        stream.zfree = Z_NULL;
        stream.opaque = Z_NULL;
        stream.avail_in = (uint)[sourceData length];
        stream.next_in = (Bytef *)[sourceData bytes];
        stream.total_out = 0;
        stream.avail_out = 0;
        
        if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, GZIP_WINDOW_BITS, DEFAULT_MEMORY_USAGE, Z_DEFAULT_STRATEGY) == Z_OK)
        {
            NSMutableData *data = [NSMutableData dataWithLength:CHUNK_SIZE];
            int result = 0;
            while (result == Z_OK)
            {
                if (stream.total_out >= [data length])
                {
                    data.length += CHUNK_SIZE;
                }
                stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out;
                stream.avail_out = (uInt)([data length] - stream.total_out);
                result = deflate(&stream, Z_FINISH);
            }
            deflateEnd(&stream);
            if (result != Z_STREAM_END) {
                return nil;
            }
            data.length = stream.total_out;
            return data;
        }
    }
    return nil;
}

@end

参考

どー考えてもライブラリのリンク関連についてまた勉強する必要がありますが、それはおかわり案件にします。現場からは以上です。

*1:ライブラリ用のプロジェクトなので /path/to/libz.tbd is not an object file (not allowed in a library) とエラーメッセージが出てテスト用にビルドできなくて... という場合はテスト用のターゲットに追加する

Heartbeatとブロックデバイスについて

短いですが、一応何かしら書くぞということで、本を読んでいたら「Heartbeatがスタンバイしたサーバを有効にする」「ブロックデバイスレプリケーション」と出てきたのが意味がわからなかったのでメモ。 

Heartbeat関連

  • クラスタに関することでハートビートといえばHAクラスタを実現する上で利用される仕組み
    • HA(High Availability)クラスタクラスタリングの中の一種で、あるサービスを提供するサーバが落ちたら代替機がそのサービスを引き継ぐようにするようなクラスタ
    • サーバ間で一定の間隔でパケットを送りあってお互いが生きているか死活監視している
    • 一定以上他のノードからパケットが届かなくなったらフェイルオーバーを作動させる
  • クラスタの種類
  • ハートビート色々
    • http://www.atmarkit.co.jp/ait/articles/0711/13/news139_2.html の記事では
      • Heartbeat … オープンソースソフトウェア「Heartbeat」全体
      • heartbeat … 上記のHeartbeatで提供されるプログラム、もしくはそれを起動したときのプロセス
      • ハートビート … HAクラスタを構成するPCが相互に状態監視をする際のネットワーク構成、およびその通信を指すものとします
        • 数ページしか調べてない時点で上記とちょっと違う定義が出て来ていて、一番上の行にあるのがそれ。要注意単語。
  • ハートビートの通信をどう行うか
    • シリアル通信や、ハートビート用に確保したLANを利用したり。
    • 監視対象のサーバに外部からのリクエストがたくさん来て高負荷のときに、輻輳に巻き込まれてハートビートのパケットが失われないように、アプリケーション本体の動作に使われるネットワークを避けたほうがいいらしい

Linux系OSのブロックデバイスとキャラクタデバイス

  • Linux系OSでは、デバイスはコンピュータに接続された周辺機器のことをデバイスと呼び、 /dev 以下に作成されるデバイスファイルが周辺機器のハードウェアへのインタフェースとなっている
  • ブロックデバイスとは、ある一定の量(通常512~2048バイト)の読み書きをランダムアクセスで行えるデバイスで、ディスク装置全般が該当する
  • キャラクタデバイスはデータの入出力をバイト(1文字)単位で扱うデバイスで、データの読み出しや書き込みがシーケンシャルアクセスとなる
    • 一度読み書きしたデータを後戻りして再び読み書きすることはできない
    • キーボードやマウスなどが該当する

参考

ウェブオペレーションまとめ用に読み直してるんですが、真面目に読むと11章や12章がとても重くて少しいやですね。わかんないものはわかんないで仕方ないですが。

*1:ここのwebアプリケーションサーバをEC2で調達したりする話を聞いたことが多いので、なんとなくクラスタ=クラウド感がありました

Amazon Athenaについて少し調べたのでメモ

まとまってないですが、忘れる前にメモ。

Amazon Athena とは

Amazon S3に入ったCSVJSONをAthena上で定義したスキーマに沿ってSQLで検索できるようにしてくれるクエリサービスです。

Athenaはサーバーレスのサービスで、利用するまでに最低限必要なステップは

  1. ファイルの入っているS3バケットの指定を含むスキーマの定義
  2. クエリを投げる

だけです。利用開始までにデータの抽出や変換、ロード(ETL)は必要なくなっています。ログ分析基盤などでやるようなアドホック分析に活用することができます。

スキーマというかテーブルの定義について

データベースはAthenaの管理画面の[Catalog Manager]などから作成可能です。 データベースごとにテーブルが100個まで作成できます*1

チュートリアルなどではテーブルをクエリエディタで定義していますが、この場合Hive互換のDDLを使って定義できます。

たとえばこういう形式のJSONが詰まったS3のバケットの中のファイルを検索させるためのテーブルを定義したい場合

{ "user_id": 12345, "data": { "imp_ids": [1,2,3,4,5] } }
{ "user_id": 23456, "data": { } }

このようにテーブルを定義することが可能です。

CREATE EXTERNAL TABLE IF NOT EXISTS array_tests (
  user_id String,
  data struct<imp_ids:array<INT>>
  )           
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://exmaple-woshidan-test/sample';

一度スキーマを定義した後にS3にあるファイルに変更を加えることなく後からスキーマを変更することも可能です。

ただし、Athenaで利用するS3のバケットの中のファイルはすべてスキーマの定義に沿った形式である必要があったり、同じバケットの中で違う形式のファイルを検索できるようにすることはできないので、ログ収集時に決めた部分を後から違うフォーマットで検索可能にすることは難しいです。

同じバケットの中でテーブルの定義にそぐわないフォーマットのファイルが見つかった場合は、そぐわない部分の列だけ空白の行が返ってきます。 アドホック分析なので途中から検索方法が違っても大丈夫、ということなのかもしれないです。

また、テーブルのパーティションについてはS3のバケットのパスに依存するので、こちらも後から変更することは難しい場合があります。

テーブルの変更はテーブルのドロップ -> 再作成という手順で行います。

RDBMSのテーブルと異なり、Athenaのスキーマ定義はS3の中のファイルを走査する際に利用されるメタデータのようなものなだけで、保存されているファイルのフォーマットがAthenaのスキーマ定義に影響を受けることはありません。

データの検索

データの検索もSQLのように行うことができます。 Athenaの検索の実行はPrestoを使って行われ、検索クエリを書く際はPrestoで利用できるキーワードの一部が利用可能となっています。

先ほどの例を用いていくらかクエリと実行結果を書いてみます。

SELECT * FROM mydatabase."array_tests" limit 10;
user_id data
1 12345 {imp_ids=[1, 2, 3, 4, 5]}
2 23456 {imp_ids=null}
SELECT * FROM mydatabase."array_tests" limit 10;
SELECT data.imp_ids FROM mydatabase."array_tests" WHERE user_id = '12345';
imp_ids
1 [1, 2, 3, 4, 5]

配列の検索は UNNESTCROSS JOIN を使って、配列の行を展開した行を生成することで可能なようです。パフォーマンスとかはあんまりわかってません。。

SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id);
user_id data imp_id
12345 {imp_ids=[1, 2, 3, 4, 5]} 1
12345 {imp_ids=[1, 2, 3, 4, 5]} 2
12345 {imp_ids=[1, 2, 3, 4, 5]} 3
12345 {imp_ids=[1, 2, 3, 4, 5]} 4
12345 {imp_ids=[1, 2, 3, 4, 5]} 5
SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id) WHERE imp_id = 3;
user_id data imp_id
12345 {imp_ids=[1, 2, 3, 4, 5]} 3

その他

Athenaでクエリを実行する先のS3に上げるJSONは1行

Athenaのクエリ実行先として指定するS3の中にあげるJSONは1行にminifyしておく必要があります。

Hive_CURSOR_ERROR: Row is not a valid JSON Object ... というエラーが出てよく見たら1行目の{あたりから怒られているような場合はこれが原因です。

参考

現場からは以上です。

*1:このあたりの制限は上限緩和申請が可能

「ライブ配信のなかみ[あれくま]」を読んだ

春の技術書典で手に取った時から、勝手にこの本は自分のための本だ、と思っていたのですが、仕事でバタバタしていまして昨日ようやく読みました。

読み終えてやっぱり自分にとってめちゃくちゃ面白かったので自己満足に感想をおいておきます。

この本を読む以前に関わっていたサービスでChromeFlashを使うと警告が出るようになるのを避けるため、動画再生画面で使うファイルの形式をflvからmp4に切り替えるという対応をしたことがあります。

その時、flvのファイルとFlashのプレイヤーの組み合わせだとストリーミング再生ができるのに、mp4とHTML5のvideo要素の組み合わせに切り替えると全てよみこむまで再生できないのをどうにかしたいのだけれど、どこをどう調べたらいいかわからなくてずっともやもやしていました*1

その辺りがこの本で触れられていて、めちゃくちゃ無知に対する恥ずかしさと嬉しさで悶えて死ぬかな、という感じです。

やっぱり感想うまくまとまらなかったので残りの感想は箇条書きにしておきます。。

  • ストリーミング配信のシークバーを実装する際に必要になるデータ形式の雰囲気から具体的なコーデックまで順を追って説明してもらえるのでめちゃくちゃわかりやすかった
    • 配信の遅延の原因でバッファにデータを貯める時間の話が面白かった
  • コーデックとコンテナの違いはこの本を読むまで知らず、動画のファイル形式、JavaScriptで二つ取れるなーみたいな感じで圧倒的無知で死にたさすらある
    • H.262のHが何由来か知らなかったので、MPEG-2 Videoと一緒なのを知った時嬉しさ
    • 各種コンテナ/コーデックの違いについて、たとえばネットワークの本であったりの一部として箇条書き一行ずつくらいで出てくるんだけど、全然覚えられなくて、この本見たら思い出話みたいに語ってあって全然違うものの話みたいで*2楽しい!!!
  • MicrosoftAdobeが独自規格をあきらめていくのにAppleお前ときたら…!
  • HTTPのプログレッシブダウンロードでは落としきっていないファイルを途中まででも無理やり再生プレイヤーが必要という話を聞いて、Flashのプレイヤー強い… 感がありました
  • 動画の配信をするメディアサーバの話をあまり聞いたことがないので新鮮でした

具体的な内容についてはぜひ下記リンクから買って確かめてくださいませ(ダイレクトマーケティング)。

www.dlsite.com

動画を扱うサービスのRails書くWebエンジニアやアプリ作るエンジニアでちょっと動画に興味があって調べたことある人ならきっとめちゃくちゃ面白いと思います。

現場からは以上です。

*1:結局動画ファイル全体の1%もないし、一旦置いておこうか、となった苦い記憶

*2:同じものだと思われます

インテントフラグとActivityStackのふるまいについていくらか

AndroidActivity のスタックの振る舞いを設定するものとして、

  • AndroidManifest.xml に記述する activity 要素の launchMode 属性
  • Activity を立ち上げる時に使う Intent に付与するインテントフラグ

があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。

今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、

  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_NO_HISTORY

あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */ 周辺のコードを編集した分を実行結果と一緒に紹介します。

TL;DR

長くなったので。

  • FLAG_ACTIVITY_SINGLE_TOP
    • launchModesingleTop と同じ
    • このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスonNewIntent()を呼びだす
  • FLAG_ACTIVITY_CLEAR_TOP
    • このフラグをつけて起動した Activity よりスタックの上にある Activity を破棄して、フラグをつけた Activity を呼び出す
    • FLAG_ACTIVITY_SINGLE_TOP との併用している、または、起動した ActivitylaunchModesingleTop の場合は既存のインスタンスが再利用される
    • そうでない場合は、既存の Activityインスタンスを破棄してから新しい Activityインスタンスを生成して起動する
  • FLAG_ACTIVITY_NO_HISTORY
    • このフラグをつけて起動した ActivityActivity のスタックに積まれない
    • = onActivityResult が決して呼び出されない

FLAG_ACTIVITY_SINGLE_TOP

まず、MainActivity を起動するときだけ、FLAG_ACTIVITY_SINGLE_TOP を利用してどのような挙動をするか見てみましょう。

public class MainActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるときはその上にActivityが積まれず

NextActivityが一番上にあるときは新しいMainActivityが積まれます。

launchModesingleTop と同じ振る舞いですね。

FLAG_ACTIVITY_CLEAR_TOP

MainActivityを表示しようとしたとき、FLAG_ACTIVITY_CLEAR_TOPインテントフラグを付与することにします。

public class MainActivity extends AppCompatActivity {

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

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるとき、すでにあるActivityが破棄されてから別のActivityが作り直されて一番上に積まれ直します。

Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@3a2375cd
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@21a00ad7
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@ac67751

何枚か NextActivity をスタックに積んでから MainActivity を起動し、その後バックキーを押すとアプリが終了します。

この場合のLog.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@2007c35
D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@25e3619b

もう少し別の条件で検証するとわかりやすいですが、起動しようとした MainActivity より上のスタックにある NextActivity と古い MainActivityを捨て、MainActivity を作り直して、呼び出していることがわかります。

ドキュメントのフラグ一覧のところの FLAG_ACTIVITY_CLEAR_TOP の欄には、

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

とあり、これは上記の挙動と異なるように見えます。

しかし、これは仕様どおりで、さらに FLAG_ACTIVITY_CLEAR_TOP単体の項目のドキュメントを読むと、FLAG_ACTIVITY_SINGLE_TOP との併用か、 ActivitylaunchModesingleTop に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。

それでは、 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_SINGLE_TOP との併用した場合の挙動を

public class MainActivity extends AppCompatActivity {

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

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d("MainActivity", "onNewIntent");
        Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show();
    }
public class NextActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
}

で試してみましょう。今度は、一覧の部分にあるように既存のMainActivityのインスタンスにIntentが送られるようになったみたいです。

FLAG_ACTIVITY_NO_HISTORY

どのActivityを起動するときも FLAG_ACTIVITY_NO_HISTORYインテントフラグを付与してがんがんActivityを起動してみます。

public class MainActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

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

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
}

たくさんスタックを積んだはずが、2回バックキーを押すとアプリが終了してしまいました。

最初の一回のバックキーで一番最初に起動した MainActivity へ戻ってしまったようです。

FLAG_ACTIVITY_NO_HISTORYインテントフラグをつけて起動されたActivityは、Activityのスタックに積まれないようです。

インテントフラグはActivityを起動する側が指定するので、使う側の都合によってActivityのスタック上での扱いを決める方法、みたいな感じなんですかね。

現場からは以上です。

参考

Intent | Android Developers

AndroidのCamera2 APIを触ってみた

ちょっと面白そうだったので調べました。

概要

https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと

  1. CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得
  2. Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成
  3. 1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る
  4. CaptureSession にリクエストをセットする
  5. リクエストが処理されて、 TotalCaptureResult (カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる

となるようです。それぞれ見ていきましょう。

なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。

CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得

  1. CanmeraManager を利用してそのデバイスで利用できるカメラのIdを取得
  2. カメラのIdと CameraDevice.StateCallbackインスタンスを利用して、CameraManager.openCamera()メソッドで CameraDevice をオープン
  3. CameraDevice.StateCallbackonOpened コールバックで CameraDeviceインスタンスを取得

利用するカメラの種類を制限したかったりする場合はCameraDevice をオープンする前に行います。

また、サンプルによるとプレビュー領域のサイズや向きなどを設定したい場合はプレビューを表示する TextureView に対して行うようです。

        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        String selectedCameraId = "";
        try {
            selectedCameraId = manager.getCameraIdList()[0];

            // https://github.com/googlesamples/android-Camera2Basic/blob/5dad16c103715b5e7e3c001cc5f6067f8d23f29e/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L499
            // あたりにあるのですが、顔用カメラを使いたくないなどがあれば、CameraCharacteristicsを経由して確認可能
            //            CameraCharacteristics characteristics
            //                    = manager.getCameraCharacteristics(selectedCameraId);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        try {
            manager.openCamera(selectedCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
        }

        @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(CameraDevice cameraDevice, int error) {
            cameraDevice.close();
            mCameraDevice = null;
        }

    };

CameraDeviceインスタンスから CameraCaptureSession を生成

Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成します。

  1. 出力先として TextureView を用意する
  2. TextureView から SurfaceTexture を取得して Surface を生成する
  3. 2のSurface をListに入れて、CameraCaptureSession.createCaptureSession() メソッドを呼び出す

2の Surface は、 ImageReaderMediaCodec など他の出力先クラスからも生成/取得が可能です*1

// Activity.onCreate内
        mTextureView = (TextureView) findViewById(R.id.texture);
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                // 先ほどのカメラを開く部分をメソッド化した
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                return true;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {

            }
        });
   private CameraCaptureSession mCaptureSession = null;

// ...

    private void createCameraPreviewSession() {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            texture.setDefaultBufferSize(320, 240); // 自分の手元のデバイスで決めうちしてます
            Surface surface = new Surface(texture);
        try {
            mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession session) {
                    // カメラがcloseされている場合
                    if (null == mCameraDevice) {
                        return;
                    }
                }

                mCaptureSession = session;

                @Override
                public void onConfigureFailed(CameraCaptureSession session) {

                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る

CaptureRequest はカメラが何ができるかなどの情報が必要なため、 CameraDevice クラスが CaptureRequest.Builder のファクトリメソッドを持っていますので利用します。

リクエストには色々ありますが、今回のリクエストは Target に与えた Surface によくあるプレビューの表示を作るための画像データを送ってくれ、というものです(たぶん)。

private CaptureRequest.Builder mPreviewRequestBuilder;
private CaptureRequest mPreviewRequest;
...

// createCameraPreviewSession メソッド内で
        try {
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);
            mPreviewRequest = mPreviewRequestBuilder.build();
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

CaptureSession にリクエストをセットする

先ほど生成したリクエストに加えて、 CameraCaptureSession.CaptureCallback が必要です。 プレビュー表示のいろいろなエラーハンドリングをする場合は結構書く必要があるみたいですが、今回はその辺は考えないのでnullを与えています。

この時点で TextureView を置いた領域にカメラで撮影した範囲が表示されているのが確認できます。

mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession session) {
        // カメラがcloseされている場合
        if (null == mCameraDevice) {
            return;
        }

        mCaptureSession = session;

        try {
            session.setRepeatingRequest(mPreviewRequest, null, null); // `CaptureSession` にリクエストをセットする
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

TotalCaptureResult へ送られてきた1フレーム分への画像データの処理について

前節で TotalCaptureResult へデータが送られてくるところまでは確認しましたが、 プレビューだけ延々と表示しても仕方ないので、ここへ飛んできたデータを利用する方法の一例を書いておしまいにします。

下記のコードでは、ボタンを押すとプレビューの表示を止めて、プレビューに表示している画像をファイルへ保存しています。

        Button capture = (Button) findViewById(R.id.button_capture);
        capture.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    mCaptureSession.stopRepeating(); // プレビューの更新を止める
                    if(mTextureView.isAvailable()) {
                        File file = new File(getFilesDir(), "surface_text.jpg");
                        FileOutputStream fos = new FileOutputStream(file);
                        Bitmap bitmap = mTextureView.getBitmap();
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
                        fos.close();
                    }
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

真剣にやろうと思ったらActivityやFragmentのライフサイクルに合わせてリソースの解放とか色々あるんですが今日は流れを把握するためにこんな感じで。

プレビューの表示止めてファイルに書き込む部分まで含めて全体のコードは以下となります。一番初めに書きましたが、今回はCamera2 APIの流れだけを把握しようと例外処理を省くため、targetSDKVersion=21にしているので少々注意してお試しください。

Camera2_API_Sample · GitHub

現場からは以上です。

参考