1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.timezone;
18 
19 import android.annotation.DurationMillisLong;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SdkConstant;
23 import android.annotation.SystemApi;
24 import android.app.Service;
25 import android.content.Intent;
26 import android.os.Handler;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 import android.os.SystemClock;
30 import android.util.Log;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.os.BackgroundThread;
34 
35 import java.io.FileDescriptor;
36 import java.io.PrintWriter;
37 import java.util.Objects;
38 
39 /**
40  * A service to generate time zone callbacks to the platform. Developers must extend this class.
41  *
42  * <p>Provider implementations are started via a call to {@link #onStartUpdates(long)} and stopped
43  * via a call to {@link #onStopUpdates()}.
44  *
45  * <p>Once started, providers are expected to detect the time zone if possible, and report the
46  * result via {@link #reportSuggestion(TimeZoneProviderSuggestion)} or {@link
47  * #reportUncertain(TimeZoneProviderStatus)}. Providers may also report that they have permanently
48  * failed by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each
49  * method for details.
50  *
51  * <p>After starting, providers are expected to issue their first callback within the timeout
52  * duration specified in {@link #onStartUpdates(long)}, or they will be implicitly considered to be
53  * uncertain.
54  *
55  * <p>Once stopped or failed, providers are required to stop generating callbacks.
56  *
57  * <p>Provider types:
58  *
59  * <p>Android supports up to two <em>location-derived</em> time zone providers. These are called the
60  * "primary" and "secondary" location time zone providers. When a location-derived time zone is
61  * required, the primary location time zone provider is started first and used until it becomes
62  * uncertain or fails, at which point the secondary provider will be started. The secondary will be
63  * started and stopped as needed.
64  *
65  * <p>Provider discovery:
66  *
67  * <p>Each provider is optional and can be disabled. When enabled, a provider's package name must
68  * be explicitly configured in the system server, see {@code
69  * config_primaryLocationTimeZoneProviderPackageName} and {@code
70  * config_secondaryLocationTimeZoneProviderPackageName} for details.
71  *
72  * <p>You must declare the service in the AndroidManifest of the app hosting the provider with the
73  * {@link android.Manifest.permission#BIND_TIME_ZONE_PROVIDER_SERVICE} permission,
74  * and include an intent filter with the necessary action indicating that it is the primary
75  * provider ({@link #PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE}) or the secondary
76  * provider ({@link #SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE}).
77  *
78  * <p>Besides declaring the android:permission attribute mentioned above, the application supplying
79  * a location provider must be granted the {@link
80  * android.Manifest.permission#INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE} permission to be
81  * accepted by the system server.
82  *
83  * <p>{@link TimeZoneProviderService}s may be deployed into processes that run once-per-user
84  * or once-per-device (i.e. they service multiple users). See serviceIsMultiuser metadata below for
85  * configuration details.
86  *
87  * <p>The service may specify metadata on its capabilities:
88  *
89  * <ul>
90  *     <li>
91  *         "serviceIsMultiuser": A boolean property, indicating if the service wishes to take
92  *         responsibility for handling changes to the current user on the device. If true, the
93  *         service will always be bound from the system user. If false, the service will always be
94  *         bound from the current user. If the current user changes, the old binding will be
95  *         released, and a new binding established under the new user. Assumed to be false if not
96  *         specified.
97  *     </li>
98  * </ul>
99  *
100  * <p>For example:
101  * <pre>
102  *   &lt;uses-permission
103  *       android:name="android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE"/&gt;
104  *
105  * ...
106  *
107  *     &lt;service android:name=".ExampleTimeZoneProviderService"
108  *             android:exported="true"
109  *             android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE"&gt;
110  *         &lt;intent-filter&gt;
111  *             &lt;action
112  *             android:name="android.service.timezone.SecondaryLocationTimeZoneProviderService"
113  *             /&gt;
114  *         &lt;/intent-filter&gt;
115  *         &lt;meta-data android:name="serviceIsMultiuser" android:value="true" /&gt;
116  *     &lt;/service&gt;
117  * </pre>
118  *
119  * <p>Threading:
120  *
121  * <p>Outgoing calls to {@code report} methods can be made on any thread and will be delivered
122  * asynchronously to the system server. Incoming calls to {@link TimeZoneProviderService}-defined
123  * service methods like {@link #onStartUpdates(long)} and {@link #onStopUpdates()} are also
124  * asynchronous with respect to the system server caller and will be delivered to this service using
125  * a single thread. {@link Service} lifecycle method calls like {@link #onCreate()} and {@link
126  * #onDestroy()} can occur on a different thread from those made to {@link
127  * TimeZoneProviderService}-defined service methods, so implementations must be defensive and not
128  * assume an ordering between them, e.g. a call to {@link #onStopUpdates()} can occur after {@link
129  * #onDestroy()} and should be handled safely. {@link #mLock} is used to ensure that synchronous
130  * calls like {@link #dump(FileDescriptor, PrintWriter, String[])} are safe with respect to
131  * asynchronous behavior.
132  *
133  * @hide
134  */
135 @SystemApi
136 public abstract class TimeZoneProviderService extends Service {
137 
138     private static final String TAG = "TimeZoneProviderService";
139 
140     /**
141      * The test command result key indicating whether a command succeeded. Value type: boolean
142      * @hide
143      */
144     public static final String TEST_COMMAND_RESULT_SUCCESS_KEY = "SUCCESS";
145 
146     /**
147      * The test command result key for the error message present when {@link
148      * #TEST_COMMAND_RESULT_SUCCESS_KEY} is false. Value type: string
149      * @hide
150      */
151     public static final String TEST_COMMAND_RESULT_ERROR_KEY = "ERROR";
152 
153     /**
154      * The Intent action that the primary location-derived time zone provider service must respond
155      * to. Add it to the intent filter of the service in its manifest.
156      */
157     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
158     public static final String PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE =
159             "android.service.timezone.PrimaryLocationTimeZoneProviderService";
160 
161     /**
162      * The Intent action that the secondary location-based time zone provider service must respond
163      * to. Add it to the intent filter of the service in its manifest.
164      */
165     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
166     public static final String SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE =
167             "android.service.timezone.SecondaryLocationTimeZoneProviderService";
168 
169     private final TimeZoneProviderServiceWrapper mWrapper = new TimeZoneProviderServiceWrapper();
170 
171     /** The object used for operations that occur between the main / handler thread. */
172     private final Object mLock = new Object();
173 
174     /** The handler used for most operations. */
175     private final Handler mHandler = BackgroundThread.getHandler();
176 
177     /** Set by {@link #mHandler} thread. */
178     @GuardedBy("mLock")
179     @Nullable
180     private ITimeZoneProviderManager mManager;
181 
182     /** Set by {@link #mHandler} thread. */
183     @GuardedBy("mLock")
184     private long mEventFilteringAgeThresholdMillis;
185 
186     /**
187      * The type of the last suggestion sent to the system server. Used to de-dupe suggestions client
188      * side and avoid calling into the system server unnecessarily. {@code null} means no previous
189      * event has been sent this cycle; this field is cleared when the service is started.
190      */
191     @GuardedBy("mLock")
192     @Nullable
193     private TimeZoneProviderEvent mLastEventSent;
194 
195     @Override
196     @NonNull
onBind(@onNull Intent intent)197     public final IBinder onBind(@NonNull Intent intent) {
198         return mWrapper;
199     }
200 
201     /**
202      * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for
203      * details.
204      */
reportSuggestion(@onNull TimeZoneProviderSuggestion suggestion)205     public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion) {
206         TimeZoneProviderStatus providerStatus = null;
207         reportSuggestionInternal(suggestion, providerStatus);
208     }
209 
210     /**
211      * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for
212      * details.
213      *
214      * @param providerStatus provider status information that can influence detector service
215      *   behavior and/or be reported via the device UI
216      */
reportSuggestion(@onNull TimeZoneProviderSuggestion suggestion, @NonNull TimeZoneProviderStatus providerStatus)217     public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion,
218             @NonNull TimeZoneProviderStatus providerStatus) {
219         Objects.requireNonNull(providerStatus);
220         reportSuggestionInternal(suggestion, providerStatus);
221     }
222 
reportSuggestionInternal(@onNull TimeZoneProviderSuggestion suggestion, @Nullable TimeZoneProviderStatus providerStatus)223     private void reportSuggestionInternal(@NonNull TimeZoneProviderSuggestion suggestion,
224             @Nullable TimeZoneProviderStatus providerStatus) {
225         Objects.requireNonNull(suggestion);
226 
227         mHandler.post(() -> {
228             synchronized (mLock) {
229                 ITimeZoneProviderManager manager = mManager;
230                 if (manager != null) {
231                     try {
232                         TimeZoneProviderEvent thisEvent =
233                                 TimeZoneProviderEvent.createSuggestionEvent(
234                                         SystemClock.elapsedRealtime(), suggestion, providerStatus);
235                         if (shouldSendEvent(thisEvent)) {
236                             manager.onTimeZoneProviderEvent(thisEvent);
237                             mLastEventSent = thisEvent;
238                         }
239                     } catch (RemoteException | RuntimeException e) {
240                         Log.w(TAG, e);
241                     }
242                 }
243             }
244         });
245     }
246 
247     /**
248      * Indicates the time zone is not known because of an expected runtime state or error, e.g. when
249      * the provider is unable to detect location, or there was connectivity issue.
250      *
251      * <p>See {@link #reportUncertain(TimeZoneProviderStatus)} for a more expressive version
252      */
reportUncertain()253     public final void reportUncertain() {
254         TimeZoneProviderStatus providerStatus = null;
255         reportUncertainInternal(providerStatus);
256     }
257 
258     /**
259      * Indicates the time zone is not known because of an expected runtime state or error.
260      *
261      * <p>When the status changes then a certain or uncertain report must be made to move the
262      * detector service to the new status.
263      *
264      * @param providerStatus provider status information that can influence detector service
265      *   behavior and/or be reported via the device UI
266      */
reportUncertain(@onNull TimeZoneProviderStatus providerStatus)267     public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) {
268         Objects.requireNonNull(providerStatus);
269         reportUncertainInternal(providerStatus);
270     }
271 
reportUncertainInternal(@ullable TimeZoneProviderStatus providerStatus)272     private void reportUncertainInternal(@Nullable TimeZoneProviderStatus providerStatus) {
273         mHandler.post(() -> {
274             synchronized (mLock) {
275                 ITimeZoneProviderManager manager = mManager;
276                 if (manager != null) {
277                     try {
278                         TimeZoneProviderEvent thisEvent =
279                                 TimeZoneProviderEvent.createUncertainEvent(
280                                         SystemClock.elapsedRealtime(), providerStatus);
281                         if (shouldSendEvent(thisEvent)) {
282                             manager.onTimeZoneProviderEvent(thisEvent);
283                             mLastEventSent = thisEvent;
284                         }
285                     } catch (RemoteException | RuntimeException e) {
286                         Log.w(TAG, e);
287                     }
288                 }
289             }
290         });
291     }
292 
293     /**
294      * Indicates there was a permanent failure. This is not generally expected, and probably means a
295      * required backend service has been turned down, or the client is unreasonably old.
296      */
reportPermanentFailure(@onNull Throwable cause)297     public final void reportPermanentFailure(@NonNull Throwable cause) {
298         Objects.requireNonNull(cause);
299 
300         mHandler.post(() -> {
301             synchronized (mLock) {
302                 ITimeZoneProviderManager manager = mManager;
303                 if (manager != null) {
304                     try {
305                         String causeString = cause.getMessage();
306                         TimeZoneProviderEvent thisEvent =
307                                 TimeZoneProviderEvent.createPermanentFailureEvent(
308                                         SystemClock.elapsedRealtime(), causeString);
309                         if (shouldSendEvent(thisEvent)) {
310                             manager.onTimeZoneProviderEvent(thisEvent);
311                             mLastEventSent = thisEvent;
312                         }
313                     } catch (RemoteException | RuntimeException e) {
314                         Log.w(TAG, e);
315                     }
316                 }
317             }
318         });
319     }
320 
321     @GuardedBy("mLock")
shouldSendEvent(TimeZoneProviderEvent newEvent)322     private boolean shouldSendEvent(TimeZoneProviderEvent newEvent) {
323         // Always send an event if it indicates a state or suggestion change.
324         if (!newEvent.isEquivalentTo(mLastEventSent)) {
325             return true;
326         }
327 
328         // Guard against implementations that generate a lot of uninteresting events in a short
329         // space of time and would cause the time_zone_detector to evaluate time zone suggestions
330         // too frequently.
331         //
332         // If the new event and last event sent are equivalent, the client will still send an update
333         // if their creation times are sufficiently different. This enables the time_zone_detector
334         // to better understand how recently the location time zone provider was certain /
335         // uncertain, which can be useful when working out ordering of events, e.g. to work out
336         // whether a suggestion was generated before or after a device left airplane mode.
337         long timeSinceLastEventMillis =
338                 newEvent.getCreationElapsedMillis() - mLastEventSent.getCreationElapsedMillis();
339         return timeSinceLastEventMillis > mEventFilteringAgeThresholdMillis;
340     }
341 
onStartUpdatesInternal(@onNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis, @DurationMillisLong long eventFilteringAgeThresholdMillis)342     private void onStartUpdatesInternal(@NonNull ITimeZoneProviderManager manager,
343             @DurationMillisLong long initializationTimeoutMillis,
344             @DurationMillisLong long eventFilteringAgeThresholdMillis) {
345         synchronized (mLock) {
346             mManager = manager;
347             mEventFilteringAgeThresholdMillis =  eventFilteringAgeThresholdMillis;
348             mLastEventSent = null;
349             onStartUpdates(initializationTimeoutMillis);
350         }
351     }
352 
353     /**
354      * Informs the provider that it should start detecting and reporting the detected time zone
355      * state via the various {@code report} methods. Implementations of {@link
356      * #onStartUpdates(long)} should return immediately, and will typically be used to start
357      * worker threads or begin asynchronous location listening.
358      *
359      * <p>Between {@link #onStartUpdates(long)} and {@link #onStopUpdates()} calls, the Android
360      * system server holds the latest report from the provider in memory. After an initial report,
361      * provider implementations are only required to send a report via {@link
362      * #reportSuggestion(TimeZoneProviderSuggestion, TimeZoneProviderStatus)} or via {@link
363      * #reportUncertain(TimeZoneProviderStatus)} when it differs from the previous report.
364      *
365      * <p>{@link #reportPermanentFailure(Throwable)} can also be called by provider implementations
366      * in rare cases, after which the provider should consider itself stopped and not make any
367      * further reports. {@link #onStopUpdates()} will not be called in this case.
368      *
369      * <p>The {@code initializationTimeoutMillis} parameter indicates how long the provider has been
370      * granted to call one of the {@code report} methods for the first time. If the provider does
371      * not call one of the {@code report} methods in this time, it may be judged uncertain and the
372      * Android system server may move on to use other providers or detection methods. Providers
373      * should therefore make best efforts during this time to generate a report, which could involve
374      * increased power usage. Providers should preferably report an explicit {@link
375      * #reportUncertain(TimeZoneProviderStatus)} if the time zone(s) cannot be detected within the
376      * initialization timeout.
377      *
378      * @see #onStopUpdates() for the signal from the system server to stop sending reports
379      */
onStartUpdates(@urationMillisLong long initializationTimeoutMillis)380     public abstract void onStartUpdates(@DurationMillisLong long initializationTimeoutMillis);
381 
onStopUpdatesInternal()382     private void onStopUpdatesInternal() {
383         synchronized (mLock) {
384             onStopUpdates();
385             mManager = null;
386         }
387     }
388 
389     /**
390      * Stops the provider sending further updates. This will be called after {@link
391      * #onStartUpdates(long)}.
392      */
onStopUpdates()393     public abstract void onStopUpdates();
394 
395     /** @hide */
396     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)397     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
398         synchronized (mLock) {
399             writer.append("mLastEventSent=" + mLastEventSent);
400         }
401     }
402 
403     private class TimeZoneProviderServiceWrapper extends ITimeZoneProvider.Stub {
404 
startUpdates(@onNull ITimeZoneProviderManager manager, @DurationMillisLong long initializationTimeoutMillis, @DurationMillisLong long eventFilteringAgeThresholdMillis)405         public void startUpdates(@NonNull ITimeZoneProviderManager manager,
406                 @DurationMillisLong long initializationTimeoutMillis,
407                 @DurationMillisLong long eventFilteringAgeThresholdMillis) {
408             Objects.requireNonNull(manager);
409             mHandler.post(() -> onStartUpdatesInternal(
410                     manager, initializationTimeoutMillis, eventFilteringAgeThresholdMillis));
411         }
412 
stopUpdates()413         public void stopUpdates() {
414             mHandler.post(TimeZoneProviderService.this::onStopUpdatesInternal);
415         }
416     }
417 }
418