/*
* Copyright 2011 Google Inc.
*
* 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.google.ipc.invalidation.ticl.android2;
import com.google.common.base.Preconditions;
import com.google.ipc.invalidation.common.ObjectIdDigestUtils.Sha1DigestFunction;
import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protos.ipc.invalidation.AndroidService.AndroidTiclState;
import com.google.protos.ipc.invalidation.AndroidService.AndroidTiclState.Metadata;
import com.google.protos.ipc.invalidation.AndroidService.AndroidTiclStateWithDigest;
import com.google.protos.ipc.invalidation.ClientProtocol.ApplicationClientIdP;
import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP;
import com.google.protos.ipc.invalidation.JavaClient.InvalidationClientState;
import android.content.Context;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
/**
* Class to save and restore instances of {@code InvalidationClient} to and from stable storage.
*
*/
public class TiclStateManager {
/** Name of the file to which Ticl state will be persisted. */
private static final String TICL_STATE_FILENAME = "android_ticl_service_state.bin";
/**
* Maximum size of a Ticl state file. Files with larger size will be ignored as invalid. We use
* this because we allocate an array of bytes of the same size as the Ticl file and want to
* avoid accidentally allocating huge arrays.
*/
private static final int MAX_TICL_FILE_SIZE_BYTES = 100 * 1024; // 100 kilobytes
/** Random number generator for created Ticls. */
private static final Random random = new Random();
/**
* Restores the Ticl from persistent storage if it exists. Otherwise, returns {@code null}.
* @param context Android system context
* @param resources resources to use for the Ticl
*/
static AndroidInvalidationClientImpl restoreTicl(Context context,
SystemResources resources) {
AndroidTiclState state = readTiclState(context, resources.getLogger());
if (state == null) {
return null;
}
AndroidInvalidationClientImpl ticl = new AndroidInvalidationClientImpl(context, resources,
random, state);
setSchedulerId(resources, ticl);
return ticl;
}
/** Creates a new Ticl. Persistent stroage must not exist. */
static void createTicl(Context context, SystemResources resources, int clientType,
byte[] clientName, ClientConfigP config, boolean skipStartForTest) {
Preconditions.checkState(!doesStateFileExist(context), "Ticl already exists");
AndroidInvalidationClientImpl ticl = new AndroidInvalidationClientImpl(context, resources,
random, clientType, clientName, config);
if (!skipStartForTest) {
// Ticls are started when created unless this should be skipped for tests; we allow tests
// to skip starting Ticls because many integration tests assume that Ticls will not be
// started when created.
setSchedulerId(resources, ticl);
ticl.start();
}
saveTicl(context, resources.getLogger(), ticl);
}
/**
* Sets the scheduling id on the scheduler in {@code resources} to {@code ticl.getSchedulingId()}.
*/
private static void setSchedulerId(SystemResources resources,
AndroidInvalidationClientImpl ticl) {
AndroidInternalScheduler scheduler =
(AndroidInternalScheduler) resources.getInternalScheduler();
scheduler.setTiclId(ticl.getSchedulingId());
}
/**
* Saves a Ticl instance to persistent storage.
*
* @param context Android system context
* @param logger logger
* @param ticl the Ticl instance to save
*/
static void saveTicl(Context context, Logger logger, AndroidInvalidationClientImpl ticl) {
FileOutputStream outputStream = null;
try {
// Create a protobuf with the Ticl state and a digest over it.
AndroidTiclStateWithDigest digestedState = createDigestedState(ticl);
AndroidIntentProtocolValidator validator = new AndroidIntentProtocolValidator(logger);
Preconditions.checkState(validator.isTiclStateValid(digestedState),
"Produced invalid digested state: %s", digestedState);
// Write the protobuf to storage.
outputStream = openStateFileForWriting(context);
outputStream.write(digestedState.toByteArray());
outputStream.close();
} catch (FileNotFoundException exception) {
logger.warning("Could not write Ticl state: %s", exception);
} catch (IOException exception) {
logger.warning("Could not write Ticl state: %s", exception);
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException exception) {
logger.warning("Exception closing Ticl state file: %s", exception);
}
}
}
/**
* Reads and returns the Android Ticl state from persistent storage. If the state was missing
* or invalid, returns {@code null}.
*/
static AndroidTiclState readTiclState(Context context, Logger logger) {
FileInputStream inputStream = null;
try {
inputStream = openStateFileForReading(context);
DataInput input = new DataInputStream(inputStream);
long fileSizeBytes = inputStream.getChannel().size();
if (fileSizeBytes > MAX_TICL_FILE_SIZE_BYTES) {
logger.warning("Ignoring too-large Ticl state file with size %s > %s",
fileSizeBytes, MAX_TICL_FILE_SIZE_BYTES);
} else {
// Cast to int must be safe due to the above size check.
byte[] fileData = new byte[(int) fileSizeBytes];
input.readFully(fileData);
AndroidTiclStateWithDigest androidState = AndroidTiclStateWithDigest.parseFrom(fileData);
AndroidIntentProtocolValidator validator = new AndroidIntentProtocolValidator(logger);
// Check the structure of the message (required fields set).
if (!validator.isTiclStateValid(androidState)) {
logger.warning("Read AndroidTiclStateWithDigest with invalid structure: %s",
androidState);
return null;
}
// Validate the digest in the method.
if (isDigestValid(androidState, logger)) {
InvalidationClientState state = androidState.getState().getTiclState();
return androidState.getState();
} else {
logger.warning("Android Ticl state failed digest check: %s", androidState);
}
}
} catch (FileNotFoundException exception) {
logger.info("Ticl state file does not exist: %s", TICL_STATE_FILENAME);
} catch (InvalidProtocolBufferException exception) {
logger.warning("Could not read Ticl state: %s", exception);
} catch (IOException exception) {
logger.warning("Could not read Ticl state: %s", exception);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException exception) {
logger.warning("Exception closing Ticl state file: %s", exception);
}
}
return null;
}
/**
* Returns a {@link AndroidTiclStateWithDigest} containing {@code ticlState} and its computed
* digest.
*/
private static AndroidTiclStateWithDigest createDigestedState(
AndroidInvalidationClientImpl ticl) {
Sha1DigestFunction digester = new Sha1DigestFunction();
ApplicationClientIdP ticlAppId = ticl.getApplicationClientIdP();
Metadata metaData = Metadata.newBuilder()
.setClientConfig(ticl.getConfig())
.setClientName(ticlAppId.getClientName())
.setClientType(ticlAppId.getClientType())
.setTiclId(ticl.getSchedulingId())
.build();
AndroidTiclState state = AndroidTiclState.newBuilder()
.setMetadata(metaData)
.setTiclState(ticl.marshal())
.setVersion(ProtocolIntents.ANDROID_PROTOCOL_VERSION_VALUE).build();
digester.update(state.toByteArray());
AndroidTiclStateWithDigest verifiedState = AndroidTiclStateWithDigest.newBuilder()
.setState(state)
.setDigest(ByteString.copyFrom(digester.getDigest()))
.build();
return verifiedState;
}
/** Returns whether the digest in {@code state} is correct. */
private static boolean isDigestValid(AndroidTiclStateWithDigest state, Logger logger) {
Sha1DigestFunction digester = new Sha1DigestFunction();
digester.update(state.getState().toByteArray());
ByteString computedDigest = ByteString.copyFrom(digester.getDigest());
if (!computedDigest.equals(state.getDigest())) {
logger.warning("Android Ticl state digest mismatch; computed %s for %s",
computedDigest, state);
return false;
}
return true;
}
/** Opens {@link #TICL_STATE_FILENAME} for writing. */
private static FileOutputStream openStateFileForWriting(Context context)
throws FileNotFoundException {
return context.openFileOutput(TICL_STATE_FILENAME, Context.MODE_PRIVATE);
}
/** Opens {@link #TICL_STATE_FILENAME} for reading. */
private static FileInputStream openStateFileForReading(Context context)
throws FileNotFoundException {
return context.openFileInput(TICL_STATE_FILENAME);
}
/** Deletes {@link #TICL_STATE_FILENAME}. */
public static void deleteStateFile(Context context) {
context.deleteFile(TICL_STATE_FILENAME);
}
/** Returns whether the state file exists, for tests. */
static boolean doesStateFileExistForTest(Context context) {
return doesStateFileExist(context);
}
/** Returns whether the state file exists. */
private static boolean doesStateFileExist(Context context) {
return context.getFileStreamPath(TICL_STATE_FILENAME).exists();
}
private TiclStateManager() {
// Disallow instantiation.
}
}