/* * 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.util.Log; import com.android.internal.os.BackgroundThread; 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()}. 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: * *

Calls to {@code report} methods can be made on on any thread and will be passed asynchronously * to the system server. Calls to {@link #onStartUpdates(long)} and {@link #onStopUpdates()} will * occur on a single thread. * * @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(); private final Handler mHandler = BackgroundThread.getHandler(); /** Set by {@link #mHandler} thread. */ @Nullable private ITimeZoneProviderManager mManager; @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) { Objects.requireNonNull(suggestion); mHandler.post(() -> { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { manager.onTimeZoneProviderSuggestion(suggestion); } 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 a problem when resolving the location * to a time zone. */ public final void reportUncertain() { mHandler.post(() -> { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { manager.onTimeZoneProviderUncertain(); } 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(() -> { ITimeZoneProviderManager manager = mManager; if (manager != null) { try { manager.onTimeZoneProviderPermanentFailure(cause.getMessage()); } catch (RemoteException | RuntimeException e) { Log.w(TAG, e); } } }); } private void onStartUpdatesInternal(@NonNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis) { mManager = manager; 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)} or via {@link #reportUncertain()} 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()} 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() { onStopUpdates(); mManager = null; } /** * Stops the provider sending further updates. This will be called after {@link * #onStartUpdates(long)}. */ public abstract void onStopUpdates(); private class TimeZoneProviderServiceWrapper extends ITimeZoneProvider.Stub { public void startUpdates(@NonNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis) { Objects.requireNonNull(manager); mHandler.post(() -> onStartUpdatesInternal(manager, initializationTimeoutMillis)); } public void stopUpdates() { mHandler.post(TimeZoneProviderService.this::onStopUpdatesInternal); } } }