blob: 9f3a3b8232241a6ed9953465d2a7f2f6e81527d5 [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 libcore.tzdata.prototype_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.database.Cursor;
import android.os.ParcelFileDescriptor;
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;
// TODO(nfuller): Prevent multiple broadcasts being handled at once?
// TODO(nfuller): Improve logging
// TODO(nfuller): Make the rules check async?
// TODO(nfuller): Need async generally for SystemService calls from BroadcastReceiver?
public class RulesCheckReceiver extends BroadcastReceiver {
final static String TAG = "RulesCheckReceiver";
private RulesManager mRulesManager;
@Override
public void onReceive(Context context, Intent intent) {
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;
}
mRulesManager = (RulesManager) context.getSystemService("timezone");
byte[] token = intent.getByteArrayExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN);
// 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.
String operation = getOperation(context);
if (operation == null) {
// TODO Log
boolean success = true; // No point in retrying.
handleCheckComplete(token, success);
return;
}
switch (operation) {
case TimeZoneRulesDataContract.OPERATION_NO_OP:
// TODO Log
// No-op. Just acknowledge the check.
handleCheckComplete(token, true /* success */);
break;
case TimeZoneRulesDataContract.OPERATION_UNINSTALL:
// TODO Log
handleUninstall(token);
break;
case TimeZoneRulesDataContract.OPERATION_INSTALL:
// TODO Log
DistroVersionInfo distroVersionInfo = getDistroVersionInfo(context);
handleCopyAndInstall(context, token, distroVersionInfo);
break;
default:
// TODO Log
final boolean success = true; // No point in retrying.
handleCheckComplete(token, success);
}
}
private String getOperation(Context context) {
Cursor cursor = context.getContentResolver()
.query(TimeZoneRulesDataContract.OPERATION_URI,
new String[] { TimeZoneRulesDataContract.COLUMN_OPERATION },
null /* selection */, null /* selectionArgs */, null /* sortOrder */);
if (cursor == null) {
Log.e(TAG, "getOperation: query returned null");
return null;
}
if (!cursor.moveToFirst()) {
Log.e(TAG, "getOperation: query returned empty results");
return null;
}
try {
return cursor.getString(0);
} catch (Exception e) {
Log.e(TAG, "getOperation: getString() threw an exception", e);
return null;
}
}
private DistroVersionInfo getDistroVersionInfo(Context context) {
Cursor cursor = context.getContentResolver()
.query(TimeZoneRulesDataContract.OPERATION_URI,
new String[] {
TimeZoneRulesDataContract.COLUMN_DISTRO_MAJOR_VERSION,
TimeZoneRulesDataContract.COLUMN_DISTRO_MINOR_VERSION,
TimeZoneRulesDataContract.COLUMN_RULES_VERSION,
TimeZoneRulesDataContract.COLUMN_REVISION},
null /* selection */, null /* selectionArgs */, null /* sortOrder */);
if (cursor == null) {
Log.e(TAG, "getDistroVersionInfo: query returned null");
return null;
}
if (!cursor.moveToFirst()) {
Log.e(TAG, "getDistroVersionInfo: query returned empty results");
return null;
}
try {
return new DistroVersionInfo(
cursor.getInt(0),
cursor.getInt(1),
cursor.getString(2),
cursor.getInt(3));
} catch (Exception e) {
Log.e(TAG, "getDistroVersionInfo: getInt()/getString() threw an exception", e);
return null;
}
}
private void handleCopyAndInstall(Context context, byte[] checkToken,
DistroVersionInfo distroVersionInfo) {
// Decide whether to proceed with the install.
RulesState rulesState = mRulesManager.getRulesState();
if (!(rulesState.isDistroFormatVersionSupported(distroVersionInfo.mDistroFormatVersion)
&& rulesState.isSystemVersionOlderThan(distroVersionInfo.mDistroRulesVersion))) {
// Nothing to do.
handleCheckComplete(checkToken, true /* success */);
return;
}
// Copy the data locally before passing it on....security and whatnot.
// TODO(nfuller): Need to do the copy here?
File file = copyDataToLocalFile(context);
if (file == null) {
// It's possible this may get better if the problem is related to storage space.
boolean success = false;
handleCheckComplete(checkToken, success);
return;
}
handleInstall(checkToken, file);
}
private static File copyDataToLocalFile(Context context) {
File extractedFile = new File(context.getFilesDir(), "temp.zip");
ParcelFileDescriptor fileDescriptor;
try {
fileDescriptor = context.getContentResolver().openFileDescriptor(
TimeZoneRulesDataContract.DATA_URI, "r");
if (fileDescriptor == null) {
throw new FileNotFoundException("ContentProvider returned null");
}
} catch (FileNotFoundException e) {
Log.e(TAG, "copyDataToLocalFile: Unable to open file descriptor"
+ TimeZoneRulesDataContract.DATA_URI, e);
return null;
}
try (ParcelFileDescriptor pfd = fileDescriptor;
InputStream fis = new FileInputStream(pfd.getFileDescriptor());
FileOutputStream fos = new FileOutputStream(extractedFile, false /* append */)) {
Streams.copy(fis, fos);
} catch (IOException e) {
Log.e(TAG, "Unable to create asset storage file: " + extractedFile, e);
return null;
}
return extractedFile;
}
private void handleInstall(final byte[] checkToken, final File contentFile) {
// Convert the distroFile to a ParcelFileDescriptor.
final ParcelFileDescriptor distroFileDescriptor;
try {
distroFileDescriptor =
ParcelFileDescriptor.open(contentFile, ParcelFileDescriptor.MODE_READ_ONLY);
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to create ParcelFileDescriptor from " + contentFile);
handleCheckComplete(checkToken, false /* success */);
return;
}
Callback callback = new Callback() {
@Override
public void onFinished(int status) {
Log.i(TAG, "onFinished: Finished install: " + status);
// TODO(nfuller): Can this be closed sooner?
try {
distroFileDescriptor.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close ParcelFileDescriptor for " + contentFile, e);
} finally {
// Delete the file we no longer need.
contentFile.delete();
}
}
};
try {
int requestStatus =
mRulesManager.requestInstall(distroFileDescriptor, checkToken, callback);
Log.i(TAG, "handleInstall: Request sent:" + requestStatus);
} catch (Exception e) {
Log.e(TAG, "handleInstall: Error", e);
}
}
private void handleUninstall(byte[] checkToken) {
Callback callback = new Callback() {
@Override
public void onFinished(int status) {
Log.i(TAG, "onFinished: Finished uninstall: " + status);
}
};
try {
int requestStatus =
mRulesManager.requestUninstall(checkToken, callback);
Log.i(TAG, "handleUninstall: Request sent" + requestStatus);
} catch (Exception e) {
Log.e(TAG, "handleUninstall: Error", e);
}
}
private void handleCheckComplete(final byte[] token, final boolean success) {
try {
mRulesManager.requestNothing(token, success);
Log.i(TAG, "doInBackground: Called checkComplete: token="
+ Arrays.toString(token) + ", success=" + success);
} catch (Exception e) {
Log.e(TAG, "doInBackground: Error calling checkComplete()", e);
}
}
private static class DistroVersionInfo {
final DistroFormatVersion mDistroFormatVersion;
final DistroRulesVersion mDistroRulesVersion;
DistroVersionInfo(int distroMajorVersion, int distroMinorVersion,
String rulesVersion, int revision) {
mDistroFormatVersion = new DistroFormatVersion(distroMajorVersion, distroMinorVersion);
mDistroRulesVersion = new DistroRulesVersion(rulesVersion, revision);
}
}
}