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

Commit 6bfc89fe authored by Neil Fuller's avatar Neil Fuller
Browse files

Extract time zone storage logic from AlarmManager

Extract time zone storage logic from AlarmManagerService and add storage
for new time zone metadata.

This simplifies the AlarmManagerService, which would otherwise have to
expose the time zone metadata (or more knowledge of the storage would be
duplicated elsewhere).

The new metadata describes the confidence Android has in the current
time zone setting. It is intended to support upcoming APIs for SetUp
Wizard, which should ask the user to confirm the time zone.  Currently
the SUW tries to infer this by watching for time zone changes, but this
is error prone and prevents the time zone detector from defaulting the
time zone using low-confidence (but still better than the hardcoded
"GMT") time zones today.

This change also includes small refactorings to how AlarmManagerService
tries to keep the kernel's time zone offset in sync with Android's time
zone setting. Code comments have also been added to try to clarify
behavior.

The system property defaulting behavior previously in SystemServer is
moved to SystemTimeZone too so that all the logic associated with the
system property is now in one place.

Bug: 236612872
Test: manual testing + inspection / treehugger
Test: atest services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
Change-Id: I493d31043f22d32f12793e0c35110233c850ed85
parent 8aa891d5
Loading
Loading
Loading
Loading
+51 −32
Original line number Diff line number Diff line
@@ -39,6 +39,8 @@ import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROU
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
import static android.os.UserHandle.USER_SYSTEM;

