Merge "Redact privacy sensitive info from logcat" into sc-dev
diff --git a/common/Android.bp b/common/Android.bp
index 08a7ee1..dbb149b 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -22,3 +22,32 @@
     name: "geotz_host_common",
     srcs: ["host/main/java/**/*.java"],
 }
+
+// Code intended for device or host usage.
+java_library {
+    name: "geotz_common",
+    host_supported: true,
+    srcs: ["src/main/java/**/*.java"],
+    libs: ["androidx.annotation_annotation"],
+    sdk_version: "current",
+    min_sdk_version: "current",
+    apex_available: [
+        "com.android.geotz",
+    ],
+}
+
+// Device side tests for the geotz_common code.
+android_test {
+    name: "GeotzCommonTests",
+    srcs: ["src/test/java/**/*.java"],
+    manifest: "src/test/AndroidManifest.xml",
+    sdk_version: "current",
+    min_sdk_version: "current",
+    static_libs: [
+        "androidx.test.runner",
+        "geotz_lookup",
+        "junit",
+        "truth-prebuilt",
+    ],
+    test_suites: ["general-tests"],
+}
diff --git a/common/src/TEST_MAPPING b/common/src/TEST_MAPPING
new file mode 100644
index 0000000..ef838db
--- /dev/null
+++ b/common/src/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "GeotzCommonTests"
+    }
+  ]
+}
diff --git a/common/src/main/java/com/android/timezone/location/common/PiiLoggable.java b/common/src/main/java/com/android/timezone/location/common/PiiLoggable.java
new file mode 100644
index 0000000..55d2cce
--- /dev/null
+++ b/common/src/main/java/com/android/timezone/location/common/PiiLoggable.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.timezone.location.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An interface for objects that could contain user or other sensitive information. Implementers
+ * must ensure that {@link #toString()} can be used for the default / redacted / log-safe form, and
+ * {@link #toPiiString()} is provided for the form that can contains sensitive information.
+ *
+ * <p>The purpose of this interface is to ensure Java's default {@link #toString()} behavior is safe
+ * to use by default in logs, exception messages, etc. See {@link PiiLoggables} for support methods.
+ */
+public interface PiiLoggable {
+
+    /** Used for logs, should be implemented to be log-safe, not containing PII. */
+    @Override
+    @NonNull String toString();
+
+    /** Like {@link #toString()} but can include PII for debugging. */
+    @NonNull String toPiiString();
+
+    /**
+     * Returns a string representation of {@code value}. Operates like {@link
+     * String#valueOf(Object)}} but calls {@link PiiLoggable#toPiiString()} instead of
+     * {@link Object#toString()}.
+     */
+    static String toPiiString(@Nullable PiiLoggable value) {
+        return value == null ? PiiLoggables.NULL_STRING : value.toPiiString();
+    }
+}
diff --git a/common/src/main/java/com/android/timezone/location/common/PiiLoggables.java b/common/src/main/java/com/android/timezone/location/common/PiiLoggables.java
new file mode 100644
index 0000000..24699a3
--- /dev/null
+++ b/common/src/main/java/com/android/timezone/location/common/PiiLoggables.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.timezone.location.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Function;
+
+/** Utility methods to support {@link PiiLoggable}. */
+public final class PiiLoggables {
+
+    static final String NULL_STRING = "null";
+    static final String REDACTED_STRING = "<redacted>";
+
+    private PiiLoggables() {}
+
+    /**
+     * Returns a null-safe function that calls {@link PiiLoggable#toPiiString(PiiLoggable)} when the
+     * function's argument is not {@code null}.
+     */
+    @NonNull
+    public static <V extends PiiLoggable> Function<V, String> toPiiStringFunction() {
+        return x -> PiiLoggable.toPiiString(x);
+    }
+
+    /**
+     * An adapter / wrapper for objects that output PII as part of their normal {@link #toString()}.
+     * The referenced value can be {@code null}.
+     *
+     * <p>The implementation contract:
+     * <ul>
+     * <li>{@link #toPiiString()} must print {@code "null"} if the value is null, and may delegate
+     * to the value's {@link #toString()}.</li>
+     * <li>{@link #toString()} must print {@code "null"} if the value is null, or a PII safe string,
+     * e.g. "&lt;redacted&gt;", or just non-PII information.
+     * <li>Implementations may implement {@link #equals(Object)} and {@link #hashCode()}.</li>
+     * </ul>
+     *
+     * <p>See {@link #fromPiiValue(Object)} for a method that returns objects that implement a
+     * minimal contract.
+     */
+    public interface PiiLoggableValue<V> extends PiiLoggable {
+        /** Returns the held value. */
+        @Nullable V get();
+    }
+
+    /**
+     * Wraps a nullable reference in a {@link PiiLoggableValue}. {@link Object#toString()}
+     * returns the string {@code "<redacted>"}. {@link Object#equals(Object)} and {@link
+     * Object#hashCode()} delegate to {@code value} when not {@code null}.
+     */
+    @NonNull
+    public static <V> PiiLoggableValue<V> fromPiiValue(@Nullable V value) {
+        class PiiLoggableValueImpl<V> implements PiiLoggableValue<V> {
+
+            private final V mValue;
+
+            PiiLoggableValueImpl(V value) {
+                mValue = value;
+            }
+
+            @Override
+            public V get() {
+                return mValue;
+            }
+
+            @Override
+            public String toPiiString() {
+                return String.valueOf(mValue);
+            }
+
+            @Override
+            public String toString() {
+                return mValue == null ? NULL_STRING : REDACTED_STRING;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass()) {
+                    return false;
+                }
+                PiiLoggableValueImpl<?> that = (PiiLoggableValueImpl<?>) o;
+                return Objects.equals(mValue, that.mValue);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mValue);
+            }
+        }
+        return new PiiLoggableValueImpl<V>(value);
+    }
+
+    /**
+     * A convenience method for creating a {@link PiiLoggable} from a {@link String} that doesn't
+     * contain PII. Both {@link PiiLoggable#toString()} and {@link PiiLoggable#toPiiString()} will
+     * return {@code "null"} if the string is {@code null}, or the result of calling {@link
+     * Object#toString()} if the string is not {@code null}.
+     */
+    @NonNull
+    public static PiiLoggable fromString(@Nullable String s) {
+        class PiiLoggableString implements PiiLoggable {
+
+            private final String mString;
+
+            public PiiLoggableString(String string) {
+                mString = string;
+            }
+
+            @Override
+            public String toPiiString() {
+                return toString();
+            }
+
+            @Override
+            public String toString() {
+                return String.valueOf(mString);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass()) {
+                    return false;
+                }
+                PiiLoggableString that = (PiiLoggableString) o;
+                return Objects.equals(mString, that.mString);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mString);
+            }
+        }
+        return new PiiLoggableString(s);
+    }
+
+    /**
+     * Creates a {@link PiiLoggable} that generates strings using the supplied template. The
+     * resulting object uses {@link String#format(String, Object...)} for {@link
+     * PiiLoggable#toString()} and {@link PiiLoggables#formatPiiString(String, PiiLoggable...)} for
+     * {@link PiiLoggable#toPiiString()}.
+     */
+    @NonNull
+    public static PiiLoggable fromTemplate(@NonNull String template, PiiLoggable... piiLoggables) {
+        class TemplatedPiiLoggable implements PiiLoggable {
+            @NonNull private final String mTemplate;
+            @NonNull private final PiiLoggable[] mLoggables;
+
+            public TemplatedPiiLoggable(String template, PiiLoggable... loggables) {
+                mTemplate = Objects.requireNonNull(template);
+                mLoggables = Objects.requireNonNull(loggables);
+            }
+
+            @Override
+            public String toString() {
+                return String.format(mTemplate, (Object[]) mLoggables);
+            }
+
+            @Override
+            public String toPiiString() {
+                return PiiLoggables.formatPiiString(mTemplate, mLoggables);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass()) {
+                    return false;
+                }
+                TemplatedPiiLoggable that = (TemplatedPiiLoggable) o;
+                return mTemplate.equals(that.mTemplate) &&
+                        Arrays.equals(mLoggables, that.mLoggables);
+            }
+
+            @Override
+            public int hashCode() {
+                int result = Objects.hash(mTemplate);
+                result = 31 * result + Arrays.hashCode(mLoggables);
+                return result;
+            }
+        }
+
+        return new TemplatedPiiLoggable(template, piiLoggables);
+    }
+
+    /**
+     * Formats a templated string. This method operates like {@link
+     * String#format(String, Object...)} except {@code %s} will be replaced with the result of
+     * {@link PiiLoggable#toPiiString(PiiLoggable)} instead of {@link String#valueOf(Object)}.
+     */
+    @NonNull
+    public static String formatPiiString(@NonNull String format, PiiLoggable... piiLoggables) {
+        String[] strings = new String[piiLoggables.length];
+        for (int i = 0; i < strings.length; i++) {
+            PiiLoggable piiLoggable = piiLoggables[i];
+            strings[i] = PiiLoggable.toPiiString(piiLoggable);
+        }
+        return String.format(format, (Object[]) strings);
+    }
+}
diff --git a/common/src/test/AndroidManifest.xml b/common/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..cc66797
--- /dev/null
+++ b/common/src/test/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2021 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.timezone.location.common">
+
+    <application android:allowBackup="false" android:debuggable="true" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.timezone.location.common" />
+</manifest>
\ No newline at end of file
diff --git a/common/src/test/java/com/android/timezone/location/common/PiiLoggableTest.java b/common/src/test/java/com/android/timezone/location/common/PiiLoggableTest.java
new file mode 100644
index 0000000..855a745
--- /dev/null
+++ b/common/src/test/java/com/android/timezone/location/common/PiiLoggableTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.timezone.location.common;
+
+import static com.android.timezone.location.common.PiiLoggables.NULL_STRING;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.timezone.location.common.PiiLoggablesTest.TestPiiLoggable;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link PiiLoggable}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class PiiLoggableTest {
+
+    @Test
+    public void toPiiString() {
+        assertEquals(NULL_STRING, PiiLoggable.toPiiString(null));
+        assertEquals("PII", PiiLoggable.toPiiString(new TestPiiLoggable("no PII", "PII")));
+    }
+}
diff --git a/common/src/test/java/com/android/timezone/location/common/PiiLoggablesTest.java b/common/src/test/java/com/android/timezone/location/common/PiiLoggablesTest.java
new file mode 100644
index 0000000..59e7a82
--- /dev/null
+++ b/common/src/test/java/com/android/timezone/location/common/PiiLoggablesTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.android.timezone.location.common;
+
+import static com.android.timezone.location.common.PiiLoggables.NULL_STRING;
+import static com.android.timezone.location.common.PiiLoggables.REDACTED_STRING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.timezone.location.common.PiiLoggable;
+import com.android.timezone.location.common.PiiLoggables;
+import com.android.timezone.location.common.PiiLoggables.PiiLoggableValue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Tests for {@link PiiLoggables}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class PiiLoggablesTest {
+
+    @Test
+    public void fromString_null() {
+        PiiLoggable piiLoggable = PiiLoggables.fromString(null);
+        assertEquals(NULL_STRING, piiLoggable.toString());
+        assertEquals(NULL_STRING, piiLoggable.toPiiString());
+        assertEquals(piiLoggable, PiiLoggables.fromString(null));
+        assertNotEquals(piiLoggable, PiiLoggables.fromString("Hello"));
+    }
+
+    @Test
+    public void fromString() {
+        String string1 = "Hello";
+        String string2 = "Goodbye";
+        PiiLoggable piiLoggable = PiiLoggables.fromString(string1);
+        assertEquals(string1, piiLoggable.toString());
+        assertEquals(string1, piiLoggable.toPiiString());
+        assertEquals(piiLoggable, PiiLoggables.fromString(string1));
+        assertNotEquals(piiLoggable, PiiLoggables.fromString(string2));
+    }
+
+    @Test
+    public void fromPiiValue_null() {
+        PiiLoggableValue<String> piiLoggableValue = PiiLoggables.fromPiiValue(null);
+        assertEquals(NULL_STRING, piiLoggableValue.toString());
+        assertEquals(NULL_STRING, piiLoggableValue.toPiiString());
+        assertEquals(piiLoggableValue, PiiLoggables.fromPiiValue(null));
+        assertNotEquals(piiLoggableValue, PiiLoggables.fromPiiValue("Hello"));
+        assertNull(piiLoggableValue.get());
+    }
+
+    @Test
+    public void fromPiiValue() {
+        Object piiValue = "Classified Info";
+        Object differentPiiValue = "Different classified Info";
+        PiiLoggableValue<Object> piiLoggableValue = PiiLoggables.fromPiiValue(piiValue);
+        assertEquals(REDACTED_STRING, piiLoggableValue.toString());
+        assertEquals(piiValue.toString(), piiLoggableValue.toPiiString());
+        assertEquals(piiLoggableValue, PiiLoggables.fromPiiValue(piiValue));
+        assertNotEquals(piiLoggableValue, PiiLoggables.fromPiiValue(differentPiiValue));
+        assertEquals(piiValue, piiLoggableValue.get());
+    }
+
+    @Test
+    public void toPiiStringFunction() {
+        String piiValue = "pii";
+        PiiLoggable piiLoggable = new TestPiiLoggable("no pii", piiValue);
+        Function<PiiLoggable, String> function = PiiLoggables.toPiiStringFunction();
+        assertEquals(NULL_STRING, function.apply(null));
+        assertEquals(piiValue, function.apply(piiLoggable));
+    }
+
+    @Test
+    public void formatPiiString() {
+        PiiLoggable piiLoggable = new TestPiiLoggable("no pii", "pii");
+        String result = PiiLoggables.formatPiiString(
+                "1=%s,2=%s,3=%s", piiLoggable, null, piiLoggable);
+        assertEquals("1=pii,2=null,3=pii", result);
+
+    }
+
+    @Test
+    public void fromTemplate() {
+        PiiLoggable piiLoggable = new TestPiiLoggable("no pii", "pii");
+        PiiLoggable result =
+                PiiLoggables.fromTemplate("1=%s,2=%s,3=%s", piiLoggable, null, piiLoggable);
+        assertEquals("1=pii,2=null,3=pii", result.toPiiString());
+        assertEquals("1=no pii,2=null,3=no pii", result.toString());
+    }
+
+    @Test
+    public void fromTemplate_withPercentS() {
+        PiiLoggable piiLoggable = new TestPiiLoggable("no pii{%s}", "pii{%s}");
+        PiiLoggable result =
+                PiiLoggables.fromTemplate("1=%s,2=%s,3=%s", piiLoggable, null, piiLoggable);
+        assertEquals("1=pii{%s},2=null,3=pii{%s}", result.toPiiString());
+        assertEquals("1=no pii{%s},2=null,3=no pii{%s}", result.toString());
+    }
+
+    static class TestPiiLoggable implements PiiLoggable {
+
+        private final String mNoPii;
+        private final String mPii;
+
+        public TestPiiLoggable(String noPii, String pii) {
+            mNoPii = noPii;
+            mPii = pii;
+        }
+
+        @Override
+        public String toPiiString() {
+            return mPii;
+        }
+
+        @Override
+        public String toString() {
+            return mNoPii;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            TestPiiLoggable that = (TestPiiLoggable) o;
+            return mNoPii.equals(that.mNoPii) &&
+                    mPii.equals(that.mPii);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mNoPii, mPii);
+        }
+    }
+}
diff --git a/geotz_lookup/Android.bp b/geotz_lookup/Android.bp
index 2e8e9c8..4b4c436 100644
--- a/geotz_lookup/Android.bp
+++ b/geotz_lookup/Android.bp
@@ -24,6 +24,7 @@
     min_sdk_version: "current",
     libs: ["androidx.annotation_annotation"],
     static_libs: [
+        "geotz_common",
         "geotz_s2storage_ro",
         "s2-geometry-library-java",
     ],
