Blog -
Androidアプリに共有機能などを付ける際に、onCreateでUriを貰って処理を行う
なんて動作を考えたりする、という事でやってみた。
画像をUriで受け取ってUriがNullだったらToastを出して終わらせる。
ACTION_SENDが渡って来たら、外部からの共有で来ている物
そう出なければアプリケーション内から呼び出して物
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.action_send); Uri mUri = null; String action = getIntent().getAction(); if (Intent.ACTION_SEND.equals(action)) { //ACTION_SENDで呼び出し Bundle bundle = getIntent().getExtras(); if (bundle != null) { mUri = (Uri)bundle.get(Intent.EXTRA_STREAM); } intent_flg = true; } else { //アプリケーション内から呼び出し Bundle bundle =getIntent().getExtras(); mUri = (Uri)bundle.get("data"); intent_flg = false; } if(mUri == null ){ if (Intent.ACTION_SEND.equals(action)) { Toast.makeText(getApplicationContext(),"Error", Toast.LENGTH_LONG).show(); finish(); } else { //Uriから画像を読み込み、ImageViewにセットする。 Intent intent = getIntent(); setResult(-1, intent); finish(); } } //UriからBitmap抽出やCopyなど }
もし受け取ったUriがNullだったら、Toastを出すかSetResultでエラーを返して finish()します。
ですが、これを実行するとエラーが発生します。
java.lang.RuntimeException: Unable to start activity
これは何故かというと 、finish()を実行した際にonDestroyが呼ばれた後にまたonCreateに戻り
finish()以降のコードが実行される為です、 onCreateが完了するとonDestroyが呼ばれて実行されます。
つまり、onDestroyはfinish()時にスケジュールされるだけだという事です。
エラーで回避したと思っていたが思わぬ処でエラーが発生しました。
その様な、ライフサイクルなのでメンバ変数などの初期化や扱いも注意しないと。
開発しているとXMLをいじる事が多い、特にAndroidはレイアウトや言語定数など
XMLで記述する、またその専用エディタもSDKに含まれている。
ただ、XMLをいじった後に、デバッグの実行や実行などを行うとコンソールに
Error in an XML file: aborting build.
が発生して、XXXXXXX.out.xmlというファイルが作成されて中止されてしまう。
これは、Eclipse側のバグではないかという話ですが、対応策をメモ代わりに記事にします。
簡単な話ですが、XXXXXXX.out.xmlを消して(すぐさま消せないのでバックグラウンドの処理が終わったら)
プロジェクトのクリーンを行ってパッケージエクスプローラーで右クリック、実行またはデバッグから
Androidアプリケーション()を実行すると、エラーにならずに実行される。
Eclipse側のXMLへのチェック機構の問題か稀に発生するのでちょっと面倒ですがこの方法で回避可能です。
文字列の操作、連結でとてもスピードの速いStringBuilderですが
私がハマったところ(ただのおっちょこちょいですが)の原因と注意を記事にします。
StringBuilderをメンバーで持って使いまわす場合に初期化の方法として、setLength(0)を使います
こうする事で、初期化と同じ行為(ポインタを初期化)する事が出来ます。
private StringBuilder mStringBuilder = new StringBuilder(); //初期化 mStringBuilder.setLength(0);
これを分岐に使用してifで比較します。
private StringBuilder mStringBuilder = new StringBuilder(); //初期化 mStringBuilder.setLength(0); int ret = 0; //1-3の乱数 ret = (int)Math.floor(Math.random()*(3+1-1))+1; switch(ret){ case 1: mStringBuilder.append("A"); break; case 2: mStringBuilder.append("BB"); break; case 3: mStringBuilder.append("CCC"); break; default: mStringBuilder.append("A"); break; } if(mStringBuilder.equals("A")){ Log.d(TAG,mStringBuilder.toString); }
これを、何度か繰り返します。
A → BB → CCC → A と発生した場合最後のAではFALSEとして評価されます。
これは何故かというと、mStringBuilderはポインタをリセットしましたが中身のメモリがクリア
された訳では無いからです。
実際、最後のif分の評価は、ACC == A を評価していることになります。
if(mStringBuilder.toString().equals("A")){ Log.d(TAG,mStringBuilder.toString); }
この様に比較すると想定通り評価出来ます。
これにハマって余分にデバッグしてしまった^^;
VideoViewでランダムで取得した動画を再生する機能を作成中に
MediaPlayer error(1,-2147483648)が発生する、再生自体に問題無い様だが、気持ちが悪い
調べても、殆どが英文で、拙く訳してもGoogleに聞けとかコメントの無い物も多かった。
その中でもいくつかヒントを得て試行錯誤した結果、回避したので記事にします。
どうも発生する原因は、動画読み込み中に再生を始めたとか動画再生中にほかの動画を読み込んだとか
そう言った感じのエラーと解釈した。
まず最初のコード(簡略化)
private static VideoView mVideoView; //中略 //ランダムでリソースIDをゲット-rawから取得 int movRId = getMovie(); mVideoView.setVideoURI(Uri.parse( "android.resource://" + mContext.getApplicationContext().getPackageName() + "/" + movRId )); mVideoView.start();
ボタンクリックにて毎回ランダムにIDを取得して再生させる。
これでエラーが発生するも、再生自体には問題無いただ、ボタンの連打が怖いので加工する。
onCompletionは再生終了後にコールバックされるので開始と合わせてフラグを設定する。
private static VideoView mVideoView; private static boolean mov_mode = false; mVideoView.setOnCompletionListener(new OnCompletionListener () { public void onCompletion(MediaPlayer mp) { mov_mode = false; //動画終了 } }); //中略 //ランダムでリソースIDをゲット-rawから取得 if(!mov_mode){ mov_mode = true; int movRId = getMovie(); mVideoView.setVideoURI(Uri.parse( "android.resource://" + mContext.getApplicationContext().getPackageName() + "/" + movRId )); mVideoView.start(); }
連打の抑止は出来たもののエラーはまだ発生している。
次に、リソース読み込み前に再生されている可能性への対応をする。
onPreparedは、読み込み完了時にコールバックされる。
mVideoView.setOnPreparedListener(videoViewPrepared); private OnPreparedListener videoViewPrepared = new OnPreparedListener() { public void onPrepared(MediaPlayer mp) { mVideoView.start(); } }; //中略 //ランダムでリソースIDをゲット-rawから取得 if(!mov_mode){ mov_mode = true; int movRId = getMovie(); mVideoView.setVideoURI(Uri.parse( "android.resource://" + mContext.getApplicationContext().getPackageName() + "/" + movRId )); //mVideoView.start(); onPreparedで再生するのでコメント }
これで、setVideoURI時にonPreparedがコールバックされて再生される事になる。
しかし、まだエラーが発生している。読み込み遅延でのエラーでは無いらしい。
次に、再生中に次の読み込みを始めている可能性を考えるが onCompletionが発生しているので
再生中という事は無いだろうと判断する デバッグを何度か見直すとどうやら初回は発生しない、
という事は2回目の読み込みの 前か同時に発生している事になる。
手始めに、リソースを初期化する事にした。
mVideoView.setOnCompletionListener(new OnCompletionListener () { public void onCompletion(MediaPlayer mp) { mov_mode = false; mVideoView.setVideoURI(null); //動画終了 } });
と、発生しなくなった。
VideoURIに再生出来るリソースが読み込まれているのに読み込むと出る様だ。
いちいち空にしろということなのかストリームから取り込めという事なのか、とにかく回避した。
開発者にとって、リバースエンジニアリングは知的財産の侵害です。
最近では、オープンソースの大変ありがたい知識が豊富にありますが
それとはまた違った形でソースを守りたいというニーズが多様にあると思います。
特に商業的な開発は、内部に多数のブラックボックス化したいアクセスなども含みますし
新発明(あまり良い表現ではないですが)は守られて当然だと思います。
この辺は、モラルの問題もあると思いますがやはり苦労した物は守りたい物です。
AndroidのSDKは2.2からproguardという難読化のツールを使ってapkを難読化出来ます
利用すれば、完全で無いまでも解読にはそれなりの努力と投資が必要となります。
もうちょっとなんとかして欲しいなあと思いますが、Googleさんお願いします。
(OSがオープンで厳しいのは分かりますがなにとぞよろしく)
それと、もうひとつのタイトルのエラーですが
Eclipseでproguardを使うといろいろエラーが出ます、特にSDK15はお手上げでしたが
苦労の末、回避出来ました(といっても大した事ないですけど)ので備忘録として記事にします。
Proguard returned with error code 1. See console がコンソールに出てエラーになる場合
1.難読化対象のパッケージ、クラス内に参照先が無い物が含まれている。
2.プラグイン関係のエラー
3.コンパイル順(同名のパッケージ、クラスの参照が目的の物で無い)
4.バージョンが低い(proguard)
1の場合、proguard.cfgに対象として含めない様に設定する。
(Twitter4jなど必要なlibだけ組み込む場合に参照先が無い場合がある)
-dontwarn twitter4j.**
これでこのパッケージは難読化対象では無くなる様です。
2の場合、eclipse.exe -cleanで回避出来る事が多い。
3の場合、ビルド・パス→ビルドパスの構成→順序およびエクスポートで該当パッケージの優先度を上げる
4の場合、自分はこれだったんですが、SDK15の場合proguard4.4だとこれが発生するようです。
最新が4.6ですのでここで入手して(記事執筆時点では4.7が最新)解凍後のlibフォルダの中身を
Android SDKのフォルダtools/proguard/libへ上書きします。
これで回避出来ました。
・リソースにある画像を順番に入れ替える
・特定のコードに当てはまる、リソース内に定義してあるArrayを呼び出したい。
リソースID(一部) + 変数でリソースを取得したいという処理があります。
こんな時に便利なのが
i = 1; picture_id = "pict_" + (i);
pict_res_id = getResources().getIdentifier(Picture_id,"drawable", getPackageName());
Arrayの場合は
i = 1; Array_id = "Array_" + (i);
Array_res_id = getResources().getIdentifier(Array_id,"array", getPackageName());
ExpandableListなどで択ばれた親のコードを元に子供のArrayなどを呼び出すなどに応用出来ます。
これで、その4ですがやっと接続後の処理への理解になります。
前回までに接続の確立まで行いましたが
個人的に注意点として
・接続した後に、有効範囲からどちらかが居なくなる。
・どちらかの電源が切れる。
いつ切れるか解らない処理も考えながら作らないと実用的にはならない。
まだまだ、先は長いですが今回は、接続以降にどの様な処理があるか理解します。 いつもの様に、Threadで実現します。 宣言部
private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; //中略 }
中で使うメンバーを定義します、見た事がある奴も居ます。コンストラクタですが、BluetoothSocketからStreamを取得します。
以降、Streamとして処理が出来ます。
public ConnectedThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Get the input and output streams, using temp objects because // member streams are final try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { } mmInStream = tmpIn; mmOutStream = tmpOut; }
次にrun()メソッドが実行されます。
先に、Handlerについてですが、ThreadからUIに対して処理が行えません
これは非同期処理中に直接ThreadからUIへ更新を掛ける為です。考えれば非同期処理からUIを触りたい処理ばかりなのですが、やり方があります。
それは、Handlerを使う方法です。Handlerは送られてきたメッセージを受け取り、処理します非同期処理から非同期にHandlerに渡して
HandlerはそれをUIへ更新します。(正確には、postかmessageを使ってBundleなどとやり取りをするみたいです)
他に非同期処理はAsynctaskなどもあります。 今回は、HandlerのMessageを利用してUIとのやり取りを行っています。
(他にも方法はありますがobtainMessageでにやり取りの効率が良いらしいです)
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
対象のメソッドと引数は obtainMessage(int what, int arg1, int arg2, Object obj) ・MESSAGE_READは、Handlerで受け取った時に
どの処理に対してか識別します。・int bytesは第一引数・bufferは、InputStream(送られて来たデータ)
これを受け取って、UIへHandlerがMessageオブジェクトとして渡しています。 と話を戻しますが。
bytes = mmInStream.read(buffer);
のメソッドでストリームからデータが送られて来るまで待機します。
送られて来たら、UIへHandlerがMessageオブジェクトとして渡しています。
public void run() { byte[] buffer = new byte[1024]; // buffer store for the stream int bytes; // bytes returned from read() // Keep listening to the InputStream until an exception occurs while (true) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI Activity mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { break; } } }
また、こちらからデータを書き込む場合の処理を準備します。これは、メインの処理から呼ばれます。
/* Call this from the main Activity to send data to the remote device */ public void write(byte[] bytes) { try { mmOutStream.write(bytes); } catch (IOException e) { } }
キャンセル時の処理を準備しておきます。
/* Call this from the main Activity to shutdown the connection */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } }
今回までで、接続からデータの受信(正確にはサーバ側でデータ書き込みは解説してませんが)まで理解出来たかと思います。
もう少し深く理解して、後の処理も考えたいと思っています。
※GoogleDevelopers チュートリアルを使用しています。
次にクライアント側へ理解を広げたいと思います。
クライアント側はどうなっているのか
【クライアントとしての接続】
クライアントとしては、接続相手を探したおく必要があります。
接続相手は、発見可能な状態であってBluetoothServerSocketがリスニング状態である必要があります。
ここでは、相手を見つけた後の処理について書きます。
この処理も、Threadで実現するべきです。まず定義から
private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; //中略 }
BluetoothSocketとBluetoothDeviceをメンバーに持たせています。
public ConnectThread(BluetoothDevice device) { // Use a temporary object that is later assigned to mmSocket, // because mmSocket is final BluetoothSocket tmp = null; mmDevice = device; // Get a BluetoothSocket to connect with the given BluetoothDevice try { // MY_UUID is the app's UUID string, also used by the server code tmp = device.createRfcommSocketToServiceRecord(UUID); } catch (IOException e) { } mmSocket = tmp; }
コンストラクタでは、BluetoothSocketを初期化してBluetoothDeviceを受け取ります。
BluetoothDeviceからcreateRfcommSocketToServiceRecord(MY_UUID)を発行する事で BluetoothSocketが相手のBluetoothServerSocketから戻されます。
その2で説明したBluetoothServerSocket開始時に指定したMY_UUIDと同じ事を前提にします。
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
成功すれば、BluetoothSocketが返されます。続いて、run()メソッドが開始されます。
public void run() { // Cancel discovery because it will slow down the connection mBluetoothAdapter.cancelDiscovery(); try { // Connect the device through the socket. This will block // until it succeeds or throws an exception mmSocket.connect(); } catch (IOException connectException) { // Unable to connect; close the socket and get out try { mmSocket.close(); } catch (IOException closeException) { } return; } // Do work to manage the connection (in a separate thread) manageConnectedSocket(mmSocket); }
cancelDiscovery()を最初に行っていますが、Discovery()(接続相手の検索)中である事が予測できる為接続前に中止します、
BluetoothAdapterは一つであればどのインスタンスからでもキャンセル出来ます。 その後、取得したBluetoothSocketに対してconnect()を行います。
Thread処理のキャンセルに対応して処理を作っておきます。
/** Will cancel an in-progress connection, and close the socket */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } }
これで、サーバ、クライアント両方の接続までは理解出来たかと思います。
なぜ、Threadなのか?に関しては、後日時間が有れば解説します。
※GoogleDevelopers チュートリアルを使用しています。
前回は、設計に際しての概要まで纏めましたが 今回はちょっと具体的に
【サーバとしての接続】
private class AcceptThread extends Thread { private final BluetoothServerSocket mmServerSocket; //中略 }
AcceptThreadというThreadを作って、そのコンストラクタで
public AcceptThread() { // Use a temporary object that is later assigned to mmServerSocket, // because mmServerSocket is final BluetoothServerSocket tmp = null; try { // MY_UUID is the app's UUID string, also used by the client code tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { } mmServerSocket = tmp; }
BluetoothServerSocketをの有無を確認して無ければ(無いというのは、接続中か接続後か)
BluetoothServerSocketを用意する、Threadなのでrun()メソッドが開始される。
public void run() { BluetoothSocket socket = null; // Keep listening until exception occurs or a socket is returned while (true) { try { socket = mmServerSocket.accept(); } catch (IOException e) { break; } // If a connection was accepted if (socket != null) { // Do work to manage the connection (in a separate thread) manageConnectedSocket(socket); mmServerSocket.close(); break; } } }
run()の中では、BluetoothSocketを初期化してaccept()を発行しリスニングを開始します。
クライアントがBluetoothSocketを要求するとBluetoothSocketがsocketへソケットを用意します。
socket(接続以降)の処理を開始して、BluetoothSocketをclose()して終了します。 また、処理を中断した時の処理も記述します。
public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } }
これで、サーバソケットでの待機が理解出来ました。
注意点として、accept() が BluetoothSocket を返すときは、ソケットがすでに接続済みでなので
connect() メソッドを呼び出す必要はありません ( クライアントサイドでしている想定 )
※GoogleDevelopers チュートリアルを使用しています。
ただ、余り文献が無いという事で、調べた事をメモ的に記載します。
(基本的に2.1以降を対象とします)
【Android内で抽象化されたBluetoothのクラス】
BluetoothAdapter・・・ハードその物を表します。
BluetoothDevice・・・リモートのデバイスを表します、いわゆる相手。
BluetoothSocket・・・相手との通信にStreamを利用する場合のソケット。
BluetoothServerSocket・・・リクエストを待ち受けしているサーバソケット
BluetoothClass・・・Bluetoothのプロパティセット(読み込み専用)
大きくはこんな所、ここでちょっと解りににくいポイントのまとめ
・サーバは待ち受けるリスナー。
・クライアントは、サーバへ接続に来る。
・接続には一般的にSocketを利用する。
【パーミッション】
Bluetoothを利用するには
BLUETOOTH
BLUETOOTH_ADMIN
が必要になる。
【発見機能】
通常、BluetoothはON/OFFですが最近ではむやみに発見されない様に
発見させるかさせないかのON/OFFが機能として実装されています。
【デバイスの接続】
サーバサイドとクライアントサイドの両方の機構で実装が必要になる。
(まあ当然ですが)
同一の RFCOMM チャネル上で、それぞれが接続済みの BluetoothSocket
を受け取ったときに接続されたとみなされます。
ポイント
・ペアリングされたデバイスの場合はこの限りではない
・ペアリングされていないデバイスは検索してBluetoothDeviceを得る必要がある
(そのMACアドレス)
【設計のポイント】
2つの端末で通信をする場合に、どちらかがサーバで一方がクライアントの動きをする
と言う感じになる、先にサーバでSocketを獲得した方がサーバとして振る舞い接続を確立する。
サーチ
発見
BluetoothDeviceを獲得
Sockeを要求、取得
通信開始
と同時に
発見可能へ
サーバソケットリスニング開始(accept())
接続要求受領
Socketを提供
*サーバソケットリスニング停止(破棄)
通信開始
通信終了
発見可能へ
サーバソケットリスニング開始
とこうなる訳ですが、重要なのは※の部分
サーバソケットを停止(破棄していますが)RFCOMMでは同時通信はありません(チャネルあたりひとつ)ですので
接続を待つ必要性がなくなります。よって高負荷の処理は破棄します。
破棄してもSocketは破棄されない為、通信は可能です。
また、acceptはブロックされた呼び出しである為、AndroidのUI(シングル)では高負荷ですのでActivityのUIで
実行するのは現実的ではありません、別スレッドで処理します。
その2へ続く