/*
* 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.android;
import com.google.common.base.Preconditions;
import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.SystemResources.Storage;
import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
import com.google.ipc.invalidation.external.client.types.Callback;
import com.google.ipc.invalidation.external.client.types.SimplePair;
import com.google.ipc.invalidation.external.client.types.Status;
import com.google.ipc.invalidation.util.NamedRunnable;
import com.google.protobuf.ByteString;
import com.google.protos.ipc.invalidation.AndroidState.ClientMetadata;
import com.google.protos.ipc.invalidation.AndroidState.ClientProperty;
import com.google.protos.ipc.invalidation.AndroidState.StoredState;
import com.google.protos.ipc.invalidation.ClientProtocol.Version;
import android.accounts.Account;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
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.io.OutputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
/**
* Provides the storage and in-memory state model for Android client persistent state. There is
* one storage instance for each client instance that is responsible for loading, making state
* available, and storing the persisted state.
* <b>
* The class is thread safe <b>after</b> the {@link #create} or {@link #load} method has been
* called to populate it with initial state.
*
*/
public class AndroidStorage implements Storage {
/*
* The current storage format is based upon a single file containing protocol buffer data. Each
* client instance will have a separate state file with a name based upon a client-key derived
* convention. The design could easily be evolved later to leverage a shared SQLite database
* or other mechanisms without requiring any changes to the public interface.
*/
/** Storage logger */
private static final Logger logger = AndroidLogger.forTag("InvStorage");
/** The version value that is stored within written state */
private static final Version CURRENT_VERSION =
Version.newBuilder().setMajorVersion(1).setMinorVersion(0).build();
/** The name of the subdirectory in the application files store where state files are stored */
static final String STATE_DIRECTORY = "InvalidationClient";
/** A simple success constant */
private static final Status SUCCESS = Status.newInstance(Status.Code.SUCCESS, "");
/**
* Deletes all persisted client state files stored in the state directory and then
* the directory itself.
*/
public static void reset(Context context) {
File stateDir = context.getDir(STATE_DIRECTORY, Context.MODE_PRIVATE);
for (File stateFile : stateDir.listFiles()) {
stateFile.delete();
}
stateDir.delete();
}
/** The execution context */
final Context context;
/** The client key associated with this storage instance */
final String key;
/** the client metadata associated with the storage instance (or {@code null} if not loaded */
private ClientMetadata metadata;
/** Stores the client properties for a client */
private final Map<String, byte []> properties = new ConcurrentHashMap<String, byte[]>();
/** Executor used to schedule background reads and writes on a single shared thread */
final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/**
* Creates a new storage object for reading or writing state for the providing client key using
* the provided execution context.
*/
protected AndroidStorage(Context context, String key) {
Preconditions.checkNotNull(context, "context");
Preconditions.checkNotNull(key, "key");
this.key = key;
this.context = context;
}
ClientMetadata getClientMetadata() {
return metadata;
}
@Override
public void deleteKey(final String key, final Callback<Boolean> done) {
scheduler.execute(new NamedRunnable("AndroidStorage.deleteKey") {
@Override
public void run() {
properties.remove(key);
store();
done.accept(true);
}
});
}
@Override
public void readAllKeys(final Callback<SimplePair<Status, String>> keyCallback) {
scheduler.execute(new NamedRunnable("AndroidStorage.readAllKeys") {
@Override
public void run() {
for (String key : properties.keySet()) {
keyCallback.accept(SimplePair.of(SUCCESS, key));
}
}
});
}
@Override
public void readKey(final String key, final Callback<SimplePair<Status, byte[]>> done) {
scheduler.execute(new NamedRunnable("AndroidStorage.readKey") {
@Override
public void run() {
byte [] value = properties.get(key);
if (value != null) {
done.accept(SimplePair.of(SUCCESS, value));
} else {
Status status =
Status.newInstance(Status.Code.PERMANENT_FAILURE, "No value in map for " + key);
done.accept(SimplePair.of(status, (byte []) null));
}
}
});
}
@Override
public void writeKey(final String key, final byte[] value, final Callback<Status> done) {
scheduler.execute(new NamedRunnable("AndroidStorage.writeKey") {
@Override
public void run() {
properties.put(key, value);
store();
done.accept(SUCCESS);
}
});
}
@Override
public void setSystemResources(SystemResources resources) {}
/**
* Returns the file where client state for this storage instance is stored.
*/
File getStateFile() {
File stateDir = context.getDir(STATE_DIRECTORY, Context.MODE_PRIVATE);
return new File(stateDir, key);
}
/**
* Returns the input stream that can be used to read state from the internal file storage for
* the application.
*/
protected InputStream getStateInputStream() throws FileNotFoundException {
return new FileInputStream(getStateFile());
}
/**
* Returns the output stream that can be used to write state to the internal file storage for
* the application.
*/
protected OutputStream getStateOutputStream() throws FileNotFoundException {
return new FileOutputStream(getStateFile());
}
void create(int clientType, Account account, String authType,
Intent eventIntent) {
ComponentName component = eventIntent.getComponent();
Preconditions.checkNotNull(component, "No component found in event intent");
metadata = ClientMetadata.newBuilder()
.setVersion(CURRENT_VERSION)
.setClientKey(key)
.setClientType(clientType)
.setAccountName(account.name)
.setAccountType(account.type)
.setAuthType(authType)
.setListenerPkg(component.getPackageName())
.setListenerClass(component.getClassName())
.build();
store();
}
/**
* Attempts to load any persisted client state for the stored client.
*
* @returns {@code true} if loaded successfully, false otherwise.
*/
boolean load() {
InputStream inputStream = null;
try {
// Load the state from internal storage and parse it the protocol
inputStream = getStateInputStream();
StoredState fullState = StoredState.parseFrom(inputStream);
metadata = fullState.getMetadata();
if (!key.equals(metadata.getClientKey())) {
logger.severe("Unexpected client key mismatch: %s, %s", key, metadata.getClientKey());
return false;
}
logger.fine("Loaded metadata: %s", metadata);
// Unpack the client properties into a map for easy lookup / iteration / update
for (ClientProperty clientProperty : fullState.getPropertyList()) {
logger.fine("Loaded property: %s", clientProperty);
properties.put(clientProperty.getKey(), clientProperty.getValue().toByteArray());
}
logger.fine("Loaded state for %s", key);
return true;
} catch (FileNotFoundException e) {
// No state persisted on disk
} catch (IOException exception) {
// Log error regarding client state read and return null
logger.severe("Error reading client state", exception);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException exception) {
logger.severe("Unable to close state file", exception);
}
}
}
return false;
}
/**
* Deletes all state associated with the storage instance.
*/
void delete() {
File stateFile = getStateFile();
if (stateFile.exists()) {
stateFile.delete();
logger.info("Deleted state for %s from %s", key, stateFile.getName());
}
}
/**
* Store the current state into the persistent storage.
*/
private void store() {
StoredState.Builder stateBuilder =
StoredState.newBuilder()
.mergeMetadata(metadata);
for (Map.Entry<String, byte []> entry : properties.entrySet()) {
stateBuilder.addProperty(
ClientProperty.newBuilder()
.setKey(entry.getKey())
.setValue(ByteString.copyFrom(entry.getValue()))
.build());
}
StoredState state = stateBuilder.build();
OutputStream outputStream = null;
try {
outputStream = getStateOutputStream();
state.writeTo(outputStream);
logger.info("State written for %s", key);
} catch (FileNotFoundException exception) {
// This should not happen when opening to create / replace
logger.severe("Unable to open state file", exception);
} catch (IOException exception) {
logger.severe("Error writing state", exception);
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException exception) {
logger.warning("Unable to close state file", exception);
}
}
}
}
/**
* Returns the underlying properties map for direct manipulation. This is extremely
* unsafe since it bypasses the concurrency control. It is intended only for use
* in {@code AndroidInvalidationService#handleC2dmMessageForUnstartedClient}.
*/
Map<String, byte[]> getPropertiesUnsafe() {
return properties;
}
/**
* Stores the properties to disk. This is extremely unsafe since it bypasses the
* concurrency control. It is intended only for use in
* {@code AndroidInvalidationService#handleC2dmMessageForUnstartedClient}.
*/
void storeUnsafe() {
store();
}
}