Merge "Re-add OfflineLocationTimeZoneDelegate.onDestroy"
diff --git a/locationtzprovider/src/main/java/com/android/timezone/location/provider/OfflineLocationTimeZoneProviderService.java b/locationtzprovider/src/main/java/com/android/timezone/location/provider/OfflineLocationTimeZoneProviderService.java
index f8dc49a..c8b7e22 100644
--- a/locationtzprovider/src/main/java/com/android/timezone/location/provider/OfflineLocationTimeZoneProviderService.java
+++ b/locationtzprovider/src/main/java/com/android/timezone/location/provider/OfflineLocationTimeZoneProviderService.java
@@ -59,6 +59,11 @@
     }
 
     @Override
+    public void onDestroy() {
+        mDelegate.onDestroy();
+    }
+
+    @Override
     public void onStartUpdates(long initializationTimeoutMillis) {
         mDelegate.onStartUpdates(Duration.ofMillis(initializationTimeoutMillis));
     }
diff --git a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegate.java b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegate.java
index 33c26f8..c808183 100644
--- a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegate.java
+++ b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegate.java
@@ -174,6 +174,23 @@
         }
     }
 
+    /** Called during {@link android.service.timezone.TimeZoneProviderService#onDestroy}. */
+    public void onDestroy() {
+        PiiLoggable entryCause = PiiLoggables.fromString("onDestroy() called");
+        logDebug(entryCause);
+
+        synchronized (mLock) {
+            cancelTimeoutsAndLocationCallbacks();
+
+            Mode currentMode = mCurrentMode.get();
+            if (currentMode.mModeEnum == MODE_STARTED) {
+                sendTimeZoneUncertainResultIfNeeded();
+            }
+            Mode newMode = new Mode(MODE_DESTROYED, entryCause);
+            mCurrentMode.set(newMode);
+        }
+    }
+
     /** Called during {@link android.service.timezone.TimeZoneProviderService#onStartUpdates}. */
     public void onStartUpdates(@NonNull Duration initializationTimeout) {
         Objects.requireNonNull(initializationTimeout);
diff --git a/locationtzprovider/src/test/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegateTest.java b/locationtzprovider/src/test/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegateTest.java
index e17656a..e1947ef 100644
--- a/locationtzprovider/src/test/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegateTest.java
+++ b/locationtzprovider/src/test/java/com/android/timezone/location/provider/core/OfflineLocationTimeZoneDelegateTest.java
@@ -18,6 +18,7 @@
 import static com.android.timezone.location.provider.core.OfflineLocationTimeZoneDelegate.LOCATION_LISTEN_MODE_ACTIVE;
 import static com.android.timezone.location.provider.core.OfflineLocationTimeZoneDelegate.LOCATION_LISTEN_MODE_PASSIVE;
 import static com.android.timezone.location.provider.core.TimeZoneProviderResult.RESULT_TYPE_SUGGESTION;
+import static com.android.timezone.location.provider.core.TimeZoneProviderResult.RESULT_TYPE_UNCERTAIN;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -91,11 +92,13 @@
         assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
         mTestEnvironment.assertIsNotListening();
         mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        final Duration initializationTimeout = Duration.ofSeconds(20);
+        mDelegate.onStartUpdates(initializationTimeout);
 
         // The provider should have started an initialization timeout and followed the first
         // instruction from the accountant.
-        final Duration initializationTimeout = Duration.ofSeconds(20);
-        mDelegate.onStartUpdates(initializationTimeout);
         FakeEnvironment.TestTimeoutState<?> initializationTimeoutState =
                 mTestEnvironment.getActiveTimeoutState(0);
         initializationTimeoutState.assertDelay(initializationTimeout);
@@ -142,11 +145,13 @@
         assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
         mTestEnvironment.assertIsNotListening();
         mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        final Duration initializationTimeout = Duration.ofSeconds(20);
+        mDelegate.onStartUpdates(initializationTimeout);
 
         // The provider should have started an initialization timeout and followed the first
         // instruction from the accountant.
-        final Duration initializationTimeout = Duration.ofSeconds(20);
-        mDelegate.onStartUpdates(initializationTimeout);
         FakeEnvironment.TestTimeoutState<?> initializationTimeoutState =
                 mTestEnvironment.getActiveTimeoutState(0);
         initializationTimeoutState.assertDelay(initializationTimeout);
@@ -169,6 +174,102 @@
                 passiveInstruction.listenMode, passiveInstruction.duration);
     }
 
