1 /*
2  * Copyright (C) 2017 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.app.timezone;
18 
19 import android.annotation.IntDef;
20 import android.content.Context;
21 import android.os.Handler;
22 import android.os.ParcelFileDescriptor;
23 import android.os.RemoteException;
24 import android.os.ServiceManager;
25 import android.util.Log;
26 
27 import java.io.IOException;
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 import java.util.Arrays;
31 
32 /**
33  * The interface through which a time zone update application interacts with the Android system
34  * to handle time zone rule updates.
35  *
36  * <p>This interface is intended for use with the default APK-based time zone rules update
37  * application but it can also be used by OEMs if that mechanism is turned off using configuration.
38  * All callers must possess the {@link android.Manifest.permission#UPDATE_TIME_ZONE_RULES} system
39  * permission unless otherwise stated.
40  *
41  * <p>When using the default mechanism, when properly configured the Android system will send a
42  * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with a
43  * {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} extra to the time zone rules updater application
44  * when it detects that it or the OEM's APK containing time zone rules data has been modified. The
45  * updater application is then responsible for calling one of
46  * {@link #requestInstall(ParcelFileDescriptor, byte[], Callback)},
47  * {@link #requestUninstall(byte[], Callback)} or
48  * {@link #requestNothing(byte[], boolean)}, indicating, respectively, whether a new time zone rules
49  * distro should be installed, the current distro should be uninstalled, or there is nothing to do
50  * (or that the correct operation could not be determined due to an error). In each case the updater
51  * must pass the {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} value it received from the intent
52  * back so the system in the {@code checkToken} parameter.
53  *
54  * <p>If OEMs want to handle their own time zone rules updates, perhaps via a server-side component
55  * rather than an APK, then they should disable the default triggering mechanism in config and are
56  * responsible for triggering their own update checks / installs / uninstalls. In this case the
57  * "check token" parameter can be left null and there is never any need to call
58  * {@link #requestNothing(byte[], boolean)}.
59  *
60  * <p>OEMs should not mix the default mechanism and their own as this could lead to conflicts and
61  * unnecessary checks being triggered.
62  *
63  * <p>Applications obtain this using {@link android.app.Activity#getSystemService(String)} with
64  * {@link Context#TIME_ZONE_RULES_MANAGER_SERVICE}.
65  * @hide
66  */
67 public final class RulesManager {
68     private static final String TAG = "timezone.RulesManager";
69     private static final boolean DEBUG = false;
70 
71     /**
72      * The action of the intent that the Android system will broadcast when a time zone rules update
73      * operation has been successfully staged  (i.e. to be applied next reboot) or unstaged.
74      *
75      * <p>See {@link #EXTRA_OPERATION_STAGED}
76      *
77      * <p>This is a protected intent that can only be sent by the system.
78      */
79     public static final String ACTION_RULES_UPDATE_OPERATION =
80             "com.android.intent.action.timezone.RULES_UPDATE_OPERATION";
81 
82     /**
83      * The key for a boolean extra for the {@link #ACTION_RULES_UPDATE_OPERATION} intent used to
84      * indicate whether the operation was a "stage" or an "unstage".
85      */
86     public static final String EXTRA_OPERATION_STAGED = "staged";
87 
88     @Retention(RetentionPolicy.SOURCE)
89     @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = {
90             SUCCESS,
91             ERROR_UNKNOWN_FAILURE,
92             ERROR_OPERATION_IN_PROGRESS
93     })
94     public @interface ResultCode {}
95 
96     /**
97      * Indicates that an operation succeeded.
98      */
99     public static final int SUCCESS = 0;
100 
101     /**
102      * Indicates that an install/uninstall cannot be initiated because there is one already in
103      * progress.
104      */
105     public static final int ERROR_OPERATION_IN_PROGRESS = 1;
106 
107     /**
108      * Indicates an install / uninstall did not fully succeed for an unknown reason.
109      */
110     public static final int ERROR_UNKNOWN_FAILURE = 2;
111 
112     private final Context mContext;
113     private final IRulesManager mIRulesManager;
114 
RulesManager(Context context)115     public RulesManager(Context context) {
116         mContext = context;
117         mIRulesManager = IRulesManager.Stub.asInterface(
118                 ServiceManager.getService(Context.TIME_ZONE_RULES_MANAGER_SERVICE));
119     }
120 
121     /**
122      * Returns information about the current time zone rules state such as the IANA version of
123      * the system and any currently installed distro. This method allows clients to determine the
124      * current device state, perhaps to see if it can be improved; for example by passing the
125      * information to a server that may provide a new distro for download.
126      *
127      * <p>Callers must possess the {@link android.Manifest.permission#QUERY_TIME_ZONE_RULES} system
128      * permission.
129      */
getRulesState()130     public RulesState getRulesState() {
131         try {
132             logDebug("mIRulesManager.getRulesState()");
133             RulesState rulesState = mIRulesManager.getRulesState();
134             logDebug("mIRulesManager.getRulesState() returned " + rulesState);
135             return rulesState;
136         } catch (RemoteException e) {
137             throw e.rethrowFromSystemServer();
138         }
139     }
140 
141     /**
142      * Requests installation of the supplied distro. The distro must have been checked for integrity
143      * by the caller or have been received via a trusted mechanism.
144      *
145      * @param distroFileDescriptor the file descriptor for the distro
146      * @param checkToken an optional token provided if the install was triggered in response to a
147      *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
148      * @param callback the {@link Callback} to receive callbacks related to the installation
149      * @return {@link #SUCCESS} if the installation will be attempted
150      */
151     @ResultCode
requestInstall( ParcelFileDescriptor distroFileDescriptor, byte[] checkToken, Callback callback)152     public int requestInstall(
153             ParcelFileDescriptor distroFileDescriptor, byte[] checkToken, Callback callback)
154             throws IOException {
155 
156         ICallback iCallback = new CallbackWrapper(mContext, callback);
157         try {
158             logDebug("mIRulesManager.requestInstall()");
159             return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback);
160         } catch (RemoteException e) {
161             throw e.rethrowFromSystemServer();
162         }
163     }
164 
165     /**
166      * Requests uninstallation of the currently installed distro (leaving the device with no
167      * distro installed).
168      *
169      * @param checkToken an optional token provided if the uninstall was triggered in response to a
170      *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
171      * @param callback the {@link Callback} to receive callbacks related to the uninstall
172      * @return {@link #SUCCESS} if the uninstallation will be attempted
173      */
174     @ResultCode
requestUninstall(byte[] checkToken, Callback callback)175     public int requestUninstall(byte[] checkToken, Callback callback) {
176         ICallback iCallback = new CallbackWrapper(mContext, callback);
177         try {
178             logDebug("mIRulesManager.requestUninstall()");
179             return mIRulesManager.requestUninstall(checkToken, iCallback);
180         } catch (RemoteException e) {
181             throw e.rethrowFromSystemServer();
182         }
183     }
184 
185     /*
186      * We wrap incoming binder calls with a private class implementation that
187      * redirects them into main-thread actions.  This serializes the backup
188      * progress callbacks nicely within the usual main-thread lifecycle pattern.
189      */
190     private class CallbackWrapper extends ICallback.Stub {
191         final Handler mHandler;
192         final Callback mCallback;
193 
CallbackWrapper(Context context, Callback callback)194         CallbackWrapper(Context context, Callback callback) {
195             mCallback = callback;
196             mHandler = new Handler(context.getMainLooper());
197         }
198 
199         // Binder calls into this object just enqueue on the main-thread handler
200         @Override
onFinished(int status)201         public void onFinished(int status) {
202             logDebug("mCallback.onFinished(status), status=" + status);
203             mHandler.post(() -> mCallback.onFinished(status));
204         }
205     }
206 
207     /**
208      * Requests the system does not modify the currently installed time zone distro, if any. This
209      * method records the fact that a time zone check operation triggered by the system is now
210      * complete and there was nothing to do. The token passed should be the one presented when the
211      * check was triggered.
212      *
213      * <p>Note: Passing {@code success == false} may result in more checks being triggered. Clients
214      * should be careful not to pass false if the failure is unlikely to resolve by itself.
215      *
216      * @param checkToken an optional token provided if the install was triggered in response to a
217      *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
218      * @param succeeded true if the check was successful, false if it was not successful but may
219      *     succeed if it is retried
220      */
requestNothing(byte[] checkToken, boolean succeeded)221     public void requestNothing(byte[] checkToken, boolean succeeded) {
222         try {
223             logDebug("mIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
224             mIRulesManager.requestNothing(checkToken, succeeded);
225         } catch (RemoteException e) {
226             throw e.rethrowFromSystemServer();
227         }
228     }
229 
logDebug(String msg)230     static void logDebug(String msg) {
231         if (DEBUG) {
232             Log.v(TAG, msg);
233         }
234     }
235 }
236