/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.timezone.updater; import android.app.timezone.Callback; import android.app.timezone.DistroFormatVersion; import android.app.timezone.DistroRulesVersion; import android.app.timezone.RulesManager; import android.app.timezone.RulesState; import android.app.timezone.RulesUpdaterContract; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.provider.TimeZoneRulesDataContract; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import libcore.io.Streams; /** * A broadcast receiver triggered by an * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK intent} from the system server in * response to the installation/replacement/uninstallation of a time zone data app. * *

The trigger intent contains a {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN byte[] check * token} which must be returned to the system server {@link RulesManager} API via one of the * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback) install}, * {@link RulesManager#requestUninstall(byte[], Callback)} or * {@link RulesManager#requestNothing(byte[], boolean)} methods. * *

The RulesCheckReceiver is responsible for handling the operation requested by the data app. * The data app makes its payload available via a {@link TimeZoneRulesDataContract specified} * {@link android.content.ContentProvider} with the URI {@link TimeZoneRulesDataContract#AUTHORITY}. * *

If the {@link TimeZoneRulesDataContract.Operation#COLUMN_TYPE operation type} is an * {@link TimeZoneRulesDataContract.Operation#TYPE_INSTALL install request}, then the time zone data * format {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MAJOR_VERSION major version} and * {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MINOR_VERSION minor version}, the * {@link TimeZoneRulesDataContract.Operation#COLUMN_RULES_VERSION IANA rules version}, and the * {@link TimeZoneRulesDataContract.Operation#COLUMN_REVISION revision} are checked to see if they * can be applied to the device. If the data is valid the {@link RulesCheckReceiver} will obtain * the payload from the data app content provider via * {@link android.content.ContentProvider#openFile(Uri, String)} and pass the data to the system * server for installation via the * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback)}. */ public class RulesCheckReceiver extends BroadcastReceiver { final static String TAG = "RulesCheckReceiver"; private RulesManager mRulesManager; @Override public void onReceive(Context context, Intent intent) { // No need to make this synchronized, onReceive() is called on the main thread, there's no // important object state that could be corrupted and the check token allows for ordering // issues. if (!RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK.equals(intent.getAction())) { // Unknown. Do nothing. Log.w(TAG, "Unrecognized intent action received: " + intent + ", action=" + intent.getAction()); return; } // The time zone update process should run as the system user exclusively as it's a // system feature, not user dependent. UserHandle currentUserHandle = android.os.Process.myUserHandle(); if (!currentUserHandle.isSystem()) { // Just do nothing. Log.w(TAG, "Supposed to be running as the system user," + " instead running as user=" + currentUserHandle); return; } mRulesManager = (RulesManager) context.getSystemService("timezone"); byte[] token = intent.getByteArrayExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN); EventLogTags.writeTimezoneCheckTriggerReceived(Arrays.toString(token)); if (shouldUninstallCurrentInstall(context)) { Log.i(TAG, "Device should be returned to having no time zone distro installed, issuing" + " uninstall request"); // Uninstall is a no-op if nothing is installed. handleUninstall(token); return; } // Note: We rely on the system server to check that the configured data application is the // one that exposes the content provider with the well-known authority, and is a privileged // application as required. It is *not* checked here and it is assumed the updater can trust // the data application. // Obtain the information about what the data app is telling us to do. DistroOperation operation = getOperation(context, token); if (operation == null) { Log.w(TAG, "Unable to read time zone operation. Halting check."); boolean success = true; // No point in retrying. handleCheckComplete(token, success); return; } // Try to do what the data app asked. Log.d(TAG, "Time zone operation: " + operation + " received."); switch (operation.mType) { case TimeZoneRulesDataContract.Operation.TYPE_NO_OP: // No-op. Just acknowledge the check. handleCheckComplete(token, true /* success */); break; case TimeZoneRulesDataContract.Operation.TYPE_UNINSTALL: handleUninstall(token); break; case TimeZoneRulesDataContract.Operation.TYPE_INSTALL: handleCopyAndInstall(context, token, operation.mDistroFormatVersion, operation.mDistroRulesVersion); break; default: Log.w(TAG, "Unknown time zone operation: " + operation + " received. Halting check."); final boolean success = true; // No point in retrying. handleCheckComplete(token, success); } } private boolean shouldUninstallCurrentInstall(Context context) { int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; PackageManager packageManager = context.getPackageManager(); ProviderInfo providerInfo = packageManager.resolveContentProvider(TimeZoneRulesDataContract.AUTHORITY, flags); if (providerInfo == null || providerInfo.applicationInfo == null) { Log.w(TAG, "No package/application info available for content provider " + TimeZoneRulesDataContract.AUTHORITY); // Something has gone wrong. Trying to return the device to clean is a reasonable // response. return true; } // If the data app is the one from /system, we can treat this as "uninstall": if nothing // is installed then the system will treat this as a no-op, and if something is installed // this will stage an uninstall. // We could install the distro from an app contained in the system image but we assume it's // going to contain the same time zone data as the base version and would be a no op. ApplicationInfo applicationInfo = providerInfo.applicationInfo; // isPrivilegedApp() => initial install directory for app /system/priv-app (required) // isUpdatedSystemApp() => app has been replaced by an updated version that resides in /data return applicationInfo.isPrivilegedApp() && !applicationInfo.isUpdatedSystemApp(); } private DistroOperation getOperation(Context context, byte[] tokenBytes) { EventLogTags.writeTimezoneCheckReadFromDataApp(Arrays.toString(tokenBytes)); Cursor c = context.getContentResolver() .query(TimeZoneRulesDataContract.Operation.CONTENT_URI, new String[] { TimeZoneRulesDataContract.Operation.COLUMN_TYPE, TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MAJOR_VERSION, TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MINOR_VERSION, TimeZoneRulesDataContract.Operation.COLUMN_RULES_VERSION, TimeZoneRulesDataContract.Operation.COLUMN_REVISION }, null /* selection */, null /* selectionArgs */, null /* sortOrder */); try (Cursor cursor = c) { if (cursor == null) { Log.e(TAG, "Query returned null"); return null; } if (!cursor.moveToFirst()) { Log.e(TAG, "Query returned empty results"); return null; } try { String type = cursor.getString(0); DistroFormatVersion distroFormatVersion = null; DistroRulesVersion distroRulesVersion = null; if (TimeZoneRulesDataContract.Operation.TYPE_INSTALL.equals(type)) { distroFormatVersion = new DistroFormatVersion(cursor.getInt(1), cursor.getInt(2)); distroRulesVersion = new DistroRulesVersion(cursor.getString(3), cursor.getInt(4)); } return new DistroOperation(type, distroFormatVersion, distroRulesVersion); } catch (Exception e) { Log.e(TAG, "Error looking up distro operation / version", e); return null; } } } private void handleCopyAndInstall(Context context, byte[] checkToken, DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion) { // Decide whether to proceed with the install. RulesState rulesState = mRulesManager.getRulesState(); if (!rulesState.isDistroFormatVersionSupported(distroFormatVersion) || rulesState.isBaseVersionNewerThan(distroRulesVersion)) { Log.d(TAG, "Candidate distro is not supported or is not better than base version."); // Nothing to do. handleCheckComplete(checkToken, true /* success */); return; } ParcelFileDescriptor inputFileDescriptor = getDistroParcelFileDescriptor(context); if (inputFileDescriptor == null) { Log.e(TAG, "No local file created for distro. Halting."); return; } // Copying the ParcelFileDescriptor to a local file proves we can read it before passing it // on to the next stage. It also ensures that we have a hermetic copy of the data we know // the originating content provider cannot modify unexpectedly. If the next stage wants to // "seek" the ParcelFileDescriptor it can do so with fewer processes affected. File file = copyDataToLocalFile(context, inputFileDescriptor); if (file == null) { Log.e(TAG, "Failed to copy distro data to a file."); // It's possible this may get better if the problem is related to storage space so we // signal success := false so it may be retried. boolean success = false; handleCheckComplete(checkToken, success); return; } handleInstall(checkToken, file); } private static ParcelFileDescriptor getDistroParcelFileDescriptor(Context context) { ParcelFileDescriptor inputFileDescriptor; try { inputFileDescriptor = context.getContentResolver().openFileDescriptor( TimeZoneRulesDataContract.Operation.CONTENT_URI, "r"); if (inputFileDescriptor == null) { throw new FileNotFoundException("ContentProvider returned null"); } } catch (FileNotFoundException e) { Log.e(TAG, "Unable to open file descriptor" + TimeZoneRulesDataContract.Operation.CONTENT_URI, e); return null; } return inputFileDescriptor; } private static File copyDataToLocalFile( Context context, ParcelFileDescriptor inputFileDescriptor) { // Adopt the ParcelFileDescriptor into a try-with-resources so we will close it when we're // done regardless of the outcome. try (ParcelFileDescriptor pfd = inputFileDescriptor) { File localFile; try { localFile = File.createTempFile("temp", ".zip", context.getFilesDir()); } catch (IOException e) { Log.e(TAG, "Unable to create local storage file", e); return null; } InputStream fis = new FileInputStream(pfd.getFileDescriptor(), false /* isFdOwner */); try (FileOutputStream fos = new FileOutputStream(localFile, false /* append */)) { Streams.copy(fis, fos); } catch (IOException e) { Log.e(TAG, "Unable to create asset storage file: " + localFile, e); return null; } return localFile; } catch (IOException e) { Log.e(TAG, "Unable to close ParcelFileDescriptor", e); return null; } } private void handleInstall(final byte[] checkToken, final File localFile) { // Create a ParcelFileDescriptor pointing to localFile. final ParcelFileDescriptor distroFileDescriptor; try { distroFileDescriptor = ParcelFileDescriptor.open(localFile, ParcelFileDescriptor.MODE_READ_ONLY); } catch (FileNotFoundException e) { Log.e(TAG, "Unable to create ParcelFileDescriptor from " + localFile); handleCheckComplete(checkToken, false /* success */); return; } finally { // It is safe to delete the File at this point. The ParcelFileDescriptor has an open // file descriptor to it if we are successful, or it is not going to be used if we are // returning early. localFile.delete(); } Callback callback = new Callback() { @Override public void onFinished(int status) { Log.i(TAG, "Finished install: " + status); } }; // Adopt the distroFileDescriptor here so the local file descriptor is closed, whatever the // outcome. try (ParcelFileDescriptor pfd = distroFileDescriptor) { String tokenString = Arrays.toString(checkToken); EventLogTags.writeTimezoneCheckRequestInstall(tokenString); int requestStatus = mRulesManager.requestInstall(pfd, checkToken, callback); Log.i(TAG, "requestInstall() called, token=" + tokenString + ", returned " + requestStatus); } catch (Exception e) { Log.e(TAG, "Error calling requestInstall()", e); } } private void handleUninstall(byte[] checkToken) { Callback callback = new Callback() { @Override public void onFinished(int status) { Log.i(TAG, "Finished uninstall: " + status); } }; try { String tokenString = Arrays.toString(checkToken); EventLogTags.writeTimezoneCheckRequestUninstall(tokenString); int requestStatus = mRulesManager.requestUninstall(checkToken, callback); Log.i(TAG, "requestUninstall() called, token=" + tokenString + ", returned " + requestStatus); } catch (Exception e) { Log.e(TAG, "Error calling requestUninstall()", e); } } private void handleCheckComplete(final byte[] token, final boolean success) { try { String tokenString = Arrays.toString(token); EventLogTags.writeTimezoneCheckRequestNothing(tokenString, success ? 1 : 0); mRulesManager.requestNothing(token, success); Log.i(TAG, "requestNothing() called, token=" + tokenString + ", success=" + success); } catch (Exception e) { Log.e(TAG, "Error calling requestNothing()", e); } } private static class DistroOperation { final String mType; final DistroFormatVersion mDistroFormatVersion; final DistroRulesVersion mDistroRulesVersion; DistroOperation(String type, DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion) { mType = type; mDistroFormatVersion = distroFormatVersion; mDistroRulesVersion = distroRulesVersion; } @Override public String toString() { return "DistroOperation{" + "mType='" + mType + '\'' + ", mDistroFormatVersion=" + mDistroFormatVersion + ", mDistroRulesVersion=" + mDistroRulesVersion + '}'; } } }