Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 87ffbf21 authored by Geoffrey Boullanger's avatar Geoffrey Boullanger
Browse files

Created telephony signal (PLMN IDs and NITZ) to be added to time zone suggestion

This will be used by the Fused Time Zone Detector (FTZD) to discard mobile phone networks with wrong configuration that affect time zone detection.

go/android-tz-detector
go/ftzd-algo
go/ftzd-cases
go/ftzd-scenarios

Test: atest FrameworksTimeCoreTests
Test: atest FrameworksTimeServicesTests
Test: atest FrameworksTelephonyTests
Flag: android.timezone.flags.enable_fused_time_zone_detector
Bug: 394770805
Change-Id: I7f20fe377ecc1e17906ab27c3de08f2d0e0bba71
parent 78b8a808
Loading
Loading
Loading
Loading
+213 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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 android.app.timezonedetector;

import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Objects;
import java.util.TimeZone;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.DurationMillisLong;

/**
 * A data class that captures information about a Network Identity and Timezone (NITZ) signal. This
 * information can be used by the device's time zone detector.
 *
 * @hide
 */
public final class NitzSignal implements Parcelable {

    /**
     * The elapsed realtime ({@link android.os.SystemClock#elapsedRealtime()}) when this NITZ signal
     * was received by the device.
     */
    @ElapsedRealtimeLong private final long mReceiptElapsedMillis;

    /** The age of the NITZ signal in milliseconds, measured from its reception time. */
    @DurationMillisLong private final long mAgeMillis;

    /** The time zone offset from UTC in milliseconds, provided by the NITZ signal. */
    private final int mZoneOffset;

    /**
     * The daylight saving time (DST) offset from standard time in milliseconds, provided by the
     * NITZ signal. {@code null} if no DST information is available.
     */
    private final Integer mDstOffset;

    /** The System.currentTimeMillis() corresponding to the NITZ signal's time. */
    private final long mCurrentTimeMillis;

    /**
     * The TimeZone object representing the host time zone of the emulator, if applicable. This is
     * useful for testing purposes.
     */
    @Nullable private final TimeZone mEmulatorHostTimeZone;

    /**
     * Creates a new {@link NitzSignal} instance.
     *
     * @param receiptElapsedMillis The elapsed realtime when the NITZ signal was received.
     * @param ageMillis The age of the NITZ signal in milliseconds.
     * @param zoneOffset The time zone offset from UTC in milliseconds.
     * @param dstOffset The DST offset in milliseconds, or {@code null}.
     * @param currentTimeMillis The System.currentTimeMillis() from the signal.
     * @param emulatorHostTimeZone The emulator host time zone, or a default if not applicable.
     */
    public NitzSignal(
            long receiptElapsedMillis,
            long ageMillis,
            int zoneOffset,
            Integer dstOffset,
            long currentTimeMillis,
            TimeZone emulatorHostTimeZone) {
        this.mReceiptElapsedMillis = receiptElapsedMillis;
        this.mAgeMillis = ageMillis;
        this.mZoneOffset = zoneOffset;
        this.mDstOffset = dstOffset;
        this.mCurrentTimeMillis = currentTimeMillis;
        this.mEmulatorHostTimeZone = emulatorHostTimeZone;
    }

    /** Returns the elapsed realtime when the NITZ signal was received. */
    public long getReceiptElapsedMillis() {
        return mReceiptElapsedMillis;
    }

    /** Returns the age of the NITZ signal in milliseconds. */
    public long getAgeMillis() {
        return mAgeMillis;
    }

    /** Returns the time zone offset from UTC in milliseconds. */
    public int getZoneOffset() {
        return mZoneOffset;
    }

    /** Returns the DST offset in milliseconds, or {@code null}. */
    public Integer getDstOffset() {
        return mDstOffset;
    }

    /** Returns the System.currentTimeMillis() from the signal. */
    public long getCurrentTimeMillis() {
        return mCurrentTimeMillis;
    }

