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