Android最速の フォトグラファーに 俺はなる!! @kassy_kz for yapf 2014 / 5 / 3
Jun 20, 2015
Android最速の フォトグラファーに 俺はなる!!
@kassy_kz
for yapf 2014 / 5 / 3
最速になりたい
自己紹介
• 名前:@kassy_kz (かっしー)
• 二つ名:神奈川の健全王(自称) 多摩川の直線鬼(自称)
• 趣味:カメラ (フォトマスター1級所持)
フォトグラファーにとって 最も重要なファクターとは
それはスピード※個人の意見です
一瞬のシャッターチャンスを逃さない
それこそが真のフォトグラファー※あくまで個人の意見です
つまり必要なのは スピード!
Androidでカメラの起動速度を極める
最速のフォトグラファーを目指して
通常の、カメラ起動までの流れ
2~5ステップ
これではチャンスを逃してしまう…
端末の状態に関わらず ワンステップでアプリを起動する
目標
Google Nowの起動シーケンスに着目
端末がどんな状態であろうと 「ホームボタン上スライド」
の1アクションで Google Nowが起動する
ヨコが余ってんじゃね?
このサークルのヨコに アイテムを追加できれば
「ホームボタン横スライド」 のワンアクションで
アプリを起動できるのでは!?
よろしい !
ならば(ROMの)改造だ
着目するのは PhoneStatusBar
この部分は PhoneStatusBarというところで実装されている
パスは/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/
PhoneStatusBarのコードを読む
private void prepareNavigationBarView() { (中略) mNavigationBarView.getHomeButton().setOnTouchListener(mHomeSearchActionListener); updateSearchPanel(); }
PhoneStatusBar.java
↑ Homeボタンをおした時のリスナーを登録してる箇所を発見!
View.OnTouchListener mHomeSearchActionListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { (中略) mHandler.postDelayed(mShowSearchPanel, mShowSearchHoldoff); (中略) };
PhoneStatusBar.java
↑ リスナーの実装はこれ、mShowSearchPanelメソッドを呼び出している!
PhoneStatusBarのコードを読む
@Override public void showSearchPanel() { int msg = MSG_OPEN_SEARCH_PANEL; mHandler.removeMessages(msg); mHandler.sendEmptyMessage(msg); }
BaseStatusBar.java (PhoneStatusBarの基底クラス)
↑ handlerにメッセ投げている
protected class H extends Handler { public void handleMessage(Message m) { switch (m.what) { case MSG_OPEN_SEARCH_PANEL: if (DEBUG) Log.d(TAG, "opening search panel"); if (mSearchPanelView != null && mSearchPanelView.isAssistantAvailable()) { mSearchPanelView.show(true, true); onShowSearchPanel(); } break; } } }
↑ handlerの受け側。SearchPanelViewのshow() メソッドをコールしている
Homeキー押下で出現するこのViewは SearchPanelViewと言うらしい
更にSearchPanelViewのコードを読む
@Override protected void onFinishInflate() { (中略) mGlowPadView = (GlowPadView) findViewById(R.id.glow_pad_view); mGlowPadView.setOnTriggerListener(mGlowPadViewListener); }
SearchPanelView.java
↑ 入れ子にGlowPadViewというViewが貼られている事がわかる。
public GlowPadView(Context context, AttributeSet attrs) { (中略) if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { internalSetTargetResources(outValue.resourceId); }
GlowPadView.java
↑ 独自カスタムViewの書き方をしている。 XMLファイルの「targetDrawables」というところから値を取ってきている事がわかる internalSetTargetResources以下では、drawbleの配置を行っている。
さらに読む(そろそろ疲れてきた)
prvandroid:targetDrawables="@array/navbar_search_targets”"
<array name="navbar_search_targets"> <item>@null</item> <item>@*android:drawable/ic_action_assist_generic</item> <item>@null</item> <item>@null</item> </array>
/frameworks/base/packages/SystemUI/res/layout/status_bar_search_panel.xml
↑ array/navbar_search_targetsというところを参照している
/frameworks/base/packages/SystemUI/res/values-land/arrays.xml
↑ drawable4つを格納する配列、 そして1つを除いてnullが格納されているという謎の状態
drawableを埋めてみた
<array name="navbar_search_targets"> <item>@*android:drawable/ic_lockscreen_camera_activated</item> <item>@*android:drawable/ic_action_assist_generic</item> <item>@*android:drawable/ic_lockscreen_camera_activated</item> <item>@*android:drawable/ic_lockscreen_camera_activated</item> </array>
/frameworks/base/packages/SystemUI/res/values-land/arrays.xml
↑nullの箇所に自分で持ってきたdrawableのIDを埋め込んでみた
画像は以下の場所に良さげなのがあったので流用 /frameworks/base/packages/Keyguard/res/ drawable-xhdpi/ic_lockscreen_camera_activated.png
追加画像がよしなに配置される!
明らかにヨコへの配置を前提にしたコードだった 将来のバージョンアップによってはもしかしたら…
アクションの取得はどうやってるのか
class GlowPadTriggerListener implements GlowPadView.OnTriggerListener { public void onTrigger(View v, final int target) { final int resId = mGlowPadView.getResourceIdForTarget(target); switch (resId) { case com.android.internal.R.drawable.ic_action_assist_generic: mWaitingForLaunch = true; startAssistActivity(); vibrate(); break; } }
SearchPanelView.java
SearchPanelView
GlowPadView
アクションの取得はどうやってるのか
リスナー登録 コールバック(引数はdrawableID)
SearchPanelView
GlowPadView
カメラ起動コードを埋め込む
class GlowPadTriggerListener implements GlowPadView.OnTriggerListener { public void onTrigger(View v, final int target) { final int resId = mGlowPadView.getResourceIdForTarget(target); switch (resId) { case com.android.internal.R.drawable.ic_action_assist_generic: mWaitingForLaunch = true; startAssistActivity(); vibrate(); break; case 17302260: final Intent intent = new Intent(); postDelayed(new Runnable() { public void run() { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction("android.media.action.IMAGE_CAPTURE"); mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); } }, SEARCH_PANEL_HOLD_DURATION); break; } }
SearchPanelView.java
追加
←自分で追加したdrawableのID
デモ
神奈川の健全王が シャッターチャンスに遭遇してから シャッター押下までのタイムは わずか0.8秒 (自分調べ) にすぎない
では、 その撮影プロセスを もう一度見てみよう
カメラ起動まで常にワンアクション!
これで決まりだ!
上司ゴースト
大体最初から ユースケースの整理が
配慮がたらんわ バカもんが
端末がスリープ状態のときはどうするんだ?
あ?
orz
端末スリープ状態から ワンアクションでカメラを起動する
次の目標
イヤホンのボタンに着目
コレ
端末がスリープ状態でも ボタンを押せば音楽の再生停止ができる
これを使えば スリープ状態からでも
なんらかのアクションを起こせるのでは
問題はどうやってマイクボタン押下を検出するかだ
ボタン押した時のログを見てみる
04-13 12:51:14.386: W/ContextImpl(28640): Implicit intents with startService are not safe: Intent { act=com.google.android.music.NETWORK_MONITOR_SERVICE } android.content.ContextWrapper.bindService:517 com.google.android.music.utils.SafeServiceConnection$ServiceConnectionImp.bindService:98 com.google.android.music.utils.SafeServiceConnection.bindService:259 04-13 12:51:14.406: D/SystemUtils(28640): getSystemProperty: lpa.decode=null 04-13 12:51:14.406: I/MediaFocusControl(771): Remote Control registerMediaButtonIntent() for PendingIntent{4313edb0: PendingIntentRecord{431d35c8 com.google.android.music broadcastIntent}} 04-13 12:51:14.416: V/Avrcp(2343): New genId = 6, clearing = 1 04-13 12:51:14.416: V/Avrcp(2343): New genId = 7, clearing = 1 04-13 12:51:14.416: I/LocalDevicePlayback(28640): play: currentPos=-1 04-13 12:51:14.416: I/MediaFocusControl(771): AudioFocus requestAudioFocus() from android.media.AudioManager@426b6090com.google.android.music.playback.LocalDevicePlayback$9@42736830 04-13 12:51:14.416: V/Avrcp(2343): New genId = 8, clearing = 0 04-13 12:51:14.416: D/CacheService(28640): onCreate 04-13 12:51:14.416: D/CacheService(28640): onBind 04-13 12:51:14.426: W/ContextImpl(28640): Implicit intents with startService are not safe: Intent { act=com.google.android.music.NETWORK_MONITOR_SERVICE } android.content.ContextWrapper.bindService:517 com.google.android.music.utils.SafeServiceConnection$ServiceConnectionImp.bindService:98 com.google.android.music.utils.SafeServiceConnection.bindService:259 04-13 12:51:14.426: I/LocalDevicePlayback(28640): Streaming client created. 04-13 12:51:14.456: I/LocalDevicePlayback(28640): stop: willPlay=false, currentPos=20 04-13 12:51:14.466: V/Avrcp(2343): updatePlayPauseState, old=0, state=0 04-13 12:51:14.466: V/Avrcp(2343): position=0 04-13 12:51:14.466: V/Avrcp(2343): mMetadata=Metadata[artist=null trackTitle=null albumTitle=null] 04-13 12:51:14.466: V/Avrcp(2343): duration=0 04-13 12:51:14.466: V/Avrcp(2343): updatePlayPauseState, old=0, state=3 04-13 12:51:14.466: V/Avrcp(2343): position=0 04-13 12:51:14.476: I/LocalDevicePlayback(28640): open: id=[19, DEFAULT], pos=0, playPos=20, fromUser=true, track=to the beginning 04-13 12:51:14.486: D/dalvikvm(28640): GC_CONCURRENT freed 322K, 2% free 17459K/17812K, paused 2ms+1ms, total 12ms 04-13 12:51:14.486: D/LocalAsyncMediaPlayer(28640): Event logging MUSIC_START_PLAYBACK_REQUESTED: 19/: local playback 04-13 12:51:14.536: D/dalvikvm(28640): GC_FOR_ALLOC freed 33K, 2% free 17605K/17960K, paused 13ms, total 13ms 04-13 12:51:14.546: D/dalvikvm(28640): GC_FOR_ALLOC freed 2K, 2% free 17749K/18108K, paused 12ms, total 13ms 04-13 12:51:14.566: D/dalvikvm(28640): GC_FOR_ALLOC freed 471K, 5% free 17456K/18256K, paused 12ms, total 12ms 04-13 12:51:14.586: D/dalvikvm(28640): GC_CONCURRENT freed 3K, 3% free 17878K/18256K, paused 2ms+1ms, total 15ms 04-13 12:51:14.606: D/dalvikvm(28640): GC_FOR_ALLOC freed 9K, 3% free 17879K/18256K, paused 9ms, total 9ms 04-13 12:51:14.606: I/dalvikvm-heap(28640): Grow heap (frag case) to 17.768MB for 294928-byte allocation 04-13 12:51:14.616: D/dalvikvm(28640): GC_FOR_ALLOC freed <1K, 3% free 18167K/18548K, paused 10ms, total 10ms 04-13 12:51:14.616: V/Avrcp(2343): updatePlayPauseState, old=3, state=3 04-13 12:51:14.616: V/Avrcp(2343): position=170455 04-13 12:51:14.626: I/ActivityManager(771): Start proc com.android.musicfx for broadcast com.android.musicfx/.ControlPanelReceiver: pid=8691 uid=10013 gids={50013, 3003, 3002} 04-13 12:51:14.636: I/AudioFlinger(183): HAL output buffer size 32768 frames, normal mix buffer size 32768 frames 04-13 12:51:14.636: I/AudioFlinger(183): AudioFlinger's thread 0xb7fc0ce8 ready to run 04-13 12:51:14.636: D/dalvikvm(181): GC_EXPLICIT freed 42K, 1% free 16750K/16824K, paused 1ms+2ms, total 15ms 04-13 12:51:14.636: W/AudioFlinger(183): moveEffectChain_l() effect chain for session 0 not on source thread 0xb5ba1008
とりあえず
ボタン押した時のログを見てみる
04-13 12:51:14.386: W/ContextImpl(28640): Implicit intents with startService are not safe: Intent { act=com.google.android.music.NETWORK_MONITOR_SERVICE } android.content.ContextWrapper.bindService:517 com.google.android.music.utils.SafeServiceConnection$ServiceConnectionImp.bindService:98 com.google.android.music.utils.SafeServiceConnection.bindService:259 04-13 12:51:14.406: D/SystemUtils(28640): getSystemProperty: lpa.decode=null 04-13 12:51:14.406: I/MediaFocusControl(771): Remote Control registerMediaButtonIntent() for PendingIntent{4313edb0: PendingIntentRecord{431d35c8 com.google.android.music broadcastIntent}} 04-13 12:51:14.416: V/Avrcp(2343): New genId = 6, clearing = 1 04-13 12:51:14.416: V/Avrcp(2343): New genId = 7, clearing = 1 04-13 12:51:14.416: I/LocalDevicePlayback(28640): play: currentPos=-1 04-13 12:51:14.416: I/MediaFocusControl(771): AudioFocus requestAudioFocus() from android.media.AudioManager@426b6090com.google.android.music.playback.LocalDevicePlayback$9@42736830 04-13 12:51:14.416: V/Avrcp(2343): New genId = 8, clearing = 0 04-13 12:51:14.416: D/CacheService(28640): onCreate 04-13 12:51:14.416: D/CacheService(28640): onBind 04-13 12:51:14.426: W/ContextImpl(28640): Implicit intents with startService are not safe: Intent { act=com.google.android.music.NETWORK_MONITOR_SERVICE } android.content.ContextWrapper.bindService:517 com.google.android.music.utils.SafeServiceConnection$ServiceConnectionImp.bindService:98 com.google.android.music.utils.SafeServiceConnection.bindService:259 04-13 12:51:14.426: I/LocalDevicePlayback(28640): Streaming client created. 04-13 12:51:14.456: I/LocalDevicePlayback(28640): stop: willPlay=false, currentPos=20 04-13 12:51:14.466: V/Avrcp(2343): updatePlayPauseState, old=0, state=0 04-13 12:51:14.466: V/Avrcp(2343): position=0 04-13 12:51:14.466: V/Avrcp(2343): mMetadata=Metadata[artist=null trackTitle=null albumTitle=null] 04-13 12:51:14.466: V/Avrcp(2343): duration=0 04-13 12:51:14.466: V/Avrcp(2343): updatePlayPauseState, old=0, state=3 04-13 12:51:14.466: V/Avrcp(2343): position=0 04-13 12:51:14.476: I/LocalDevicePlayback(28640): open: id=[19, DEFAULT], pos=0, playPos=20, fromUser=true, track=to the beginning 04-13 12:51:14.486: D/dalvikvm(28640): GC_CONCURRENT freed 322K, 2% free 17459K/17812K, paused 2ms+1ms, total 12ms 04-13 12:51:14.486: D/LocalAsyncMediaPlayer(28640): Event logging MUSIC_START_PLAYBACK_REQUESTED: 19/: local playback 04-13 12:51:14.536: D/dalvikvm(28640): GC_FOR_ALLOC freed 33K, 2% free 17605K/17960K, paused 13ms, total 13ms 04-13 12:51:14.546: D/dalvikvm(28640): GC_FOR_ALLOC freed 2K, 2% free 17749K/18108K, paused 12ms, total 13ms 04-13 12:51:14.566: D/dalvikvm(28640): GC_FOR_ALLOC freed 471K, 5% free 17456K/18256K, paused 12ms, total 12ms 04-13 12:51:14.586: D/dalvikvm(28640): GC_CONCURRENT freed 3K, 3% free 17878K/18256K, paused 2ms+1ms, total 15ms 04-13 12:51:14.606: D/dalvikvm(28640): GC_FOR_ALLOC freed 9K, 3% free 17879K/18256K, paused 9ms, total 9ms 04-13 12:51:14.606: I/dalvikvm-heap(28640): Grow heap (frag case) to 17.768MB for 294928-byte allocation 04-13 12:51:14.616: D/dalvikvm(28640): GC_FOR_ALLOC freed <1K, 3% free 18167K/18548K, paused 10ms, total 10ms 04-13 12:51:14.616: V/Avrcp(2343): updatePlayPauseState, old=3, state=3 04-13 12:51:14.616: V/Avrcp(2343): position=170455 04-13 12:51:14.626: I/ActivityManager(771): Start proc com.android.musicfx for broadcast com.android.musicfx/.ControlPanelReceiver: pid=8691 uid=10013 gids={50013, 3003, 3002} 04-13 12:51:14.636: I/AudioFlinger(183): HAL output buffer size 32768 frames, normal mix buffer size 32768 frames 04-13 12:51:14.636: I/AudioFlinger(183): AudioFlinger's thread 0xb7fc0ce8 ready to run 04-13 12:51:14.636: D/dalvikvm(181): GC_EXPLICIT freed 42K, 1% free 16750K/16824K, paused 1ms+2ms, total 15ms 04-13 12:51:14.636: W/AudioFlinger(183): moveEffectChain_l() effect chain for session 0 not on source thread 0xb5ba1008
アヤシイ
I/MediaFocusControl(771): Remote Control registerMediaButtonIntent() for PendingIntent{4313edb0: PendingIntentRecord{431d35c8
とりあえず
Androidソースコードから検索
MediaFocusControl#registerMediaButtonIntent
AudioManager#registerMediaButtonEventReceiver↑
この辺りが怪しい
調べたらフツーにリファレンスが出てきた…
http://developer.android.com/training/managing-audio/volume-playback.html
イヤホンボタンイベントを取得するコード
@Override public int onStartCommand(Intent intent, int flags, int startId){ AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); myEventReceiver = new ComponentName(getPackageName(), MyRemoteControlEventReceiver.class.getName()); am.registerMediaButtonEventReceiver(myEventReceiver); return START_NOT_STICKY; } @Override public void onDestroy() { AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); am.unregisterMediaButtonEventReceiver(myEventReceiver); }
MainService.java
@Override public void onReceive(Context context, Intent intent) { // カメラ起動 Intent intent2 = new Intent(); //intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent2.setAction("android.media.action.IMAGE_CAPTURE"); startActivity(intent2); }
MyRemoteControlEventReceiver.java
さらに仕込む
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
AndroidManifest.xml
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakelock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, "Your App Tag"); wakelock.acquire(); wakelock.release();
// フラグセット Window window = getWindow(); window.addFlags( WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
スリープを解除するコード
画面ロックをパスするコード
ここまでROM改造なし、誰でもボタン入力を扱える
デモ※本日二回目
多摩川の直線鬼が シャッターチャンスに遭遇してから シャッター押下までのタイムは わずか1.2秒 (自分調べ) にすぎない
では、 その撮影プロセスを もう一度見てみよう
最速!
あらゆる状況で最速!!
友人 なんか似たようなん 既に見たで
僕
僕
僕
友人
絶望が俺のゴールだ
発表は以上です ご静聴ありがとうございました
申し訳ありませんでした