diff --git a/geotz_lookup/src/main/java/com/android/timezone/location/lookup/GeoTimeZonesFinder.java b/geotz_lookup/src/main/java/com/android/timezone/location/lookup/GeoTimeZonesFinder.java
index caa7721..abfcf58 100644
--- a/geotz_lookup/src/main/java/com/android/timezone/location/lookup/GeoTimeZonesFinder.java
+++ b/geotz_lookup/src/main/java/com/android/timezone/location/lookup/GeoTimeZonesFinder.java
@@ -15,6 +15,8 @@
  */
 package com.android.timezone.location.lookup;
 
+import com.android.timezone.location.common.PiiLoggable;
+
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
@@ -87,7 +89,7 @@
      * <p>Depending on the implementation, it may be cheaper to obtain a {@link LocationToken} than
      * doing a full lookup.
      */
-    public abstract static class LocationToken {
+    public abstract static class LocationToken implements PiiLoggable {
         @Override
         public abstract boolean equals(Object other);
 
diff --git a/geotz_lookup/src/main/java/com/android/timezone/location/lookup/S2RangeFileBasedGeoTimeZonesFinder.java b/geotz_lookup/src/main/java/com/android/timezone/location/lookup/S2RangeFileBasedGeoTimeZonesFinder.java
index 089850a..681af52 100644
--- a/geotz_lookup/src/main/java/com/android/timezone/location/lookup/S2RangeFileBasedGeoTimeZonesFinder.java
+++ b/geotz_lookup/src/main/java/com/android/timezone/location/lookup/S2RangeFileBasedGeoTimeZonesFinder.java
@@ -114,6 +114,11 @@
 
         @Override
         public String toString() {
+            return "LocationToken{<redacted>}";
+        }
+
+        @Override
+        public String toPiiString() {
             return "LocationToken{"
                     + "mS2CellId=" + mS2CellId
                     + '}';
diff --git a/locationtzprovider/Android.bp b/locationtzprovider/Android.bp
index a435ec7..63fa3d7 100644
--- a/locationtzprovider/Android.bp
+++ b/locationtzprovider/Android.bp
@@ -26,6 +26,7 @@
         "androidx.annotation_annotation",
     ],
     static_libs: [
+        "geotz_common",
         "geotz_lookup",
     ],
     apex_available: [
diff --git a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Environment.java b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Environment.java
index d1678f9..fe24651 100644
--- a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Environment.java
+++ b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Environment.java
@@ -26,8 +26,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.timezone.location.common.PiiLoggable;
+import com.android.timezone.location.common.PiiLoggables;
 import com.android.timezone.location.lookup.GeoTimeZonesFinder;
 import com.android.timezone.location.provider.core.OfflineLocationTimeZoneDelegate.ListenModeEnum;
+import com.android.timezone.location.common.PiiLoggables.PiiLoggableValue;
 
 import java.io.IOException;
 import java.time.Duration;
@@ -63,7 +66,7 @@
      * <p>With active listening the {@link #getLocation() location} can be {@code null} (meaning
      * "location unknown"), with passive listening it is never {@code null}.
      */
-    final class LocationListeningResult {
+    final class LocationListeningResult implements PiiLoggable {
         /**
          * The type of listening that produced the result. This is recorded for logging / debugging.
          */
@@ -81,8 +84,8 @@
          */
         private final long mResultElapsedRealtimeMillis;
 
-        /** The location, or {@code null} if the location is not known (active only). */
-        @Nullable private final Location mLocation;
+        /** Holds the location, or {@code null} if the location is not known (active only). */
+        @NonNull private final PiiLoggableValue<Location> mPiiLoggableLocation;
 
         public LocationListeningResult(
                 @ListenModeEnum int listenMode,
@@ -94,7 +97,7 @@
             mListeningDuration = Objects.requireNonNull(listeningDuration);
             mStartElapsedRealtimeMillis = startElapsedRealtimeMillis;
             mResultElapsedRealtimeMillis = resultElapsedRealtimeMillis;
-            mLocation = location;
+            mPiiLoggableLocation = PiiLoggables.fromPiiValue(location);
         }
 
         /** Returns how long listening was requested for. */
@@ -105,13 +108,13 @@
 
         /** Returns whether result of listening was a known location. */
         public boolean isLocationKnown() {
-            return mLocation != null;
+            return mPiiLoggableLocation.get() != null;
         }
 
         /** Returns the location. See {@link #isLocationKnown()}. */
         @Nullable
         public Location getLocation() {
-            return mLocation;
+            return mPiiLoggableLocation.get();
         }
 
         /** Returns (an approximation) of when listening started. */
@@ -151,16 +154,28 @@
          */
         @NonNull
         public Duration getLocationAge(long elapsedRealtimeMillis) {
-            if (mLocation == null) {
+            Location location = mPiiLoggableLocation.get();
+            if (location == null) {
                 throw new IllegalStateException();
             }
             long locationAgeMillis = elapsedRealtimeMillis
-                    - NANOSECONDS.toMillis(mLocation.getElapsedRealtimeNanos());
+                    - NANOSECONDS.toMillis(location.getElapsedRealtimeNanos());
             return Duration.ofMillis(locationAgeMillis);
         }
 
         @Override
+        public String toPiiString() {
+            String template = toStringTemplate();
+            return PiiLoggables.formatPiiString(template, mPiiLoggableLocation);
+        }
+
+        @Override
         public String toString() {
+            String template = toStringTemplate();
+            return String.format(template, mPiiLoggableLocation);
+        }
+
+        private String toStringTemplate() {
             return "LocationListeningResult{"
                     + "mListenMode=" + prettyPrintListenModeEnum(mListenMode)
                     + ", mListeningDuration=" + mListeningDuration
@@ -168,7 +183,7 @@
                     + formatElapsedRealtimeMillis(mStartElapsedRealtimeMillis)
                     + ", mResultElapsedRealtimeMillis="
                     + formatElapsedRealtimeMillis(mResultElapsedRealtimeMillis)
-                    + ", mLocation=" + mLocation
+                    + ", mPiiLoggableLocation=%s"
                     + ", getTotalEstimatedTimeListening()=" + getTotalEstimatedTimeListening()
                     + '}';
         }
diff --git a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/LogUtils.java b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/LogUtils.java
index 3395a42..e330884 100644
--- a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/LogUtils.java
+++ b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/LogUtils.java
@@ -20,6 +20,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.timezone.location.common.PiiLoggable;
+
 import java.time.Duration;
 import java.time.Instant;
 
@@ -42,6 +44,13 @@
     }
 
     /** Logs at debug level when debug logging is enabled via {@link #DBG}. */
+    public static void logDebug(@NonNull PiiLoggable piiLoggable) {
+        if (DBG) {
+            Log.d(LOG_TAG, piiLoggable.toString());
+        }
+    }
+
+    /** Logs at debug level when debug logging is enabled via {@link #DBG}. */
     public static void logDebug(@NonNull String msg) {
         if (DBG) {
             Log.d(LOG_TAG, msg);
@@ -49,11 +58,21 @@
     }
 
     /** Logs at warn level. */
+    public static void logWarn(@NonNull PiiLoggable piiLoggable) {
+        Log.w(LOG_TAG, piiLoggable.toString());
+    }
+
+    /** Logs at warn level. */
     public static void logWarn(@NonNull String msg) {
         Log.w(LOG_TAG, msg);
     }
 
     /** Logs at warn level. */
+    public static void logWarn(@NonNull PiiLoggable piiLoggable, @Nullable Throwable t) {
+        Log.w(LOG_TAG, piiLoggable.toString(), t);
+    }
+
+    /** Logs at warn level. */
     public static void logWarn(@NonNull String msg, @Nullable Throwable t) {
         Log.w(LOG_TAG, msg, t);
     }
diff --git a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Mode.java b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Mode.java
index 3c37f8e..6d0eda4 100644
--- a/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Mode.java
+++ b/locationtzprovider/src/main/java/com/android/timezone/location/provider/core/Mode.java
@@ -26,6 +26,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.timezone.location.common.PiiLoggable;
+import com.android.timezone.location.common.PiiLoggables;
 import com.android.timezone.location.provider.core.OfflineLocationTimeZoneDelegate.ListenModeEnum;
 
 import java.util.Objects;
@@ -62,7 +64,7 @@
  *       - when the provider's service is destroyed, perhaps as part of the current user changing
  * </pre>
  */
-class Mode {
+class Mode implements PiiLoggable {
 
     @IntDef({ MODE_STOPPED, MODE_STARTED, MODE_FAILED, MODE_DESTROYED })
     @interface ModeEnum {}
@@ -111,7 +113,7 @@
      * Debug information: Information about why the mode was entered.
      */
     @NonNull
-    private final String mEntryCause;
+    private final PiiLoggable mEntryCause;
 
     /**
      * Used when mModeEnum == {@link #MODE_STARTED}. The {@link Cancellable} that can be
@@ -120,15 +122,15 @@
     @Nullable
     private final Cancellable mLocationListenerCancellable;
 
-    Mode(@ModeEnum int modeEnum, @NonNull String entryCause) {
+    Mode(@ModeEnum int modeEnum, @NonNull PiiLoggable entryCause) {
         this(modeEnum, entryCause, LOCATION_LISTEN_MODE_NA, null);
     }
 
-    Mode(@ModeEnum int modeEnum, @NonNull String entryCause, @ListenModeEnum int listenMode,
+    Mode(@ModeEnum int modeEnum, @NonNull PiiLoggable entryCause, @ListenModeEnum int listenMode,
             @Nullable Cancellable listeningCancellable) {
         mModeEnum = validateModeEnum(modeEnum);
         mListenMode = validateListenModeEnum(modeEnum, listenMode);
-        mEntryCause = entryCause;
+        mEntryCause = Objects.requireNonNull(entryCause);
         mLocationListenerCancellable = listeningCancellable;
 
         // Information useful for logging / debugging.
@@ -138,7 +140,7 @@
     /** Returns the stopped mode which is the starting state for a provider. */
     @NonNull
     static Mode createStoppedMode() {
-        return new Mode(MODE_STOPPED, "init" /* entryCause */);
+        return new Mode(MODE_STOPPED, PiiLoggables.fromString("init") /* entryCause */);
     }
 
     /**
@@ -153,13 +155,24 @@
     }
 
     @Override
+    public String toPiiString() {
+        String template = toStringTemplate();
+        return PiiLoggables.formatPiiString(template, mEntryCause);
+    }
+
+    @Override
     public String toString() {
+        String template = toStringTemplate();
+        return String.format(template, mEntryCause);
+    }
+
+    private String toStringTemplate() {
         return "Mode{"
                 + "mModeEnum=" + prettyPrintModeEnum(mModeEnum)
                 + ", mListenMode=" + prettyPrintListenModeEnum(mListenMode)
                 + ", mCreationElapsedRealtimeMillis="
                 + formatElapsedRealtimeMillis(mCreationElapsedRealtimeMillis)
-                + ", mEntryCause={" + mEntryCause + '}'
+                + ", mEntryCause={%s}"
                 + ", mLocationListenerCancellable=" + mLocationListenerCancellable
                 + '}';
     }
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 1bfb202..7d8a053 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
@@ -36,6 +36,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.timezone.location.common.PiiLoggable;
+import com.android.timezone.location.common.PiiLoggables;
 import com.android.timezone.location.lookup.GeoTimeZonesFinder;
 import com.android.timezone.location.lookup.GeoTimeZonesFinder.LocationToken;
 import com.android.timezone.location.provider.core.Environment.LocationListeningResult;
@@ -117,7 +119,8 @@
 
     /** The current mode of the provider. See {@link Mode} for details. */
     @GuardedBy("mLock")
-    private final ReferenceWithHistory<Mode> mCurrentMode = new ReferenceWithHistory<>(10);
+    private final ReferenceWithHistory<Mode> mCurrentMode =
+            new ReferenceWithHistory<>(10, PiiLoggables.toPiiStringFunction());
 
     /**
      * The last location listening result. Holds {@code null} if location listening hasn't started
@@ -126,7 +129,7 @@
      */
     @GuardedBy("mLock")
     private final ReferenceWithHistory<LocationListeningResult> mLastLocationListeningResult =
-            new ReferenceWithHistory<>(10);
+            new ReferenceWithHistory<>(10, PiiLoggables.toPiiStringFunction());
 
     /**
      * A token associated with the last location time zone lookup. Used to avoid unnecessary time
@@ -171,7 +174,7 @@
     }
 
     public void onBind() {
-        String entryCause = "onBind() called";
+        PiiLoggable entryCause = PiiLoggables.fromString("onBind() called");
         logDebug(entryCause);
 
         synchronized (mLock) {
@@ -188,7 +191,7 @@
     }
 
     public void onDestroy() {
-        String entryCause = "onDestroy() called";
+        PiiLoggable entryCause = PiiLoggables.fromString("onDestroy() called");
         logDebug(entryCause);
 
         synchronized (mLock) {
@@ -206,8 +209,8 @@
     public void onStartUpdates(@NonNull Duration initializationTimeout) {
         Objects.requireNonNull(initializationTimeout);
 
-        String debugInfo = "onStartUpdates(),"
-                + " initializationTimeout=" + initializationTimeout;
+        PiiLoggable debugInfo = PiiLoggables.fromString("onStartUpdates(),"
+                + " initializationTimeout=" + initializationTimeout);
         logDebug(debugInfo);
 
         synchronized (mLock) {
@@ -234,7 +237,7 @@
     }
 
     public void onStopUpdates() {
-        String debugInfo = "onStopUpdates()";
+        PiiLoggable debugInfo = PiiLoggables.fromString("onStopUpdates()");
         logDebug(debugInfo);
 
         synchronized (mLock) {
@@ -270,7 +273,9 @@
             // State and constants.
             pw.println("mInitializationTimeoutCancellable=" + mInitializationTimeoutCancellable);
             pw.println("mLocationListeningAccountant=" + mLocationListeningAccountant);
-            pw.println("mLastLocationToken=" + mLastLocationToken);
+            String locationTokenString =
+                    mLastLocationToken == null ? "null" : mLastLocationToken.toPiiString();
+            pw.println("mLastLocationToken=" + locationTokenString);
             pw.println();
             pw.println("Mode history:");
             mCurrentMode.dump(pw);
@@ -305,8 +310,8 @@
                 return;
             }
 
-            String debugInfo = "onActiveListeningResult()"
-                    + ", activeListeningResult=" + activeListeningResult;
+            PiiLoggable debugInfo = PiiLoggables.fromTemplate("onActiveListeningResult(),"
+                            + " activeListeningResult=%s", activeListeningResult);
             logDebug(debugInfo);
 
             // Recover any active listening budget we didn't use.
@@ -366,19 +371,22 @@
         cancelInitializationTimeout();
 
         Mode currentMode = mCurrentMode.get();
-        String debugInfo = "handleLocationKnown(), locationResult=" + locationResult
-                + ", currentMode.mListenMode=" + prettyPrintListenModeEnum(currentMode.mListenMode);
+        PiiLoggable debugInfo = PiiLoggables.fromTemplate(
+                "handleLocationKnown(), locationResult=%s"
+                +", currentMode.mListenMode=" + prettyPrintListenModeEnum(currentMode.mListenMode),
+                locationResult);
         logDebug(debugInfo);
 
         try {
             sendTimeZoneCertainResultIfNeeded(locationResult.getLocation());
         } catch (IOException e) {
             // This should never happen.
-            String lookupFailureDebugInfo = "IOException while looking up location."
-                    + " previous debugInfo=" + debugInfo;
+            PiiLoggable lookupFailureDebugInfo = PiiLoggables.fromTemplate(
+                    "IOException while looking up location. previous debugInfo=%s", debugInfo);
             logWarn(lookupFailureDebugInfo, e);
 
-            enterFailedMode(new IOException(lookupFailureDebugInfo, e));
+            enterFailedMode(new IOException(lookupFailureDebugInfo.toString(), e),
+                    lookupFailureDebugInfo);
         }
     }
 
@@ -409,7 +417,8 @@
      * @param duration the duration that listening took place for
      */
     private void onPassiveListeningEnded(@NonNull Duration duration) {
-        String debugInfo = "onPassiveListeningEnded()";
+        PiiLoggable debugInfo = PiiLoggables.fromString(
+                "onPassiveListeningEnded(), duration=" + duration);
         logDebug(debugInfo);
 
         synchronized (mLock) {
@@ -473,11 +482,11 @@
             // If the location token is the same as the last lookup, there is no need to do the
             // lookup / send another suggestion.
             if (locationToken.equals(mLastLocationToken)) {
-                logDebug("Location token=" + locationToken + " has not changed.");
+                logDebug("Location token has not changed.");
             } else {
                 List<String> tzIds =
                         geoTimeZonesFinder.findTimeZonesForLocationToken(locationToken);
-                logDebug("tzIds found for location=" + location + ", tzIds=" + tzIds);
+                logDebug("tzIds found for locationToken=" + locationToken + ", tzIds=" + tzIds);
                 // Rather than use the current elapsed realtime clock, use the time associated with
                 // the location since that gives a more accurate answer.
                 long elapsedRealtimeMillis =
@@ -564,9 +573,9 @@
 
     @GuardedBy("mLock")
     private void enterStartedMode(
-            @NonNull Duration initializationTimeout, @NonNull String debugInfo) {
+            @NonNull Duration initializationTimeout, @NonNull PiiLoggable entryCause) {
         Objects.requireNonNull(initializationTimeout);
-        Objects.requireNonNull(debugInfo);
+        Objects.requireNonNull(entryCause);
 
         // The request contains the initialization time in which the LTZP is given to provide the
         // first result. We set a timeout to try to ensure that we do send a result.
@@ -576,24 +585,23 @@
                 this::onInitializationTimeout, initializationToken,
                 initializationTimeout);
 
-        startNextLocationListening(debugInfo);
+        startNextLocationListening(entryCause);
     }
 
     @GuardedBy("mLock")
-    private void enterFailedMode(@NonNull Throwable entryCause) {
-        logDebug("Provider entering failed mode, entryCause=" + entryCause);
+    private void enterFailedMode(@NonNull Throwable failure, @NonNull PiiLoggable entryCause) {
+        logDebug(entryCause);
 
         cancelTimeoutsAndLocationCallbacks();
 
-        sendPermanentFailureResult(entryCause);
+        sendPermanentFailureResult(failure);
 
-        String failureReason = entryCause.getMessage();
-        Mode newMode = new Mode(MODE_FAILED, failureReason);
+        Mode newMode = new Mode(MODE_FAILED, entryCause);
         mCurrentMode.set(newMode);
     }
 
     @GuardedBy("mLock")
-    private void enterStoppedMode(@NonNull String entryCause) {
+    private void enterStoppedMode(@NonNull PiiLoggable entryCause) {
         logDebug("Provider entering stopped mode, entryCause=" + entryCause);
 
         cancelTimeoutsAndLocationCallbacks();
@@ -607,7 +615,7 @@
     }
 
     @GuardedBy("mLock")
-    private void startNextLocationListening(@NonNull String entryCause) {
+    private void startNextLocationListening(@NonNull PiiLoggable entryCause) {
         logDebug("Provider entering location listening mode entryCause=" + entryCause);
 
         Mode currentMode = mCurrentMode.get();
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 9b13d13..25aff0b 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
@@ -494,6 +494,12 @@
 
             @Override
             public String toString() {
+                // Using the debug string is ok for test code.
+                return toPiiString();
+            }
+
+            @Override
+            public String toPiiString() {
                 return "FakeLocationToken{"
                         + "mLngDegrees=" + mLngDegrees
                         + ", mLatDegrees=" + mLatDegrees