+    @Test
+    public void onDestroy_neverStarted() throws Exception {
+        assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        mDelegate.onDestroy();
+
+        assertEquals(Mode.MODE_DESTROYED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+    }
+
+    @Test
+    public void onDestroy_afterStarted() throws Exception {
+        // Prime the accountant with instructions.
+        ListeningInstruction activeInstruction = new ListeningInstruction(
+                LOCATION_LISTEN_MODE_ACTIVE, Duration.ofSeconds(15));
+        ListeningInstruction passiveInstruction = new ListeningInstruction(
+                LOCATION_LISTEN_MODE_PASSIVE, Duration.ofSeconds(25));
+        mTestLocationListeningAccountant.addInstruction(activeInstruction)
+                .addInstruction(passiveInstruction);
+
+        // Start of the test
+
+        assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        final Duration initializationTimeout = Duration.ofSeconds(20);
+        mDelegate.onStartUpdates(initializationTimeout);
+
+        // The provider should have started an initialization timeout and followed the first
+        // instruction from the accountant.
+        FakeEnvironment.TestTimeoutState<?> initializationTimeoutState =
+                mTestEnvironment.getActiveTimeoutState(0);
+        initializationTimeoutState.assertDelay(initializationTimeout);
+        assertEquals(Mode.MODE_STARTED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsLocationListening(
+                activeInstruction.listenMode, activeInstruction.duration);
+
+        // Destroyed without first being stopped.
+        mDelegate.onDestroy();
+
+        assertEquals(Mode.MODE_DESTROYED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertUncertainResultReported();
+    }
+
+    @Test
+    public void onDestroy_afterStartedAndStopped() throws Exception {
+        // Prime the accountant with instructions.
+        ListeningInstruction activeInstruction = new ListeningInstruction(
+                LOCATION_LISTEN_MODE_ACTIVE, Duration.ofSeconds(15));
+        ListeningInstruction passiveInstruction = new ListeningInstruction(
+                LOCATION_LISTEN_MODE_PASSIVE, Duration.ofSeconds(25));
+        mTestLocationListeningAccountant.addInstruction(activeInstruction)
+                .addInstruction(passiveInstruction);
+
+        // Start of the test
+        assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        final Duration initializationTimeout = Duration.ofSeconds(20);
+        mDelegate.onStartUpdates(initializationTimeout);
+
+        // The provider should have started an initialization timeout and followed the first
+        // instruction from the accountant.
+        FakeEnvironment.TestTimeoutState<?> initializationTimeoutState =
+                mTestEnvironment.getActiveTimeoutState(0);
+        initializationTimeoutState.assertDelay(initializationTimeout);
+        assertEquals(Mode.MODE_STARTED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsLocationListening(
+                activeInstruction.listenMode, activeInstruction.duration);
+
+        mDelegate.onStopUpdates();
+        assertEquals(Mode.MODE_STOPPED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+
+        // Destroyed without first being stopped.
+        mDelegate.onDestroy();
+
+        assertEquals(Mode.MODE_DESTROYED, mDelegate.getCurrentModeEnumForTests());
+        mTestEnvironment.assertIsNotListening();
+        mTestEnvironment.assertNoActiveTimeouts();
+        mTestEnvironment.assertNoResultReported();
+    }
+
     private static class FakeEnvironment implements Environment {
 
         private final FakeGeoTimeZonesFinder mFakeGeoTimeZonesFinder = new FakeGeoTimeZonesFinder();
@@ -321,6 +422,12 @@
             return this;
         }
 
+        FakeEnvironment assertUncertainResultReported() {
+            assertNotNull(mLastResultReported);
+            assertEquals(RESULT_TYPE_UNCERTAIN, mLastResultReported.getType());
+            return this;
+        }
+
         FakeEnvironment simulateTimePassing(Duration duration) {
             mElapsedRealtimeMillis += duration.toMillis();
             return this;