| /* |
| * 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. |
| * |
| * <p>Provider implementations are started via a call to {@link #onStartUpdates(long)} and stopped |
| * via a call to {@link #onStopUpdates()}. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>Once stopped or failed, providers are required to stop generating callbacks. |
| * |
| * <p>Provider types: |
| * |
| * <p>Android supports up to two <em>location-derived</em> 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. |
| * |
| * <p>Provider discovery: |
| * |
| * <p>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. |
| * |
| * <p>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}). |
| * |
| * <p>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. |
| * |
| * <p>{@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. |
| * |
| * <p>The service may specify metadata on its capabilities: |
| * |
| * <ul> |
| * <li> |
| * "serviceIsMultiuser": A boolean property, indicating if the service wishes to take |
| * responsibility for handling changes to the current user on the device. If true, the |
| * service will always be bound from the system user. If false, the service will always be |
| * bound from the current user. If the current user changes, the old binding will be |
| * released, and a new binding established under the new user. Assumed to be false if not |
| * specified. |
| * </li> |
| * </ul> |
| * |
| * <p>For example: |
| * <pre> |
| * <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> |
| * </pre> |
| * |
| * <p>Threading: |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>{@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. |
| * |
| * <p>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); |
| } |
| } |
| } |