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