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 package com.android.timezone.updater;
17 
18 import android.app.timezone.Callback;
19 import android.app.timezone.DistroFormatVersion;
20 import android.app.timezone.DistroRulesVersion;
21 import android.app.timezone.RulesManager;
22 import android.app.timezone.RulesState;
23 import android.app.timezone.RulesUpdaterContract;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ProviderInfo;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.ParcelFileDescriptor;
33 import android.os.UserHandle;
34 import android.provider.TimeZoneRulesDataContract;
35 import android.util.Log;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.util.Arrays;
44 import libcore.io.Streams;
45 
46 /**
47  * A broadcast receiver triggered by an
48  * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK intent} from the system server in
49  * response to the installation/replacement/uninstallation of a time zone data app.
50  *
51  * <p>The trigger intent contains a {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN byte[] check
52  * token} which must be returned to the system server {@link RulesManager} API via one of the
53  * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback) install},
54  * {@link RulesManager#requestUninstall(byte[], Callback)} or
55  * {@link RulesManager#requestNothing(byte[], boolean)} methods.
56  *
57  * <p>The RulesCheckReceiver is responsible for handling the operation requested by the data app.
58  * The data app makes its payload available via a {@link TimeZoneRulesDataContract specified}
59  * {@link android.content.ContentProvider} with the URI {@link TimeZoneRulesDataContract#AUTHORITY}.
60  *
61  * <p>If the {@link TimeZoneRulesDataContract.Operation#COLUMN_TYPE operation type} is an
62  * {@link TimeZoneRulesDataContract.Operation#TYPE_INSTALL install request}, then the time zone data
63  * format {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MAJOR_VERSION major version} and
64  * {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MINOR_VERSION minor version}, the
65  * {@link TimeZoneRulesDataContract.Operation#COLUMN_RULES_VERSION IANA rules version}, and the
66  * {@link TimeZoneRulesDataContract.Operation#COLUMN_REVISION revision} are checked to see if they
67  * can be applied to the device. If the data is valid the {@link RulesCheckReceiver} will obtain
68  * the payload from the data app content provider via
69  * {@link android.content.ContentProvider#openFile(Uri, String)} and pass the data to the system
70  * server for installation via the
71  * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback)}.
72  */
73 public class RulesCheckReceiver extends BroadcastReceiver {
74     final static String TAG = "RulesCheckReceiver";
75 
76     private RulesManager mRulesManager;
77 
78     @Override
onReceive(Context context, Intent intent)79     public void onReceive(Context context, Intent intent) {
80         // No need to make this synchronized, onReceive() is called on the main thread, there's no
81         // important object state that could be corrupted and the check token allows for ordering
82         // issues.
83         if (!RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK.equals(intent.getAction())) {
84             // Unknown. Do nothing.
85             Log.w(TAG, "Unrecognized intent action received: " + intent
86                     + ", action=" + intent.getAction());
87             return;
88         }
89 
90         // The time zone update process should run as the system user exclusively as it's a
91         // system feature, not user dependent.
92         UserHandle currentUserHandle = android.os.Process.myUserHandle();
93         if (!currentUserHandle.isSystem()) {
94             // Just do nothing.
95             Log.w(TAG, "Supposed to be running as the system user,"
96                     + " instead running as user=" + currentUserHandle);
97             return;
98         }
99 
100         mRulesManager = (RulesManager) context.getSystemService("timezone");
101 
102         byte[] token = intent.getByteArrayExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN);
103         EventLogTags.writeTimezoneCheckTriggerReceived(Arrays.toString(token));
104 
105         if (shouldUninstallCurrentInstall(context)) {
106             Log.i(TAG, "Device should be returned to having no time zone distro installed, issuing"
107                     + " uninstall request");
108             // Uninstall is a no-op if nothing is installed.
109             handleUninstall(token);
110             return;
111         }
112 
113         // Note: We rely on the system server to check that the configured data application is the
114         // one that exposes the content provider with the well-known authority, and is a privileged
115         // application as required. It is *not* checked here and it is assumed the updater can trust
116         // the data application.
117 
118         // Obtain the information about what the data app is telling us to do.
119         DistroOperation operation = getOperation(context, token);
120         if (operation == null) {
121             Log.w(TAG, "Unable to read time zone operation. Halting check.");
122             boolean success = true; // No point in retrying.
123             handleCheckComplete(token, success);
124             return;
125         }
126 
127         // Try to do what the data app asked.
128         Log.d(TAG, "Time zone operation: " + operation + " received.");
129         switch (operation.mType) {
130             case TimeZoneRulesDataContract.Operation.TYPE_NO_OP:
131                 // No-op. Just acknowledge the check.
132                 handleCheckComplete(token, true /* success */);
133                 break;
134             case TimeZoneRulesDataContract.Operation.TYPE_UNINSTALL:
135                 handleUninstall(token);
136                 break;
137             case TimeZoneRulesDataContract.Operation.TYPE_INSTALL:
138                 handleCopyAndInstall(context, token, operation.mDistroFormatVersion,
139                         operation.mDistroRulesVersion);
140                 break;
141             default:
142                 Log.w(TAG, "Unknown time zone operation: " + operation
143                         + " received. Halting check.");
144                 final boolean success = true; // No point in retrying.
145                 handleCheckComplete(token, success);
146         }
147     }
148 
shouldUninstallCurrentInstall(Context context)149     private boolean shouldUninstallCurrentInstall(Context context) {
150         int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
151         PackageManager packageManager = context.getPackageManager();
152         ProviderInfo providerInfo =
153                 packageManager.resolveContentProvider(TimeZoneRulesDataContract.AUTHORITY, flags);
154         if (providerInfo == null || providerInfo.applicationInfo == null) {
155             Log.w(TAG, "No package/application info available for content provider "
156                     + TimeZoneRulesDataContract.AUTHORITY);
157             // Something has gone wrong. Trying to return the device to clean is a reasonable
158             // response.
159             return true;
160         }
161 
162         // If the data app is the one from /system, we can treat this as "uninstall": if nothing
163         // is installed then the system will treat this as a no-op, and if something is installed
164         // this will stage an uninstall.
165         // We could install the distro from an app contained in the system image but we assume it's
166         // going to contain the same time zone data as the base version and would be a no op.
167 
168         ApplicationInfo applicationInfo = providerInfo.applicationInfo;
169         // isPrivilegedApp() => initial install directory for app /system/priv-app (required)
170         // isUpdatedSystemApp() => app has been replaced by an updated version that resides in /data
171         return applicationInfo.isPrivilegedApp() && !applicationInfo.isUpdatedSystemApp();
172     }
173 
getOperation(Context context, byte[] tokenBytes)174     private DistroOperation getOperation(Context context, byte[] tokenBytes) {
175         EventLogTags.writeTimezoneCheckReadFromDataApp(Arrays.toString(tokenBytes));
176         Cursor c = context.getContentResolver()
177                 .query(TimeZoneRulesDataContract.Operation.CONTENT_URI,
178                         new String[] {
179                                 TimeZoneRulesDataContract.Operation.COLUMN_TYPE,
180                                 TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MAJOR_VERSION,
181                                 TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MINOR_VERSION,
182                                 TimeZoneRulesDataContract.Operation.COLUMN_RULES_VERSION,
183                                 TimeZoneRulesDataContract.Operation.COLUMN_REVISION
184                         },
185                         null /* selection */, null /* selectionArgs */, null /* sortOrder */);
186         try (Cursor cursor = c) {
187             if (cursor == null) {
188                 Log.e(TAG, "Query returned null");
189                 return null;
190             }
191             if (!cursor.moveToFirst()) {
192                 Log.e(TAG, "Query returned empty results");
193                 return null;
194             }
195 
196             try {
197                 String type = cursor.getString(0);
198                 DistroFormatVersion distroFormatVersion = null;
199                 DistroRulesVersion distroRulesVersion = null;
200                 if (TimeZoneRulesDataContract.Operation.TYPE_INSTALL.equals(type)) {
201                     distroFormatVersion = new DistroFormatVersion(cursor.getInt(1),
202                             cursor.getInt(2));
203                     distroRulesVersion = new DistroRulesVersion(cursor.getString(3),
204                             cursor.getInt(4));
205                 }
206                 return new DistroOperation(type, distroFormatVersion, distroRulesVersion);
207             } catch (Exception e) {
208                 Log.e(TAG, "Error looking up distro operation / version", e);
209                 return null;
210             }
211         }
212     }
213 
handleCopyAndInstall(Context context, byte[] checkToken, DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion)214     private void handleCopyAndInstall(Context context, byte[] checkToken,
215             DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion) {
216         // Decide whether to proceed with the install.
217         RulesState rulesState = mRulesManager.getRulesState();
218         if (!rulesState.isDistroFormatVersionSupported(distroFormatVersion)
219             || rulesState.isBaseVersionNewerThan(distroRulesVersion)) {
220             Log.d(TAG, "Candidate distro is not supported or is not better than base version.");
221             // Nothing to do.
222             handleCheckComplete(checkToken, true /* success */);
223             return;
224         }
225 
226         ParcelFileDescriptor inputFileDescriptor = getDistroParcelFileDescriptor(context);
227         if (inputFileDescriptor == null) {
228             Log.e(TAG, "No local file created for distro. Halting.");
229             return;
230         }
231 
232         // Copying the ParcelFileDescriptor to a local file proves we can read it before passing it
233         // on to the next stage. It also ensures that we have a hermetic copy of the data we know
234         // the originating content provider cannot modify unexpectedly. If the next stage wants to
235         // "seek" the ParcelFileDescriptor it can do so with fewer processes affected.
236         File file = copyDataToLocalFile(context, inputFileDescriptor);
237         if (file == null) {
238             Log.e(TAG, "Failed to copy distro data to a file.");
239             // It's possible this may get better if the problem is related to storage space so we
240             // signal success := false so it may be retried.
241             boolean success = false;
242             handleCheckComplete(checkToken, success);
243             return;
244         }
245         handleInstall(checkToken, file);
246     }
247 
getDistroParcelFileDescriptor(Context context)248     private static ParcelFileDescriptor getDistroParcelFileDescriptor(Context context) {
249         ParcelFileDescriptor inputFileDescriptor;
250         try {
251             inputFileDescriptor = context.getContentResolver().openFileDescriptor(
252                     TimeZoneRulesDataContract.Operation.CONTENT_URI, "r");
253             if (inputFileDescriptor == null) {
254                 throw new FileNotFoundException("ContentProvider returned null");
255             }
256         } catch (FileNotFoundException e) {
257             Log.e(TAG, "Unable to open file descriptor"
258                     + TimeZoneRulesDataContract.Operation.CONTENT_URI, e);
259             return null;
260         }
261         return inputFileDescriptor;
262     }
263 
copyDataToLocalFile( Context context, ParcelFileDescriptor inputFileDescriptor)264     private static File copyDataToLocalFile(
265             Context context, ParcelFileDescriptor inputFileDescriptor) {
266 
267         // Adopt the ParcelFileDescriptor into a try-with-resources so we will close it when we're
268         // done regardless of the outcome.
269         try (ParcelFileDescriptor pfd = inputFileDescriptor) {
270             File localFile;
271             try {
272                 localFile = File.createTempFile("temp", ".zip", context.getFilesDir());
273             } catch (IOException e) {
274                 Log.e(TAG, "Unable to create local storage file", e);
275                 return null;
276             }
277 
278             InputStream fis = new FileInputStream(pfd.getFileDescriptor(), false /* isFdOwner */);
279             try (FileOutputStream fos = new FileOutputStream(localFile, false /* append */)) {
280                 Streams.copy(fis, fos);
281             } catch (IOException e) {
282                 Log.e(TAG, "Unable to create asset storage file: " + localFile, e);
283                 return null;
284             }
285             return localFile;
286         } catch (IOException e) {
287             Log.e(TAG, "Unable to close ParcelFileDescriptor", e);
288             return null;
289         }
290     }
291 
handleInstall(final byte[] checkToken, final File localFile)292     private void handleInstall(final byte[] checkToken, final File localFile) {
293         // Create a ParcelFileDescriptor pointing to localFile.
294         final ParcelFileDescriptor distroFileDescriptor;
295         try {
296             distroFileDescriptor =
297                     ParcelFileDescriptor.open(localFile, ParcelFileDescriptor.MODE_READ_ONLY);
298         } catch (FileNotFoundException e) {
299             Log.e(TAG, "Unable to create ParcelFileDescriptor from " + localFile);
300             handleCheckComplete(checkToken, false /* success */);
301             return;
302         } finally {
303             // It is safe to delete the File at this point. The ParcelFileDescriptor has an open
304             // file descriptor to it if we are successful, or it is not going to be used if we are
305             // returning early.
306             localFile.delete();
307         }
308 
309         Callback callback = new Callback() {
310             @Override
311             public void onFinished(int status) {
312                 Log.i(TAG, "Finished install: " + status);
313             }
314         };
315 
316         // Adopt the distroFileDescriptor here so the local file descriptor is closed, whatever the
317         // outcome.
318         try (ParcelFileDescriptor pfd = distroFileDescriptor) {
319             String tokenString = Arrays.toString(checkToken);
320             EventLogTags.writeTimezoneCheckRequestInstall(tokenString);
321             int requestStatus = mRulesManager.requestInstall(pfd, checkToken, callback);
322             Log.i(TAG, "requestInstall() called, token=" + tokenString
323                     + ", returned " + requestStatus);
324         } catch (Exception e) {
325             Log.e(TAG, "Error calling requestInstall()", e);
326         }
327     }
328 
handleUninstall(byte[] checkToken)329     private void handleUninstall(byte[] checkToken) {
330         Callback callback = new Callback() {
331             @Override
332             public void onFinished(int status) {
333                 Log.i(TAG, "Finished uninstall: " + status);
334             }
335         };
336 
337         try {
338             String tokenString = Arrays.toString(checkToken);
339             EventLogTags.writeTimezoneCheckRequestUninstall(tokenString);
340             int requestStatus = mRulesManager.requestUninstall(checkToken, callback);
341             Log.i(TAG, "requestUninstall() called, token=" + tokenString
342                     + ", returned " + requestStatus);
343         } catch (Exception e) {
344             Log.e(TAG, "Error calling requestUninstall()", e);
345         }
346     }
347 
handleCheckComplete(final byte[] token, final boolean success)348     private void handleCheckComplete(final byte[] token, final boolean success) {
349         try {
350             String tokenString = Arrays.toString(token);
351             EventLogTags.writeTimezoneCheckRequestNothing(tokenString, success ? 1 : 0);
352             mRulesManager.requestNothing(token, success);
353             Log.i(TAG, "requestNothing() called, token=" + tokenString + ", success=" + success);
354         } catch (Exception e) {
355             Log.e(TAG, "Error calling requestNothing()", e);
356         }
357     }
358 
359     private static class DistroOperation {
360         final String mType;
361         final DistroFormatVersion mDistroFormatVersion;
362         final DistroRulesVersion mDistroRulesVersion;
363 
DistroOperation(String type, DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion)364         DistroOperation(String type, DistroFormatVersion distroFormatVersion,
365                 DistroRulesVersion distroRulesVersion) {
366             mType = type;
367             mDistroFormatVersion = distroFormatVersion;
368             mDistroRulesVersion = distroRulesVersion;
369         }
370 
371         @Override
toString()372         public String toString() {
373             return "DistroOperation{" +
374                     "mType='" + mType + '\'' +
375                     ", mDistroFormatVersion=" + mDistroFormatVersion +
376                     ", mDistroRulesVersion=" + mDistroRulesVersion +
377                     '}';
378         }
379     }
380 }
381