/*
 * Copyright 2011 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.android.apps.mytracks.content;

import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.isA;

import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.TrackStubUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.location.Location;
import android.provider.BaseColumns;
import android.test.AndroidTestCase;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import org.easymock.Capture;
import org.easymock.IAnswer;

/**
 * Tests for {@link TrackDataHub}.
 * 
 * @author Rodrigo Damazio
 */
public class TrackDataHubTest extends AndroidTestCase {

  private static final long TRACK_ID = 42L;
  private static final int TARGET_POINTS = 50;

  private MockContext context;
  private SharedPreferences sharedPreferences;
  private MyTracksProviderUtils myTracksProviderUtils;
  private DataSource dataSource;
  private TrackDataManager trackDataManager;
  private TrackDataHub trackDataHub;
  private TrackDataListener trackDataListener1;
  private TrackDataListener trackDataListener2;
  private Capture<OnSharedPreferenceChangeListener> preferenceChangeListenerCapture = new Capture<
      SharedPreferences.OnSharedPreferenceChangeListener>();

  @UsesMocks({ MyTracksProviderUtils.class, DataSource.class, TrackDataListener.class })
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    context = new MockContext(new MockContentResolver(), new RenamingDelegatingContext(
        getContext(), getContext(), "test."));
    sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
    myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class);
    dataSource = AndroidMock.createMock(DataSource.class, context);
    trackDataManager = new TrackDataManager();
    trackDataHub = new TrackDataHub(
        context, trackDataManager, myTracksProviderUtils, TARGET_POINTS) {
        @Override
      protected DataSource newDataSource() {
        return dataSource;
      }

        @Override
      protected void runInHanderThread(Runnable runnable) {
        // Run everything in the same thread
        runnable.run();
      }
    };

    trackDataListener1 = AndroidMock.createStrictMock(
        "trackDataListener1", TrackDataListener.class);
    trackDataListener2 = AndroidMock.createStrictMock(
        "trackDataListener2", TrackDataListener.class);
    PreferencesUtils.setLong(context, R.string.recording_track_id_key, TRACK_ID);
  }

  @Override
  protected void tearDown() throws Exception {
    AndroidMock.reset(dataSource);

    // Expect everything to be unregistered.
    dataSource.unregisterContentObserver(isA(ContentObserver.class));
    AndroidMock.expectLastCall().times(3);
    dataSource.unregisterOnSharedPreferenceChangeListener(
        isA(OnSharedPreferenceChangeListener.class));
    AndroidMock.replay(dataSource);

    trackDataHub.stop();
    trackDataHub = null;
    super.tearDown();
  }

  /**
   * Tests registering for tracks table update.
   */
  public void testTracksTableUpdate() {

    // Register two listeners
    Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
    Track track = new Track();
    expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
    dataSource.registerContentObserver(
        eq(TracksColumns.CONTENT_URI), capture(contentObserverCapture));
    trackDataListener1.onTrackUpdated(track);
    trackDataListener2.onTrackUpdated(track);
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE));
    trackDataHub.registerTrackDataListener(
        trackDataListener2, EnumSet.of(TrackDataType.TRACKS_TABLE));
    verifyAndReset();

    // Causes tracks table update
    ContentObserver contentObserver = contentObserverCapture.getValue();
    expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
    trackDataListener1.onTrackUpdated(track);
    trackDataListener2.onTrackUpdated(track);
    replay();

    contentObserver.onChange(false);
    verifyAndReset();

    // Unregister one listener
    expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
    trackDataListener2.onTrackUpdated(track);
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener1);
    contentObserver.onChange(false);
    verifyAndReset();

    // Unregister the second listener
    dataSource.unregisterContentObserver(contentObserver);
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener2);
    contentObserver.onChange(false);
    verifyAndReset();
  }

  /**
   * Tests registering for waypoints table update.
   */
  public void testWaypointsTableUpdate() {
    Waypoint waypoint1 = new Waypoint();
    Waypoint waypoint2 = new Waypoint();
    Waypoint waypoint3 = new Waypoint();
    Waypoint waypoint4 = new Waypoint();
    Location location = new Location("gps");
    location.setLatitude(10.0);
    location.setLongitude(8.0);
    waypoint1.setLocation(location);
    waypoint2.setLocation(location);
    waypoint3.setLocation(location);
    waypoint4.setLocation(location);

    // Register two listeners
    Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
    expect(myTracksProviderUtils.getWaypointCursor(
        eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
        .andStubAnswer(new FixedSizeCursorAnswer(2));
    expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class)))
        .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint1).andReturn(waypoint2);
    dataSource.registerContentObserver(
        eq(WaypointsColumns.CONTENT_URI), capture(contentObserverCapture));
    trackDataListener1.clearWaypoints();
    trackDataListener2.clearWaypoints();
    trackDataListener1.onNewWaypoint(waypoint1);
    trackDataListener2.onNewWaypoint(waypoint1);
    trackDataListener1.onNewWaypoint(waypoint2);
    trackDataListener2.onNewWaypoint(waypoint2);
    trackDataListener1.onNewWaypointsDone();
    trackDataListener2.onNewWaypointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
    trackDataHub.registerTrackDataListener(
        trackDataListener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
    verifyAndReset();

    // Cause waypoints table update
    ContentObserver contentObserver = contentObserverCapture.getValue();
    expect(myTracksProviderUtils.getWaypointCursor(
        eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
        .andStubAnswer(new FixedSizeCursorAnswer(3));
    expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class)))
        .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3);
    trackDataListener1.clearWaypoints();
    trackDataListener2.clearWaypoints();
    trackDataListener1.onNewWaypoint(waypoint1);
    trackDataListener2.onNewWaypoint(waypoint1);
    trackDataListener1.onNewWaypoint(waypoint2);
    trackDataListener2.onNewWaypoint(waypoint2);
    trackDataListener1.onNewWaypoint(waypoint3);
    trackDataListener2.onNewWaypoint(waypoint3);
    trackDataListener1.onNewWaypointsDone();
    trackDataListener2.onNewWaypointsDone();
    replay();

    contentObserver.onChange(false);
    verifyAndReset();

    // Unregister one listener
    expect(myTracksProviderUtils.getWaypointCursor(
        eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
        .andStubAnswer(new FixedSizeCursorAnswer(4));
    expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class)))
        .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3).andReturn(waypoint4);
    trackDataListener2.clearWaypoints();
    trackDataListener2.onNewWaypoint(waypoint1);
    trackDataListener2.onNewWaypoint(waypoint2);
    trackDataListener2.onNewWaypoint(waypoint3);
    trackDataListener2.onNewWaypoint(waypoint4);
    trackDataListener2.onNewWaypointsDone();
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener1);
    contentObserver.onChange(false);
    verifyAndReset();

    // Unregister the second listener
    dataSource.unregisterContentObserver(contentObserver);
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener2);
    contentObserver.onChange(false);
    verifyAndReset();
  }

  /**
   * Tests track points table update.
   */
  public void testTrackPointsTableUpdate() {
    // Register one listener
    Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));

    FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();

    // Register a second listener
    locationIterator = new FixedSizeLocationIterator(1, 10, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
    trackDataListener2.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener2);
    trackDataListener2.onNewTrackPointsDone();
    replay();

    trackDataHub.registerTrackDataListener(
        trackDataListener2, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();

    // Deliver more points - should go to both listeners without clearing
    ContentObserver contentObserver = contentObserverCapture.getValue();
    locationIterator = new FixedSizeLocationIterator(11, 10, 1);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(20L);
    locationIterator.expectLocationsDelivered(trackDataListener1);
    locationIterator.expectLocationsDelivered(trackDataListener2);
    trackDataListener1.onNewTrackPointsDone();
    trackDataListener2.onNewTrackPointsDone();
    replay();

    contentObserver.onChange(false);
    verifyAndReset();

    // Unregister one listener and change track
    locationIterator = new FixedSizeLocationIterator(101, 10);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
        .andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID + 1)).andReturn(110L);
    trackDataListener2.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener2);
    trackDataListener2.onNewTrackPointsDone();
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener1);
    trackDataHub.loadTrack(TRACK_ID + 1);
    verifyAndReset();
  }

  /**
   * Tests track points table update with registering the same listener.
   */
  public void testTrackPointsTableUpdate_reRegister() {

    // Register one listener
    Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));

    FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);

    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();

    // Unregister the listener
    ContentObserver observer = contentObserverCapture.getValue();
    dataSource.unregisterContentObserver(observer);
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener1);
    verifyAndReset();

    // Register again
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));
    locationIterator = new FixedSizeLocationIterator(1, 10, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();
  }

  /**
   * Tests tracks point able change. Register a listener after a track change.
   */
  public void testTrackPointsTableUpdate_reRegisterAfterTrackChange() {

    // Register one listener
    Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));

    FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();

    // Unregister the listener
    ContentObserver observer = observerCapture.getValue();
    dataSource.unregisterContentObserver(observer);
    replay();

    trackDataHub.unregisterTrackDataListener(trackDataListener1);
    verifyAndReset();

    // Register the listener after a new track
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));
    locationIterator = new FixedSizeLocationIterator(1, 10);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
        .andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID + 1)).andReturn(10L);
    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.loadTrack(TRACK_ID + 1);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();
  }

  /**
   * Tests track points table update with large track sampling.
   */
  public void testTrackPointsTableUpdate_largeTrackSampling() {
    Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));

    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(200L);
    AndroidMock.expectLastCall().anyTimes();
    FixedSizeLocationIterator locationIterator1 = new FixedSizeLocationIterator(
        1, 200, 4, 25, 71, 120);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator1);
    FixedSizeLocationIterator locationIterator2 = new FixedSizeLocationIterator(
        1, 200, 4, 25, 71, 120);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator2);

    trackDataListener1.clearTrackPoints();
    locationIterator1.expectSampledLocationsDelivered(trackDataListener1, 4, false);
    trackDataListener1.onNewTrackPointsDone();
    trackDataListener2.clearTrackPoints();
    locationIterator2.expectSampledLocationsDelivered(trackDataListener2, 4, true);
    trackDataListener2.onNewTrackPointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    trackDataHub.registerTrackDataListener(trackDataListener2, EnumSet.of(
        TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
    verifyAndReset();
  }

  /**
   * Tests track points table update with resampling.
   */
  public void testTrackPointsTableUpdate_resampling() {
    Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(
        eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));

    // Deliver 30 points (no sampling happens)
    FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(30L);

    trackDataListener1.clearTrackPoints();
    locationIterator.expectLocationsDelivered(trackDataListener1);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    trackDataHub.start();
    trackDataHub.loadTrack(TRACK_ID);
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
    verifyAndReset();

    // Now deliver 30 more (incrementally sampled)
    ContentObserver observer = observerCapture.getValue();
    locationIterator = new FixedSizeLocationIterator(31, 30);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(60L);
    locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    observer.onChange(false);
    verifyAndReset();

    // Now another 30 (triggers resampling)
    locationIterator = new FixedSizeLocationIterator(1, 90);
    expect(myTracksProviderUtils.getTrackPointLocationIterator(
        eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
    expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(90L);
    trackDataListener1.clearTrackPoints();
    locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
    trackDataListener1.onNewTrackPointsDone();
    replay();

    observer.onChange(false);
    verifyAndReset();
  }

  /**
   * Tests preferences change.
   */
  public void testPreferencesChange() throws Exception {

    // Register two listeners
    PreferencesUtils.setString(
        context, R.string.stats_rate_key, PreferencesUtils.STATS_RATE_DEFAULT);
    PreferencesUtils.setString(
        context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
    PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key,
        PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
    PreferencesUtils.setInt(context, R.string.recording_distance_interval_key,
        PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);

    dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture));
    expect(trackDataListener1.onMetricUnitsChanged(true)).andReturn(false);
    expect(trackDataListener1.onReportSpeedChanged(true)).andReturn(false);
    expect(
        trackDataListener1.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT))
        .andReturn(false);
    expect(trackDataListener1.onRecordingDistanceIntervalChanged(
        PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).andReturn(false);
    expect(trackDataListener1.onMapTypeChanged(PreferencesUtils.MAP_TYPE_DEFAUlT)).andReturn(false);
    
    expect(trackDataListener2.onMetricUnitsChanged(true)).andReturn(false);
    expect(trackDataListener2.onReportSpeedChanged(true)).andReturn(false);
    expect(
        trackDataListener2.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT))
        .andReturn(false);
    expect(trackDataListener2.onRecordingDistanceIntervalChanged(
        PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).andReturn(false);
    expect(trackDataListener2.onMapTypeChanged(PreferencesUtils.MAP_TYPE_DEFAUlT)).andReturn(false);
    replay();

    trackDataHub.start();
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.PREFERENCE));
    trackDataHub.registerTrackDataListener(
        trackDataListener2, EnumSet.of(TrackDataType.PREFERENCE));
    verifyAndReset();

    // Change report speed to false
    expect(trackDataListener1.onReportSpeedChanged(false)).andReturn(false);
    expect(trackDataListener2.onReportSpeedChanged(false)).andReturn(false);
    replay();

    PreferencesUtils.setString(
        context, R.string.stats_rate_key, context.getString(R.string.stats_rate_pace));
    OnSharedPreferenceChangeListener listener = preferenceChangeListenerCapture.getValue();
    listener.onSharedPreferenceChanged(
        sharedPreferences, PreferencesUtils.getKey(context, R.string.stats_rate_key));
    verifyAndReset();

    // Change metric units to false
    expect(trackDataListener1.onMetricUnitsChanged(false)).andReturn(false);
    expect(trackDataListener2.onMetricUnitsChanged(false)).andReturn(false);
    replay();

    String imperialUnits = context.getString(R.string.stats_units_imperial);
    PreferencesUtils.setString(context, R.string.stats_units_key, imperialUnits);
    listener.onSharedPreferenceChanged(
        sharedPreferences, PreferencesUtils.getKey(context, R.string.stats_units_key));
    verifyAndReset();
  }

  /**
   * Replays mocks.
   */
  private void replay() {
    AndroidMock.replay(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2);
  }

  /**
   * Verifies and resets mocks.
   */
  private void verifyAndReset() {
    AndroidMock.verify(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2);
    AndroidMock.reset(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2);
  }

  /**
   * Fixed size cursor answer.
   * 
   * @author Jimmy Shih
   */
  private static class FixedSizeCursorAnswer implements IAnswer<Cursor> {
    private final int size;

    public FixedSizeCursorAnswer(int size) {
      this.size = size;
    }

    @Override
    public Cursor answer() throws Throwable {
      MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID });
      for (long i = 0; i < size; i++) {
        cursor.addRow(new Object[] { i });
      }
      return cursor;
    }
  }

  /**
   * Fixed size location iterator.
   * 
   * @author Jimmy Shih
   */
  private static class FixedSizeLocationIterator implements LocationIterator {
    private final long startId;
    private final Location[] locations;
    private final Set<Integer> splitIndexSet = new HashSet<Integer>();
    private int currentIndex = -1;

    public FixedSizeLocationIterator(long startId, int size) {
      this(startId, size, null);
    }

    public FixedSizeLocationIterator(long startId, int size, int... splitIndexes) {
      this.startId = startId;
      this.locations = new Location[size];

      for (int i = 0; i < size; i++) {
        Location location = new Location("gps");
        location.setLatitude(-15.0 + i / 1000.0);
        location.setLongitude(37 + i / 1000.0);
        location.setAltitude(i);
        locations[i] = location;
      }

      if (splitIndexes != null) {
        for (int splitIndex : splitIndexes) {
          splitIndexSet.add(splitIndex);

          Location splitLocation = locations[splitIndex];
          splitLocation.setLatitude(100.0);
          splitLocation.setLongitude(200.0);
        }
      }
    }

    public void expectLocationsDelivered(TrackDataListener listener) {
      for (int i = 0; i < locations.length; i++) {
        if (splitIndexSet.contains(i)) {
          listener.onSegmentSplit(locations[i]);
        } else {
          listener.onSampledInTrackPoint(locations[i]);
        }
      }
    }

    public void expectSampledLocationsDelivered(
        TrackDataListener listener, int sampleFrequency, boolean includeSampledOut) {
      boolean includeNext = false;
      for (int i = 0; i < locations.length; i++) {
        if (splitIndexSet.contains(i)) {
          listener.onSegmentSplit(locations[i]);
          includeNext = true;
        } else if (includeNext || (i % sampleFrequency == 0)) {
          listener.onSampledInTrackPoint(locations[i]);
          includeNext = false;
        } else if (includeSampledOut) {
          listener.onSampledOutTrackPoint(locations[i]);
        }
      }
    }

    @Override
    public boolean hasNext() {
      return currentIndex < locations.length - 1;
    }

    @Override
    public Location next() {
      currentIndex++;
      return locations[currentIndex];
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException();
    }

    @Override
    public long getLocationId() {
      return startId + currentIndex;
    }

    @Override
    public void close() {
      // Do nothing
    }
  }

  /**
   * Tests the method {@link TrackDataHub#start()}.
   */
  public void testRegisterTracksTableListener() {
    Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), capture(observerCapture));
    Track track = TrackStubUtils.createTrack(1);
    expect(myTracksProviderUtils.getTrack(capture(new Capture<Long>()))).andReturn(track);
    // Make the track id is unique.
    PreferencesUtils.setLong(context, R.string.recording_track_id_key, System.currentTimeMillis());
    trackDataListener1.onTrackUpdated(track);
    replay();
    trackDataHub.start();
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE));
    verifyAndReset();
  }

  /**
   * Tests the method {@link TrackDataHub#start()}.
   */
  public void testRegisterWaypointsTableListener() {
    Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
    dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), capture(observerCapture));
    expect(myTracksProviderUtils.getWaypointCursor(
        capture(new Capture<Long>()), capture(new Capture<Long>()),
        capture(new Capture<Integer>()))).andReturn(null);
    trackDataListener1.clearWaypoints();
    trackDataListener1.onNewWaypointsDone();
    replay();
    trackDataHub.start();
    trackDataHub.registerTrackDataListener(
        trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
    verifyAndReset();
  }

  /**
   * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
   * the key is R.string.min_required_accuracy_key.
   */
  public void testNotifyPreferenceChanged_minRequiredAccuracy() {
    int value = 1;
    PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, value);
    trackDataHub.notifyPreferenceChanged(PreferencesUtils
        .getKey(context, R.string.recording_gps_accuracy_key));
    assertEquals(value, trackDataHub.getRecordingGpsAccuracy());
    PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, value + 1);
    trackDataHub.notifyPreferenceChanged(PreferencesUtils
        .getKey(context, R.string.recording_gps_accuracy_key));
    assertEquals(value + 1, trackDataHub.getRecordingGpsAccuracy());
  }
  
  /**
   * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
   * the key is R.string.metric_units_key.
   */
  public void testNotifyPreferenceChanged_metricUnitsNoNotify() {
    String imperialUnits = context.getString(R.string.stats_units_imperial);

    PreferencesUtils.setString(context, R.string.stats_units_key, imperialUnits);
    trackDataHub.notifyPreferenceChanged(
        PreferencesUtils.getKey(context, R.string.stats_units_key));
    assertEquals(false, trackDataHub.isMetricUnits());
    PreferencesUtils.setString(
        context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
    trackDataHub.notifyPreferenceChanged(
        PreferencesUtils.getKey(context, R.string.stats_units_key));
    assertEquals(true, trackDataHub.isMetricUnits());
  }
  
  
  /**
   * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
   * the key is R.string.metric_units_key.
   */
  public void testNotifyPreferenceChanged_reportSpeedNoNotify() {
    String value = context.getString(R.string.stats_rate_pace);
    PreferencesUtils.setString(context, R.string.stats_rate_key, value);
    trackDataHub.notifyPreferenceChanged(PreferencesUtils
        .getKey(context, R.string.stats_rate_key));
    assertEquals(false, trackDataHub.isReportSpeed());
    
    value = context.getString(R.string.stats_rate_speed);
    PreferencesUtils.setString(context, R.string.stats_rate_key, value);
    trackDataHub.notifyPreferenceChanged(PreferencesUtils
        .getKey(context, R.string.stats_rate_key));
    assertEquals(true, trackDataHub.isReportSpeed());
  }
}