    /** Returns the emulator host time zone. */
    @Nullable
    public TimeZone getEmulatorHostTimeZone() {
        return mEmulatorHostTimeZone;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof NitzSignal that) {
            return mReceiptElapsedMillis == that.mReceiptElapsedMillis
                    && mAgeMillis == that.mAgeMillis
                    && mZoneOffset == that.mZoneOffset
                    && mCurrentTimeMillis == that.mCurrentTimeMillis
                    && Objects.equals(mDstOffset, that.mDstOffset)
                    && Objects.equals(mEmulatorHostTimeZone, that.mEmulatorHostTimeZone);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mReceiptElapsedMillis,
                mAgeMillis,
                mZoneOffset,
                mDstOffset,
                mCurrentTimeMillis,
                mEmulatorHostTimeZone);
    }

    @Override
    public String toString() {
        return "NitzSignal{"
                + "mReceiptElapsedMillis="
                + mReceiptElapsedMillis
                + ", mAgeMillis="
                + mAgeMillis
                + ", mZoneOffset="
                + mZoneOffset
                + ", mDstOffset="
                + mDstOffset
                + ", mCurrentTimeMillis="
                + mCurrentTimeMillis
                + ", mEmulatorHostTimeZone="
                + (mEmulatorHostTimeZone == null ? null : mEmulatorHostTimeZone.getID())
                + '}';
    }

    /** Implement the {@link Parcelable} interface. */
    @Override
    public int describeContents() {
        return 0; // No special object types
    }

    /**
     * Writes the object's data to the parcel.
     *
     * @param dest The parcel to which the object's data is written.
     * @param flags Additional flags about how the object should be written. May be 0 or {@link
     *     #PARCELABLE_WRITE_RETURN_VALUE}.
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(mReceiptElapsedMillis);
        dest.writeLong(mAgeMillis);
        dest.writeInt(mZoneOffset);
        dest.writeSerializable(mDstOffset);
        dest.writeLong(mCurrentTimeMillis);
        dest.writeSerializable(mEmulatorHostTimeZone);
    }

    /** Helper to create {@link NitzSignal} objects from a {@link Parcel}. */
    public static final Creator<NitzSignal> CREATOR =
            new Creator<>() {
                @Override
                public NitzSignal createFromParcel(Parcel in) {
                    long receiptElapsedMillis = in.readLong();
                    long ageMillis = in.readLong();
                    int zoneOffset = in.readInt();
                    Integer dstOffset =
                            in.readSerializable(Integer.class.getClassLoader(), Integer.class);
                    long currentTimeMillis = in.readLong();
                    TimeZone emulatorHostTimeZone =
                            in.readSerializable(TimeZone.class.getClassLoader(), TimeZone.class);
                    return new NitzSignal(
                            receiptElapsedMillis,
                            ageMillis,
                            zoneOffset,
                            dstOffset,
                            currentTimeMillis,
                            emulatorHostTimeZone);
                }

                @Override
                public NitzSignal[] newArray(int size) {
                    return new NitzSignal[size];
                }
            };
}
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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 android.app.timezonedetector;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * A data class that captures information about a telephony signal. This information can be used by
 * the device's time zone detector to infer the device's time zone.
 *
 * @hide
 */
public final class TelephonySignal implements Parcelable {
    /** The Mobile Country Code (MCC) of the registered network. */
    private final String mMcc;

    /** The Mobile Network Code (MNC) of the registered network. Can be {@code null}. */
    @Nullable private final String mMnc;

    /** The default ISO 3166-1 alpha-2 country code derived from the network information. */
    private final String mDefaultCountryIsoCode;

    /** A set of all possible ISO 3166-1 alpha-2 country codes associated with the network. */
    private final Set<String> mCountryIsoCodes;

    /**
     * The NITZ (Network Identity and Timezone) signal information received, if available. Can be
     * {@code null}.
     */
    @Nullable private final NitzSignal mNitzSignal;

    /**
     * Creates a new {@link TelephonySignal} instance.
     *
     * @param mcc The Mobile Country Code (MCC) of the registered network.
     * @param mnc The Mobile Network Code (MNC) of the registered network. Can be {@code null}.
     * @param defaultCountryIsoCode The default ISO 3166-1 alpha-2 country code.
     * @param countryIsoCodes A set of all possible ISO 3166-1 alpha-2 country codes.
     * @param nitzSignal The NITZ signal information, or {@code null}.
     */
    public TelephonySignal(
            String mcc,
            @Nullable String mnc,
            String defaultCountryIsoCode,
            Set<String> countryIsoCodes,
            @Nullable NitzSignal nitzSignal) {
        this.mMcc = Objects.requireNonNull(mcc);
        this.mMnc = mnc;
        this.mDefaultCountryIsoCode = Objects.requireNonNull(defaultCountryIsoCode);
        this.mCountryIsoCodes = Set.copyOf(Objects.requireNonNull(countryIsoCodes));
        this.mNitzSignal = nitzSignal;
    }

    /** Returns the Mobile Country Code (MCC). */
    @NonNull
    public String getMcc() {
        return mMcc;
    }

    /** Returns the Mobile Network Code (MNC), or {@code null}. */
    @Nullable
    public String getMnc() {
        return mMnc;
    }

    /** Returns the default country ISO code. */
    @NonNull
    public String getDefaultCountryIsoCode() {
        return mDefaultCountryIsoCode;
    }

    /** Returns an unmodifiable set of associated country ISO codes. */
    @NonNull
    public Set<String> getCountryIsoCodes() {
        return mCountryIsoCodes;
    }

