blob: 003b2e423e4b7853d6119932f5e12a50b9d44fb2 [file] [log] [blame]
/*
* 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.
*
* <p>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.
*
* <p>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}.
*
* <p>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 in /system 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.isSystemVersionNewerThan(distroRulesVersion)) {
Log.d(TAG, "Candidate distro is not supported or is not better than system 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 +
'}';
}
}
}