/*
* 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.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.android.service.AndroidClientException;
import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
import com.google.ipc.invalidation.external.client.android.service.Response.Status;
import com.google.ipc.invalidation.ticl.InvalidationClientCore;
import com.google.ipc.invalidation.util.TypedUtil;
import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import java.util.HashMap;
import java.util.Map;
/**
* Manages active client instances for the Android invalidation service. The client manager contains
* the code to create, persist, load, and lookup client instances, as well as handling the
* propagation of any C2DM registration notifications to active clients.
*
*/
public class AndroidClientManager {
/** Logger */
private static final Logger logger = AndroidLogger.forTag("InvClientManager");
/**
* The client configuration used creating new invalidation client instances. This is normally
* a constant but may be varied for testing.
*/
private static ClientConfigP clientConfig = InvalidationClientCore.createConfig().build();
/** The invalidation service associated with this manager */
private final AndroidInvalidationService service;
/**
* When set, this registration ID is used rather than the ID returned by
* {@code GCMRegistrar.getRegistrationId()}.
*/
private static String registrationIdForTest;
/** A map from client key to client proxy instances for in-memory client instances */
private final Map<String, AndroidClientProxy> clientMap =
new HashMap<String, AndroidClientProxy>();
/** All client manager operations are synchronized on this lock */
private final Object lock = new Object();
/** Creates a new client manager instance associated with the provided service */
AndroidClientManager(AndroidInvalidationService service) {
this.service = service;
}
/**
* Returns the number of managed clients.
*/
int getClientCount() {
synchronized (lock) {
return clientMap.size();
}
}
/**
* Creates a new Android client proxy with the provided attributes. Before creating, will check to
* see if there is an existing client with attributes that match and return it if found. If there
* is an existing client with the same key but attributes that do not match, an exception will be
* thrown. If no client with a matching key exists, a new client proxy will be created and
* returned.
*
* @param clientKey key that uniquely identifies the client on the device.
* @param clientType client type.
* @param account user account associated with the client.
* @param authType authentication type for the client.
* @param eventIntent intent that can be used to bind to an event listener for the client.
* @return an android invalidation client instance representing the client.
*/
AndroidClientProxy create(String clientKey, int clientType, Account account, String authType,
Intent eventIntent) {
synchronized (lock) {
// First check to see if an existing client is found
AndroidClientProxy proxy = lookup(clientKey);
if (proxy != null) {
if (!proxy.getAccount().equals(account) || !proxy.getAuthType().equals(authType)) {
throw new AndroidClientException(
Status.INVALID_CLIENT, "Account does not match existing client");
}
return proxy;
}
// If not found, create a new client proxy instance to represent the client.
AndroidStorage store = createAndroidStorage(service, clientKey);
store.create(clientType, account, authType, eventIntent);
proxy = new AndroidClientProxy(service, store, clientConfig);
if (registrationIdForTest != null) {
proxy.getChannel().setRegistrationIdForTest(registrationIdForTest);
}
clientMap.put(clientKey, proxy);
logger.fine("Client %s created", clientKey);
return proxy;
}
}
/**
* Retrieves an existing client that matches the provided key, loading it if necessary. If no
* matching client can be found, an exception is thrown.
*
* @param clientKey the client key for the client to retrieve.
* @return the matching client instance
*/
AndroidClientProxy get(String clientKey) {
synchronized (lock) {
return lookup(clientKey);
}
}
/**
* Removes any client proxy instance associated with the provided key from memory but leaves the
* instance persisted. The client may subsequently be loaded again by calling {@code #get}.
*
* @param clientKey the client key of the instance to remove from memory.
*/
void remove(String clientKey) {
synchronized (lock) {
// Remove the proxy from the managed set and release any associated resources
AndroidClientProxy proxy = clientMap.remove(clientKey);
if (proxy != null) {
proxy.release();
}
}
}
/**
* Looks up the client proxy instance associated with the provided key and returns it (or {@code
* null} if not found).
*
* @param clientKey the client key to look up
* @return the client instance or {@code null}.
*/
AndroidClientProxy lookup(String clientKey) {
synchronized (lock) {
// See if the client is already resident in memory
AndroidClientProxy client = clientMap.get(clientKey);
if (client == null) {
// Attempt to load the client from the store
AndroidStorage storage = createAndroidStorage(service, clientKey);
if (storage.load()) {
logger.fine("Client %s loaded from disk", clientKey);
client = new AndroidClientProxy(service, storage, clientConfig);
clientMap.put(clientKey, client);
}
}
return client;
}
}
/**
* Sets the GCM registration ID that should be used for all managed clients (new and existing).
*/
void informRegistrationIdChanged() {
synchronized (lock) {
// Propagate the value to all existing clients
for (AndroidClientProxy proxy : clientMap.values()) {
proxy.getChannel().informRegistrationIdChanged();
}
}
}
/**
* Releases all managed clients and drops them from the managed set.
*/
void releaseAll() {
synchronized (lock) {
for (AndroidClientProxy clientProxy : clientMap.values()) {
clientProxy.release();
}
clientMap.clear();
}
}
/**
* Returns an android storage instance for managing client state.
*/
protected AndroidStorage createAndroidStorage(Context context, String clientKey) {
synchronized (lock) {
return new AndroidStorage(context, clientKey);
}
}
static ClientConfigP setConfigForTest(ClientConfigP newConfig) {
logger.info("Setting client configuration: %s", newConfig);
ClientConfigP currentConfig = clientConfig;
clientConfig = newConfig;
return clientConfig;
}
public static void setRegistrationIdForTest(String registrationIdForTest) {
AndroidClientManager.registrationIdForTest = registrationIdForTest;
}
/** Returns whether all loaded clients are stopped. */
public boolean areAllClientsStopped() {
synchronized (lock) {
for (AndroidClientProxy proxy : clientMap.values()) {
if (proxy.isStarted()) {
return false;
}
}
return true;
}
}
/** Returns whether the client with key {@code clientKey} is in memory. */
public boolean isLoadedForTest(String clientKey) {
synchronized (lock) {
return TypedUtil.containsKey(clientMap, clientKey);
}
}
}