    /** Returns the NITZ signal information, or {@code null}. */
    @Nullable
    public NitzSignal getNitzSignal() {
        return mNitzSignal;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof TelephonySignal that) {
            return Objects.equals(mMcc, that.mMcc)
                    && Objects.equals(mMnc, that.mMnc)
                    && Objects.equals(mDefaultCountryIsoCode, that.mDefaultCountryIsoCode)
                    && Objects.equals(mCountryIsoCodes, that.mCountryIsoCodes)
                    && Objects.equals(mNitzSignal, that.mNitzSignal);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mMcc, mMnc, mDefaultCountryIsoCode, mCountryIsoCodes, mNitzSignal);
    }

    @Override
    public String toString() {
        return "TelephonySignal{"
                + "mcc='"
                + mMcc
                + '\''
                + ", mnc='"
                + mMnc
                + '\''
                + ", defaultCountryIsoCode='"
                + mDefaultCountryIsoCode
                + '\''
                + ", countryIsoCodes="
                + mCountryIsoCodes
                + ", mNitzSignal="
                + mNitzSignal
                + '}';
    }

    /** Implement the {@link Parcelable} interface. */
    @Override
    public int describeContents() {
        return 0; // No special object types
    }

    /**
     * Writes the object's data to the parcel.
     *
     * @param dest The parcel to which the object's data is written.
     * @param flags Additional flags about how the object should be written. May be 0 or {@link
     *     #PARCELABLE_WRITE_RETURN_VALUE}.
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString8(mMcc);
        dest.writeString8(mMnc);
        dest.writeString8(mDefaultCountryIsoCode);
        // Convert Set to List for parceling
        dest.writeStringList(new ArrayList<>(mCountryIsoCodes));
        dest.writeParcelable(mNitzSignal, flags);
    }

    /** Helper to create {@link TelephonySignal} objects from a {@link Parcel}. */
    public static final Creator<TelephonySignal> CREATOR =
            new Creator<>() {
                @Override
                public TelephonySignal createFromParcel(Parcel in) {
                    String mcc = in.readString8();
                    String mnc = in.readString8();
                    String defaultCountryIsoCode = in.readString8();
                    // Read List and convert back to Set
                    List<String> tempCountryIsoCodes = new ArrayList<>();
                    in.readStringList(tempCountryIsoCodes);
                    Set<String> countryIsoCodes = new HashSet<>(tempCountryIsoCodes);
                    NitzSignal nitzSignal =
                            in.readParcelable(NitzSignal.class.getClassLoader(), NitzSignal.class);
                    return new TelephonySignal(
                            mcc, mnc, defaultCountryIsoCode, countryIsoCodes, nitzSignal);
                }

                @Override
                public TelephonySignal[] newArray(int size) {
                    return new TelephonySignal[size];
                }
            };
}
+186 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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 android.app.timezonedetector;

import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import android.platform.test.annotations.Presubmit;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.TimeZone;

@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class NitzSignalTest {

    private static final long RECEIPT_ELAPSED_MILLIS = 1234L;
    private static final long AGE_MILLIS = 5678L;
    private static final int ZONE_OFFSET = 3600000;
    private static final Integer DST_OFFSET = 3600000;
    private static final long CURRENT_TIME_MILLIS = 987654321L;
    private static final TimeZone EMU_HOST_TZ = TimeZone.getTimeZone("America/Los_Angeles");

    @Test
    public void testEquals() {
        NitzSignal signal1 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        NitzSignal signal2 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertEquals(signal1, signal2);
        assertEquals(signal1.hashCode(), signal2.hashCode());

        NitzSignal signal3 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS + 1,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertNotEquals(signal1, signal3);

        NitzSignal signal4 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS + 1,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertNotEquals(signal1, signal4);

        NitzSignal signal5 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET + 1,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertNotEquals(signal1, signal5);

        NitzSignal signal6 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        null,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertNotEquals(signal1, signal6);

        NitzSignal signal7 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS + 1,
                        EMU_HOST_TZ);
        assertNotEquals(signal1, signal7);

        NitzSignal signal8 =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        TimeZone.getTimeZone("Europe/London"));
        assertNotEquals(signal1, signal8);
    }

    @Test
    public void testGetters() {
        NitzSignal signal =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);

        assertEquals(RECEIPT_ELAPSED_MILLIS, signal.getReceiptElapsedMillis());
        assertEquals(AGE_MILLIS, signal.getAgeMillis());
        assertEquals(ZONE_OFFSET, signal.getZoneOffset());
        assertEquals(DST_OFFSET, signal.getDstOffset());
        assertEquals(CURRENT_TIME_MILLIS, signal.getCurrentTimeMillis());
        assertEquals(EMU_HOST_TZ, signal.getEmulatorHostTimeZone());
    }

    @Test
    public void testParcelable() {
        NitzSignal signalWithDst =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        assertRoundTripParcelable(signalWithDst);

        NitzSignal signalWithoutDst =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        null,
                        CURRENT_TIME_MILLIS,
                        null);
        assertRoundTripParcelable(signalWithoutDst);
    }

    @Test
    public void testToString() {
        NitzSignal signal =
                new NitzSignal(
                        RECEIPT_ELAPSED_MILLIS,
                        AGE_MILLIS,
                        ZONE_OFFSET,
                        DST_OFFSET,
                        CURRENT_TIME_MILLIS,
                        EMU_HOST_TZ);
        String str = signal.toString();
        // A basic check that the toString() method doesn't crash and contains some info.
        assertTrue(str.contains(String.valueOf(RECEIPT_ELAPSED_MILLIS)));
        assertTrue(str.contains(String.valueOf(CURRENT_TIME_MILLIS)));
        assertTrue(str.contains(EMU_HOST_TZ.getID()));
    }
}
+137 −0

File added.

Preview size limit exceeded, changes collapsed.