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 って何だろ。