381 lines
18 KiB
Java
381 lines
18 KiB
Java
|
/*
|
||
|
* 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 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 +
|
||
|
'}';
|
||
|
}
|
||
|
}
|
||
|
}
|