import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;
import static com.android.server.SystemTimeZone.getTimeZoneId;
import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
import static com.android.server.alarm.Alarm.BATTERY_SAVER_POLICY_INDEX;
import static com.android.server.alarm.Alarm.DEVICE_IDLE_POLICY_INDEX;
@@ -144,6 +146,8 @@ import com.android.server.JobSchedulerBackgroundThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.SystemTimeZone;
import com.android.server.SystemTimeZone.TimeZoneConfidence;
import com.android.server.pm.permission.PermissionManagerService;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.pm.pkg.AndroidPackage;
@@ -199,7 +203,6 @@ public class AlarmManagerService extends SystemService {
    static final boolean DEBUG_TARE = localLOGV || false;
    static final boolean RECORD_ALARMS_IN_HISTORY = true;
    static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
    static final String TIMEZONE_PROPERTY = "persist.sys.timezone";

    static final int TICK_HISTORY_DEPTH = 10;
    static final long INDEFINITE_DELAY = 365 * INTERVAL_DAY;
@@ -1881,9 +1884,10 @@ public class AlarmManagerService extends SystemService {

            mNextWakeup = mNextNonWakeup = 0;

            // We have to set current TimeZone info to kernel
            // because kernel doesn't keep this after reboot
            setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
            // We set the current offset in kernel because the kernel doesn't keep this after a
            // reboot. Keeping the kernel time zone in sync is "best effort" and can be wrong
            // for a period after daylight savings transitions.
            mInjector.syncKernelTimeZoneOffset();

            // Ensure that we're booting with a halfway sensible current time.  Use the
            // most recent of Build.TIME, the root file system's timestamp, and the
@@ -2117,12 +2121,14 @@ public class AlarmManagerService extends SystemService {
        synchronized (mLock) {
            final long currentTimeMillis = mInjector.getCurrentTimeMillis();
            mInjector.setKernelTime(millis);
            final TimeZone timeZone = TimeZone.getDefault();

            // Changing the time may cross a DST transition; sync the kernel offset if needed.
            final TimeZone timeZone = TimeZone.getTimeZone(SystemTimeZone.getTimeZoneId());
            final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
            final int newTzOffset = timeZone.getOffset(millis);
            if (currentTzOffset != newTzOffset) {
                Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
                mInjector.setKernelTimezone(-(newTzOffset / 60000));
                mInjector.setKernelTimeZoneOffset(newTzOffset);
            }
            // The native implementation of setKernelTime can return -1 even when the kernel
            // time was set correctly, so assume setting kernel time was successful and always
@@ -2131,31 +2137,28 @@ public class AlarmManagerService extends SystemService {
        }
    }

    void setTimeZoneImpl(String tz) {
        if (TextUtils.isEmpty(tz)) {
    void setTimeZoneImpl(String tzId, @TimeZoneConfidence int confidence) {
        if (TextUtils.isEmpty(tzId)) {
            return;
        }

        TimeZone zone = TimeZone.getTimeZone(tz);
        TimeZone newZone = TimeZone.getTimeZone(tzId);
        // Prevent reentrant calls from stepping on each other when writing
        // the time zone property
        boolean timeZoneWasChanged = false;
        boolean timeZoneWasChanged;
        synchronized (this) {
            String current = SystemProperties.get(TIMEZONE_PROPERTY);
            if (current == null || !current.equals(zone.getID())) {
                if (localLOGV) {
                    Slog.v(TAG, "timezone changed: " + current + ", new=" + zone.getID());
                }
                timeZoneWasChanged = true;
                SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());
            }
            // TimeZone.getTimeZone() can return a time zone with a different ID (e.g. it can return
            // "GMT" if the ID is unrecognized). The parameter ID is used here rather than
            // newZone.getId(). It will be rejected if it is invalid.
            timeZoneWasChanged = SystemTimeZone.setTimeZoneId(tzId, confidence);

            // Update the kernel timezone information
            // Kernel tracks time offsets as 'minutes west of GMT'
            int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
            mInjector.setKernelTimezone(-(gmtOffset / 60000));
            int utcOffsetMillis = newZone.getOffset(mInjector.getCurrentTimeMillis());
            mInjector.setKernelTimeZoneOffset(utcOffsetMillis);
        }

        // Clear the default time zone in the system server process. This forces the next call
        // to TimeZone.getDefault() to re-read the device settings.
        TimeZone.setDefault(null);

        if (timeZoneWasChanged) {
@@ -2168,7 +2171,7 @@ public class AlarmManagerService extends SystemService {
                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                    | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                    | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
            intent.putExtra(Intent.EXTRA_TIMEZONE, zone.getID());
            intent.putExtra(Intent.EXTRA_TIMEZONE, newZone.getID());
            mOptsTimeBroadcast.setTemporaryAppAllowlist(
                    mActivityManagerInternal.getBootTimeTempAllowListDuration(),
                    TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
@@ -2684,6 +2687,11 @@ public class AlarmManagerService extends SystemService {
                    || hasUseExactAlarmInternal(packageName, uid);
        }

        @Override
        public void setTimeZone(String tzId, @TimeZoneConfidence int confidence) {
            setTimeZoneImpl(tzId, confidence);
        }

        @Override
        public void registerInFlightListener(InFlightListener callback) {
            synchronized (mLock) {
@@ -2969,7 +2977,11 @@ public class AlarmManagerService extends SystemService {

            final long oldId = Binder.clearCallingIdentity();
            try {
                setTimeZoneImpl(tz);
                // The public API (and the shell command that also uses this method) have no concept
                // of confidence, but since the time zone ID should come either from apps working on
                // behalf of the user or a developer, confidence is assumed "high".
                final int timeZoneConfidence = TIME_ZONE_CONFIDENCE_HIGH;
                setTimeZoneImpl(tz, timeZoneConfidence);
            } finally {
                Binder.restoreCallingIdentity(oldId);
            }
@@ -4541,8 +4553,18 @@ public class AlarmManagerService extends SystemService {
            return AlarmManagerService.getNextAlarm(mNativeData, type);
        }

        void setKernelTimezone(int minutesWest) {
            AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
        void setKernelTimeZoneOffset(int utcOffsetMillis) {
            // Kernel tracks time offsets as 'minutes west of GMT'
            AlarmManagerService.setKernelTimezone(mNativeData, -(utcOffsetMillis / 60000));
        }

        void syncKernelTimeZoneOffset() {
            long currentTimeMillis = getCurrentTimeMillis();
            TimeZone currentTimeZone = TimeZone.getTimeZone(getTimeZoneId());
            // If the time zone ID is invalid, GMT will be returned and this will set a kernel
            // offset of zero.
            int utcOffsetMillis = currentTimeZone.getOffset(currentTimeMillis);
            setKernelTimeZoneOffset(utcOffsetMillis);
        }

        void setKernelTime(long millis) {
@@ -5009,13 +5031,10 @@ public class AlarmManagerService extends SystemService {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED)) {
                // Since the kernel does not keep track of DST, we need to
                // reset the TZ information at the beginning of each day
                // based off of the current Zone gmt offset + userspace tracked
                // daylight savings information.
                TimeZone zone = TimeZone.getTimeZone(SystemProperties.get(TIMEZONE_PROPERTY));
                int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
                mInjector.setKernelTimezone(-(gmtOffset / 60000));
                // Since the kernel does not keep track of DST, we reset the TZ information at the
                // beginning of each day. This may miss a DST transition, but it will correct itself
                // within 24 hours.
                mInjector.syncKernelTimeZoneOffset();
                scheduleDateChangedEvent();
            }
        }
+11 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.server;

import android.app.PendingIntent;

import com.android.server.SystemTimeZone.TimeZoneConfidence;

public interface AlarmManagerInternal {
    // Some other components in the system server need to know about
    // broadcast alarms currently in flight
@@ -48,4 +50,13 @@ public interface AlarmManagerInternal {
     * {@link android.Manifest.permission#USE_EXACT_ALARM}.
     */
    boolean hasExactAlarmPermission(String packageName, int uid);

    /**
     * Sets the device's current time zone and time zone confidence.
     *
     * @param tzId the time zone ID
     * @param confidence the confidence that {@code tzId} is correct, see {@link TimeZoneConfidence}
     *     for details
     */
    void setTimeZone(String tzId, @TimeZoneConfidence int confidence);
}
+160 −0
Original line number Diff line number Diff line
/*
 * Copyright 2022 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.server;

import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Slog;

import com.android.i18n.timezone.ZoneInfoDb;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * A set of constants and static methods that encapsulate knowledge of how time zone and associated
 * metadata are stored on Android.
 */
public final class SystemTimeZone {

    private static final String TAG = "SystemTimeZone";
    private static final boolean DEBUG = false;
    private static final String TIME_ZONE_SYSTEM_PROPERTY = "persist.sys.timezone";
    private static final String TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY =
            "persist.sys.timezone_confidence";

    /**
     * The "special" time zone ID used as a low-confidence default when the device's time zone
     * is empty or invalid during boot.
     */
    private static final String DEFAULT_TIME_ZONE_ID = "GMT";

    /**
     * An annotation that indicates a "time zone confidence" value is expected.
     *
     * <p>The confidence indicates whether the time zone is expected to be correct. The confidence
     * can be upgraded or downgraded over time. It can be used to decide whether a user could /
     * should be asked to confirm the time zone. For example, during device set up low confidence
     * would describe a time zone that has been initialized by default or by using low quality
     * or ambiguous signals. The user may then be asked to confirm the time zone, moving it to a
     * high confidence.
     */
    @Retention(SOURCE)
    @Target(TYPE_USE)
    @IntDef(prefix = "TIME_ZONE_CONFIDENCE_",
            value = { TIME_ZONE_CONFIDENCE_LOW, TIME_ZONE_CONFIDENCE_HIGH })
    public @interface TimeZoneConfidence {
    }

    /** Used when confidence is low and would (ideally) be confirmed by a user. */
    public static final @TimeZoneConfidence int TIME_ZONE_CONFIDENCE_LOW = 0;
    /**
     * Used when confidence in the time zone is high and does not need to be confirmed by a user.
     */
    public static final @TimeZoneConfidence int TIME_ZONE_CONFIDENCE_HIGH = 100;

    private SystemTimeZone() {}

    /**
     * Called during device boot to validate and set the time zone ID to a low-confidence default.
     */
    public static void initializeTimeZoneSettingsIfRequired() {
        String timezoneProperty = SystemProperties.get(TIME_ZONE_SYSTEM_PROPERTY);
        if (!isValidTimeZoneId(timezoneProperty)) {
            Slog.w(TAG, TIME_ZONE_SYSTEM_PROPERTY + " is not valid (" + timezoneProperty
                    + "); setting to " + DEFAULT_TIME_ZONE_ID);
            setTimeZoneId(DEFAULT_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW);
        }
    }

    /**
     * Updates the device's time zone system property and associated metadata. Returns {@code true}
     * if the device's time zone changed, {@code false} if the ID is invalid or the device is
     * already set to the supplied ID.
     *
     * <p>This method ensures the confidence metadata is set to the supplied value if the supplied
     * time zone ID is considered valid.
     *
     * <p>This method is intended only for use by the AlarmManager. When changing the device's time
     * zone other system service components must use {@link
     * com.android.server.AlarmManagerInternal#setTimeZone(String, int)} to ensure that important
     * system-wide side effects occur.
     */
    public static boolean setTimeZoneId(String timeZoneId, @TimeZoneConfidence int confidence) {
        if (TextUtils.isEmpty(timeZoneId) || !isValidTimeZoneId(timeZoneId)) {
            return false;
        }

        boolean timeZoneChanged = false;
        synchronized (SystemTimeZone.class) {
            String currentTimeZoneId = getTimeZoneId();
            if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
                SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
                if (DEBUG) {
                    Slog.v(TAG, "Time zone changed: " + currentTimeZoneId + ", new=" + timeZoneId);
                }
                timeZoneChanged = true;
            }
            setTimeZoneConfidence(confidence);
        }

        return timeZoneChanged;
    }

    /**
     * Sets the time zone confidence value if required. See {@link TimeZoneConfidence} for details.
     */
    private static void setTimeZoneConfidence(@TimeZoneConfidence int confidence) {
        int currentConfidence = getTimeZoneConfidence();
        if (currentConfidence != confidence) {
            SystemProperties.set(
                    TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, Integer.toString(confidence));
            if (DEBUG) {
                Slog.v(TAG, "Time zone confidence changed: old=" + currentConfidence
                        + ", new=" + confidence);
            }
        }
    }

    /** Returns the time zone confidence value. See {@link TimeZoneConfidence} for details. */
    public static @TimeZoneConfidence int getTimeZoneConfidence() {
        int confidence = SystemProperties.getInt(
                TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, TIME_ZONE_CONFIDENCE_LOW);
        if (!isValidTimeZoneConfidence(confidence)) {
            confidence = TIME_ZONE_CONFIDENCE_LOW;
        }
        return confidence;
    }

    /** Returns the device's time zone ID setting. */
    public static String getTimeZoneId() {
        return SystemProperties.get(TIME_ZONE_SYSTEM_PROPERTY);
    }

    private static boolean isValidTimeZoneConfidence(@TimeZoneConfidence int confidence) {
        return confidence >= TIME_ZONE_CONFIDENCE_LOW && confidence <= TIME_ZONE_CONFIDENCE_HIGH;
    }

    private static boolean isValidTimeZoneId(String timeZoneId) {
        return timeZoneId != null
                && !timeZoneId.isEmpty()
                && ZoneInfoDb.getInstance().hasTimeZone(timeZoneId);
    }
}
+15 −17
Original line number Diff line number Diff line
@@ -18,13 +18,16 @@ package com.android.server.timezonedetector;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlarmManager;
import android.content.Context;
import android.os.Handler;
import android.os.SystemClock;
import android.os.SystemProperties;

import com.android.server.AlarmManagerInternal;
import com.android.server.LocalServices;
import com.android.server.SystemTimeZone;
import com.android.server.SystemTimeZone.TimeZoneConfidence;

import java.util.Objects;

/**
@@ -59,27 +62,22 @@ final class EnvironmentImpl implements TimeZoneDetectorStrategyImpl.Environment
    }

    @Override
    public boolean isDeviceTimeZoneInitialized() {
        // timezone.equals("GMT") will be true and only true if the time zone was
        // set to a default value by the system server (when starting, system server
        // sets the persist.sys.timezone to "GMT" if it's not set). "GMT" is not used by
        // any code that sets it explicitly (in case where something sets GMT explicitly,
        // "Etc/GMT" Olson ID would be used).

        String timeZoneId = getDeviceTimeZone();
        return timeZoneId != null && timeZoneId.length() > 0 && !timeZoneId.equals("GMT");
    @NonNull
    public String getDeviceTimeZone() {
        return SystemProperties.get(TIMEZONE_PROPERTY);
    }

    @Override
    @Nullable
    public String getDeviceTimeZone() {
        return SystemProperties.get(TIMEZONE_PROPERTY);
    public @TimeZoneConfidence int getDeviceTimeZoneConfidence() {
        return SystemTimeZone.getTimeZoneConfidence();
    }

    @Override
    public void setDeviceTimeZone(String zoneId) {
        AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
        alarmManager.setTimeZone(zoneId);
    public void setDeviceTimeZoneAndConfidence(
            @NonNull String zoneId, @TimeZoneConfidence int confidence) {
        AlarmManagerInternal alarmManagerInternal =
                LocalServices.getService(AlarmManagerInternal.class);
        alarmManagerInternal.setTimeZone(zoneId, confidence);
    }

    @Override
+28 −17
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_M
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE;

import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -39,6 +41,7 @@ import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemTimeZone.TimeZoneConfidence;

import java.time.Duration;
import java.util.List;
@@ -73,19 +76,20 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
        @NonNull ConfigurationInternal getCurrentUserConfigurationInternal();

        /**
         * Returns true if the device has had an explicit time zone set.
         * Returns the device's currently configured time zone.
         */
        boolean isDeviceTimeZoneInitialized();
        @NonNull String getDeviceTimeZone();

        /**
         * Returns the device's currently configured time zone.
         * Returns the confidence of the device's current time zone.
         */
        String getDeviceTimeZone();
        @TimeZoneConfidence int getDeviceTimeZoneConfidence();

        /**
         * Sets the device's time zone.
         * Sets the device's time zone and associated confidence.
         */
        void setDeviceTimeZone(@NonNull String zoneId);
        void setDeviceTimeZoneAndConfidence(
                @NonNull String zoneId, @TimeZoneConfidence int confidence);

        /**
         * Returns the time according to the elapsed realtime clock, the same as {@link
@@ -620,25 +624,32 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
    @GuardedBy("this")
    private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) {
        String currentZoneId = mEnvironment.getDeviceTimeZone();

        // Avoid unnecessary changes / intents.
        if (newZoneId.equals(currentZoneId)) {
            // No need to set the device time zone - the setting is already what we would be
            // suggesting.
        // All manual and automatic suggestions are considered high confidence as low-quality
        // suggestions are not currently passed on.
        int newConfidence = TIME_ZONE_CONFIDENCE_HIGH;
        int currentConfidence = mEnvironment.getDeviceTimeZoneConfidence();

        // Avoid unnecessary changes / intents. If the newConfidence is higher than the stored value
        // then we want to upgrade it.
        if (newZoneId.equals(currentZoneId) && newConfidence <= currentConfidence) {
            // No need to modify the device time zone settings.
            if (DBG) {
                Slog.d(LOG_TAG, "No need to change the time zone;"
                        + " device is already set to newZoneId."
                        + ", newZoneId=" + newZoneId
                        + ", cause=" + cause);
                        + ", cause=" + cause
                        + ", currentScore=" + currentConfidence
                        + ", newConfidence=" + newConfidence);
            }
            return;
        }

        mEnvironment.setDeviceTimeZone(newZoneId);
        String logMsg = "Set device time zone."
        mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence);
        String logMsg = "Set device time zone or higher confidence."
                + ", currentZoneId=" + currentZoneId
                + ", newZoneId=" + newZoneId
                + ", cause=" + cause;
                + ", cause=" + cause
                + ", newConfidence=" + newConfidence;
        logTimeZoneDetectorChange(logMsg);
    }

@@ -710,9 +721,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
        ipw.println("mCurrentConfigurationInternal=" + mCurrentConfigurationInternal);
        ipw.println("[Capabilities=" + mCurrentConfigurationInternal.createCapabilitiesAndConfig()
                + "]");
        ipw.println("mEnvironment.isDeviceTimeZoneInitialized()="
                + mEnvironment.isDeviceTimeZoneInitialized());
        ipw.println("mEnvironment.getDeviceTimeZone()=" + mEnvironment.getDeviceTimeZone());
        ipw.println("mEnvironment.getDeviceTimeZoneConfidence()="
                + mEnvironment.getDeviceTimeZoneConfidence());

        ipw.println("Misc state:");
        ipw.increaseIndent(); // level 2
Loading