/* * Copyright (C) 2020 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.service.timezone; import android.annotation.DurationMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.BackgroundThread; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Objects; /** * A service to generate time zone callbacks to the platform. Developers must extend this class. * *
Provider implementations are started via a call to {@link #onStartUpdates(long)} and stopped * via a call to {@link #onStopUpdates()}. * *
Once started, providers are expected to detect the time zone if possible, and report the * result via {@link #reportSuggestion(TimeZoneProviderSuggestion)} or {@link * #reportUncertain(TimeZoneProviderStatus)}. Providers may also report that they have permanently * failed by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each * method for details. * *
After starting, providers are expected to issue their first callback within the timeout * duration specified in {@link #onStartUpdates(long)}, or they will be implicitly considered to be * uncertain. * *
Once stopped or failed, providers are required to stop generating callbacks. * *
Provider types: * *
Android supports up to two location-derived time zone providers. These are called the * "primary" and "secondary" location time zone providers. When a location-derived time zone is * required, the primary location time zone provider is started first and used until it becomes * uncertain or fails, at which point the secondary provider will be started. The secondary will be * started and stopped as needed. * *
Provider discovery: * *
Each provider is optional and can be disabled. When enabled, a provider's package name must * be explicitly configured in the system server, see {@code * config_primaryLocationTimeZoneProviderPackageName} and {@code * config_secondaryLocationTimeZoneProviderPackageName} for details. * *
You must declare the service in the AndroidManifest of the app hosting the provider with the * {@link android.Manifest.permission#BIND_TIME_ZONE_PROVIDER_SERVICE} permission, * and include an intent filter with the necessary action indicating that it is the primary * provider ({@link #PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE}) or the secondary * provider ({@link #SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE}). * *
Besides declaring the android:permission attribute mentioned above, the application supplying * a location provider must be granted the {@link * android.Manifest.permission#INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE} permission to be * accepted by the system server. * *
{@link TimeZoneProviderService}s may be deployed into processes that run once-per-user * or once-per-device (i.e. they service multiple users). See serviceIsMultiuser metadata below for * configuration details. * *
The service may specify metadata on its capabilities: * *
For example: *
* <uses-permission * android:name="android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE"/> * * ... * * <service android:name=".ExampleTimeZoneProviderService" * android:exported="true" * android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE"> * <intent-filter> * <action * android:name="android.service.timezone.SecondaryLocationTimeZoneProviderService" * /> * </intent-filter> * <meta-data android:name="serviceIsMultiuser" android:value="true" /> * </service> ** *
Threading: * *
Outgoing calls to {@code report} methods can be made on any thread and will be delivered * asynchronously to the system server. Incoming calls to {@link TimeZoneProviderService}-defined * service methods like {@link #onStartUpdates(long)} and {@link #onStopUpdates()} are also * asynchronous with respect to the system server caller and will be delivered to this service using * a single thread. {@link Service} lifecycle method calls like {@link #onCreate()} and {@link * #onDestroy()} can occur on a different thread from those made to {@link * TimeZoneProviderService}-defined service methods, so implementations must be defensive and not * assume an ordering between them, e.g. a call to {@link #onStopUpdates()} can occur after {@link * #onDestroy()} and should be handled safely. {@link #mLock} is used to ensure that synchronous * calls like {@link #dump(FileDescriptor, PrintWriter, String[])} are safe with respect to * asynchronous behavior. * * @hide */ @SystemApi public abstract class TimeZoneProviderService extends Service { private static final String TAG = "TimeZoneProviderService"; /** * The test command result key indicating whether a command succeeded. Value type: boolean * @hide */ public static final String TEST_COMMAND_RESULT_SUCCESS_KEY = "SUCCESS"; /** * The test command result key for the error message present when {@link * #TEST_COMMAND_RESULT_SUCCESS_KEY} is false. Value type: string * @hide */ public static final String TEST_COMMAND_RESULT_ERROR_KEY = "ERROR"; /** * The Intent action that the primary location-derived time zone provider service must respond * to. Add it to the intent filter of the service in its manifest. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.PrimaryLocationTimeZoneProviderService"; /** * The Intent action that the secondary location-based time zone provider service must respond * to. Add it to the intent filter of the service in its manifest. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.SecondaryLocationTimeZoneProviderService"; private final TimeZoneProviderServiceWrapper mWrapper = new TimeZoneProviderServiceWrapper(); /** The object used for operations that occur between the main / handler thread. */ private final Object mLock = new Object(); /** The handler used for most operations. */ private final Handler mHandler = BackgroundThread.getHandler(); /** Set by {@link #mHandler} thread. */ @GuardedBy("mLock") @Nullable private ITimeZoneProviderManager mManager; /** Set by {@link #mHandler} thread. */ @GuardedBy("mLock") private long mEventFilteringAgeThresholdMillis; /** * The type of the last suggestion sent to the system server. Used to de-dupe suggestions client * side and avoid calling into the system server unnecessarily. {@code null} means no previous * event has been sent this cycle; this field is cleared when the service is started. */ @GuardedBy("mLock") @Nullable private TimeZoneProviderEvent mLastEventSent; @Override @NonNull public final IBinder onBind(@NonNull Intent intent) { return mWrapper; } /** * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for * details. */ public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion) { TimeZoneProviderStatus providerStatus = null; reportSuggestionInternal(suggestion, providerStatus); } /** * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for * details. * * @param providerStatus provider status information that can influence detector service * behavior and/or be reported via the device UI */ public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion, @NonNull TimeZoneProviderStatus providerStatus) { Objects.requireNonNull(providerStatus); reportSuggestionInternal(suggestion, providerStatus); } private void reportSuggestionInternal(@NonNull TimeZoneProviderSuggestion suggestion, @Nullable TimeZoneProviderStatus providerStatus) { Objects.requireNonNull(suggestion); mHandler.post(() -> { synchronized (mLock) { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { TimeZoneProviderEvent thisEvent = TimeZoneProviderEvent.createSuggestionEvent( SystemClock.elapsedRealtime(), suggestion, providerStatus); if (shouldSendEvent(thisEvent)) { manager.onTimeZoneProviderEvent(thisEvent); mLastEventSent = thisEvent; } } catch (RemoteException | RuntimeException e) { Log.w(TAG, e); } } } }); } /** * Indicates the time zone is not known because of an expected runtime state or error, e.g. when * the provider is unable to detect location, or there was connectivity issue. * *
See {@link #reportUncertain(TimeZoneProviderStatus)} for a more expressive version */ public final void reportUncertain() { TimeZoneProviderStatus providerStatus = null; reportUncertainInternal(providerStatus); } /** * Indicates the time zone is not known because of an expected runtime state or error. * *
When the status changes then a certain or uncertain report must be made to move the * detector service to the new status. * * @param providerStatus provider status information that can influence detector service * behavior and/or be reported via the device UI */ public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) { Objects.requireNonNull(providerStatus); reportUncertainInternal(providerStatus); } private void reportUncertainInternal(@Nullable TimeZoneProviderStatus providerStatus) { mHandler.post(() -> { synchronized (mLock) { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { TimeZoneProviderEvent thisEvent = TimeZoneProviderEvent.createUncertainEvent( SystemClock.elapsedRealtime(), providerStatus); if (shouldSendEvent(thisEvent)) { manager.onTimeZoneProviderEvent(thisEvent); mLastEventSent = thisEvent; } } catch (RemoteException | RuntimeException e) { Log.w(TAG, e); } } } }); } /** * Indicates there was a permanent failure. This is not generally expected, and probably means a * required backend service has been turned down, or the client is unreasonably old. */ public final void reportPermanentFailure(@NonNull Throwable cause) { Objects.requireNonNull(cause); mHandler.post(() -> { synchronized (mLock) { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { String causeString = cause.getMessage(); TimeZoneProviderEvent thisEvent = TimeZoneProviderEvent.createPermanentFailureEvent( SystemClock.elapsedRealtime(), causeString); if (shouldSendEvent(thisEvent)) { manager.onTimeZoneProviderEvent(thisEvent); mLastEventSent = thisEvent; } } catch (RemoteException | RuntimeException e) { Log.w(TAG, e); } } } }); } @GuardedBy("mLock") private boolean shouldSendEvent(TimeZoneProviderEvent newEvent) { // Always send an event if it indicates a state or suggestion change. if (!newEvent.isEquivalentTo(mLastEventSent)) { return true; } // Guard against implementations that generate a lot of uninteresting events in a short // space of time and would cause the time_zone_detector to evaluate time zone suggestions // too frequently. // // If the new event and last event sent are equivalent, the client will still send an update // if their creation times are sufficiently different. This enables the time_zone_detector // to better understand how recently the location time zone provider was certain / // uncertain, which can be useful when working out ordering of events, e.g. to work out // whether a suggestion was generated before or after a device left airplane mode. long timeSinceLastEventMillis = newEvent.getCreationElapsedMillis() - mLastEventSent.getCreationElapsedMillis(); return timeSinceLastEventMillis > mEventFilteringAgeThresholdMillis; } private void onStartUpdatesInternal(@NonNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis, @DurationMillisLong long eventFilteringAgeThresholdMillis) { synchronized (mLock) { mManager = manager; mEventFilteringAgeThresholdMillis = eventFilteringAgeThresholdMillis; mLastEventSent = null; onStartUpdates(initializationTimeoutMillis); } } /** * Informs the provider that it should start detecting and reporting the detected time zone * state via the various {@code report} methods. Implementations of {@link * #onStartUpdates(long)} should return immediately, and will typically be used to start * worker threads or begin asynchronous location listening. * *
Between {@link #onStartUpdates(long)} and {@link #onStopUpdates()} calls, the Android * system server holds the latest report from the provider in memory. After an initial report, * provider implementations are only required to send a report via {@link * #reportSuggestion(TimeZoneProviderSuggestion, TimeZoneProviderStatus)} or via {@link * #reportUncertain(TimeZoneProviderStatus)} when it differs from the previous report. * *
{@link #reportPermanentFailure(Throwable)} can also be called by provider implementations * in rare cases, after which the provider should consider itself stopped and not make any * further reports. {@link #onStopUpdates()} will not be called in this case. * *
The {@code initializationTimeoutMillis} parameter indicates how long the provider has been * granted to call one of the {@code report} methods for the first time. If the provider does * not call one of the {@code report} methods in this time, it may be judged uncertain and the * Android system server may move on to use other providers or detection methods. Providers * should therefore make best efforts during this time to generate a report, which could involve * increased power usage. Providers should preferably report an explicit {@link * #reportUncertain(TimeZoneProviderStatus)} if the time zone(s) cannot be detected within the * initialization timeout. * * @see #onStopUpdates() for the signal from the system server to stop sending reports */ public abstract void onStartUpdates(@DurationMillisLong long initializationTimeoutMillis); private void onStopUpdatesInternal() { synchronized (mLock) { onStopUpdates(); mManager = null; } } /** * Stops the provider sending further updates. This will be called after {@link * #onStartUpdates(long)}. */ public abstract void onStopUpdates(); /** @hide */ @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { synchronized (mLock) { writer.append("mLastEventSent=" + mLastEventSent); } } private class TimeZoneProviderServiceWrapper extends ITimeZoneProvider.Stub { public void startUpdates(@NonNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis, @DurationMillisLong long eventFilteringAgeThresholdMillis) { Objects.requireNonNull(manager); mHandler.post(() -> onStartUpdatesInternal( manager, initializationTimeoutMillis, eventFilteringAgeThresholdMillis)); } public void stopUpdates() { mHandler.post(TimeZoneProviderService.this::onStopUpdatesInternal); } } }