MyTracks 読み (12)
リハビリ。測位情報を保存するあたりのソレ。
開始、終了は MyTracks の OptionsMenu 選択によります。
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MyTracksConstants.MENU_START_RECORDING: { startRecording(); return true; } case MyTracksConstants.MENU_STOP_RECORDING: { stopRecording(); return true; }
startRecording メソドの定義が以下。
private void startRecording() { if (trackRecordingService == null) { startNewTrackRequested = true; Intent startIntent = new Intent(this, TrackRecordingService.class); startService(startIntent); tryBindTrackRecordingService(); } else { try { recordingTrackId = trackRecordingService.startNewTrack(); Toast.makeText(this, getString(R.string.status_now_recording), Toast.LENGTH_SHORT).show(); setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId); } catch (RemoteException e) { Toast.makeText(this, getString(R.string.error_unable_to_start_recording), Toast.LENGTH_SHORT).show(); Log.e(MyTracksConstants.TAG, "Failed to start track recording service", e); } } }
ええとまづ、startNewTrackRequested というフラグなんですが
- startRecording でフラグが立つ
- ServiceConnection#onServiceConnected (bind した時) に false
- フラグが立ってたら、という条件分岐付き
結構細かい制御をしてるなぁ。
bind
ちょっと以前サンプルで見たソレとは違ってこの例では aidl なソレを使っている模様。tryBindTrackRecordingService メソドの定義が以下。
private void tryBindTrackRecordingService() { Log.d(MyTracksConstants.TAG, "MyTracks: Trying to bind to track recording service..."); bindService(new Intent(this, TrackRecordingService.class), serviceConnection, 0); }
serviceConnection の定義が以下 (一部のみ)。
private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { Log.d(MyTracksConstants.TAG, "MyTracks: Service now connected."); trackRecordingService = ITrackRecordingService.Stub.asInterface(service); if (startNewTrackRequested) { startNewTrackRequested = false; try { recordingTrackId = trackRecordingService.startNewTrack(); Toast.makeText(MyTracks.this, R.string.status_now_recording, Toast.LENGTH_SHORT).show(); setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId); } catch (RemoteException e) { Toast.makeText(MyTracks.this, R.string.error_unable_to_start_recording, Toast.LENGTH_SHORT) .show(); Log.w(MyTracksConstants.TAG, "Unable to start recording.", e); } } }
onServiceConnected に渡される IBinder な service は何かというと、TrackRecordingService#onBind で戻されるソレのはず。
@Override public IBinder onBind(Intent intent) { Log.d(MyTracksConstants.TAG, "TrackRecordingService.onBind"); return binder; }
binder という属性ですが、TrackRecordingService クラス定義の末端のあたりで以下。
private final ITrackRecordingService.Stub binder = new ITrackRecordingService.Stub() {
aidl の定義から ITrackRecordingService.Stub というインターフェースが自動生成されるので、その実体をここで定義している、という理解で良いかな。
で、これがサービス使う側とサービスとの i/f になる、と。おそらくあのサンプルが理解できてれば、なんとなくイメージはできるはず。だんだん体がこっちに慣れてきた気がしてきた。
つづき
onServiceConnected メソドの中身を確認。trackRecordingService という属性ですが
private ITrackRecordingService trackRecordingService;
という形で定義されてます。
trackRecordingService = ITrackRecordingService.Stub.asInterface(service);
な取り出し方って定型なのでしょうか。木南さん本では_引数の service を IHelloService インターフェースに変換する_との記述があるので定型の処理という理解で良いはず。これで onBind から戻ってきたソレを ITrackRecordingService として取り扱う事ができるようになった、という事か。後は
- ITrackRecordingService#startNewTrack
- MyTracks#setSelectedAndRecordingTrack
を確認。まずは上側からなんですが定義が以下。
public long startNewTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); Track track = new Track(); track.setName("new"); track.setStartTime(System.currentTimeMillis()); track.setStartId(-1); Uri trackUri = providerUtils.insertTrack(track); long trackId = Long.parseLong(trackUri.getLastPathSegment()); track.setId(trackId); track.setName(String.format(getString(R.string.new_track), trackId)); providerUtils.updateTrack(track); recordingTrackId = trackId; currentWaypointId = insertStatisticsMarker(null); isRecording = true; isMoving = true; stats = new TripStatistics(track.getStartTime()); if (announcementFrequency != -1 && executer != null) { executer.scheduleTask(announcementFrequency * 60000); } length = 0; showNotification(); registerLocationListener(); splitManager.restore(); signalManager.restore(); return trackId; }
とほほほ。何だこれは。ええと
- Track なオブジェクト作って初期設定
- TrackRecordingService 側の属性も初期設定
次の TripStatistics はスルーして以降は何だろ。
if (announcementFrequency != -1 && executer != null) { executer.scheduleTask(announcementFrequency * 60000); } length = 0; showNotification(); registerLocationListener(); splitManager.restore(); signalManager.restore(); return trackId;
以下な属性が定義されております。
/** * Status announcer executer. */ private PeriodicTaskExecuter executer; private TaskExecuterManager signalManager; private SplitManager splitManager;
このヒト達はいずれも onCreate で初期化されております。よく考えたら onCreate 見てないな。上記の方々は com.google.android.apps.mytracks.services パケジにて定義となっておりますな。
とりあえず executer な PeriodicTaskExecuter ですが、これは正に一定時間おきに何かをするためのソレですな。定義の引用は略。
で、何をするか、というと onCreate でオブジェクト生成してる部分に着目すると
SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this); executer = new PeriodicTaskExecuter(announcer, this);
ええと、上記 announcer の run を繰り返す形か。SafeStatusAnnouncerTask は短いので以下に引用。
public class SafeStatusAnnouncerTask implements PeriodicTask { private StatusAnnouncerTask announcer; /* class initialization fails when this throws an exception */ static { try { Class.forName( "com.google.android.apps.mytracks.services.StatusAnnouncerTask"); } catch (ClassNotFoundException ex) { throw new RuntimeException(ex); } catch (LinkageError er) { throw new RuntimeException(er); } } /* calling here forces class initialization */ public static void checkAvailable() { } public SafeStatusAnnouncerTask(Context context) { announcer = new StatusAnnouncerTask(context); } public void run(TrackRecordingService service) { announcer.run(service); } public void shutdown() { announcer.shutdown(); } @Override public void start() { } }
なんでこんな微妙な wrap の仕方をするのかが分からん。しかも Text to Speech が云々とあるのでスルーしてしまえ。しかしこの PeriodicTaskExecutor の考え方は面白い。
- PeriodicTaskExecutor で実行できるのは PeriodicTask なインターフェースを実装したクラス
- 一定時間おきになにかをする
- 何をするか、はコンストラクタに渡すオブジェクトで決まる
あら、TaskExecuterManager はさらにその wrapper というかドライバみたい。もう一つの SplitManager は微妙にポイント高そうなんだけど、とり急ぎスルー。
MyTracks#setSelectedAndRecordingTrack を確認。
private void setSelectedAndRecordingTrack(final long theSelectedTrackId, final long theRecordingTrackId) { runOnUiThread(new Runnable() { public void run() { SharedPreferences prefs = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (prefs != null) { SharedPreferences.Editor editor = prefs.edit(); editor.putLong(MyTracksSettings.SELECTED_TRACK, theSelectedTrackId); editor.putLong(MyTracksSettings.RECORDING_TRACK, theRecordingTrackId); editor.commit(); } } }); }
UI Thread で Preferences の更新をしてます。このアプリ、ある意味 Preferences Driven と言っても過言ではないな。で、Service 側では onPreferenceChanged なメソドが呼び出されて recordingTrackId が確保された後、registerLocationListener メソド呼び出しで onLocationChanged が呼び出されるようになるはず。
この中で測位情報が保存されているはずなんだけどダウトかなぁ。。。
む
ここですな。
// If separation from last recorded point is too large insert a // separator // to indicate end of a segment: boolean startNewSegment = lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90 && distanceToLastRecorded > maxRecordingDistance && recordingTrack.getStartId() >= 0; if (startNewSegment) { // Insert a separator point to indicate start of new track: Log.d(MyTracksConstants.TAG, "Inserting a separator."); Location separator = new Location(MyTracksConstants.GPS_PROVIDER); separator.setLongitude(0); separator.setLatitude(100); separator.setTime(lastRecordedLocation.getTime()); providerUtils.insertTrackPoint(separator, recordingTrackId); }
こっちじゃないかな。その下みたい。
if (!insertLocation(recordingTrack, location, lastRecordedLocation, lastRecordedLocationId, recordingTrackId)) { return; }
しかし separator って何だろ。