/*
* Copyright (c) 2013 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.cloud.backend.android;
import android.app.Activity;
import android.app.Application;
import android.app.Fragment;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.cloud.backend.android.CloudQuery.Order;
import com.google.cloud.backend.android.CloudQuery.Scope;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Cloud Backend API class that provides asynchronous APIs in addition to
* {@link CloudBackend}. The added methods work asynchronously with
* {@link CloudCallbackHandler}, so they can be called from UI thread of
* {@link Activity}s or {@link Fragment}s. This class also implements Continuous
* Query feature based on Google Cloud Messaging.
*
*/
public class CloudBackendAsync extends CloudBackend {
/**
* Map of ContinuousQueryHandlers (key = queryId)
*/
protected static final Map<String, ContinuousQueryHandler> continuousQueries = new HashMap<String, ContinuousQueryHandler>();
/**
* {@link Application} for this backend object, such as {@link Activity}.
*/
protected final Context context;
/**
* Creates an instance of {@link CloudBackendAsync}. Caller need to pass a
* {@link Context} such as {@link Activity} that will be used to Google
* Cloud Messaging and {@link SharedPreferences}.
*
* @param context
* {@link Context} for getting Application for GCM registration
* and SharedPreference. Null can be passed if you don't use
* those features.
*/
public CloudBackendAsync(Context context) {
// set Application
this.context = (context != null ? context.getApplicationContext() : null);
}
/**
* Inserts a CloudEntity into the backend asynchronously.
*
* @param ce
* {@link CloudEntity} for inserting a CloudEntity.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void insert(CloudEntity ce, CloudCallbackHandler<CloudEntity> handler) {
(new BackendCaller<CloudEntity, CloudEntity>(ce, handler) {
@Override
protected CloudEntity callBackend(CloudEntity param) throws IOException {
return CloudBackendAsync.super.insert(param);
}
}).start();
}
/**
* Inserts multiple {@link CloudEntity}s on the backend asynchronously.
* Works just the same as {@link #insert(CloudEntity, CloudCallbackHandler)}
* .
*
* @param ceList
* {@link List} that holds {@link CloudEntity}s to save.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void insertAll(List<CloudEntity> ceList, CloudCallbackHandler<List<CloudEntity>> handler) {
(new BackendCaller<List<CloudEntity>, List<CloudEntity>>(ceList, handler) {
@Override
protected List<CloudEntity> callBackend(List<CloudEntity> ceList) throws IOException {
return CloudBackendAsync.super.insertAll(ceList);
}
}).start();
}
/**
* Updates the specified {@link CloudEntity} on the backend asynchronously.
* If it does not have any Id, it creates a new Entity. If it has, find the
* existing entity and update it.
*
* @param ce
* {@link CloudEntity} for updating a CloudEntity.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void update(CloudEntity ce, CloudCallbackHandler<CloudEntity> handler) {
(new BackendCaller<CloudEntity, CloudEntity>(ce, handler) {
@Override
protected CloudEntity callBackend(CloudEntity param) throws IOException {
return CloudBackendAsync.super.update(param);
}
}).start();
}
/**
* Updates multiple {@link CloudEntity}s on the backend asynchronously.
* Works just the same as
* {@link #updateAll(CloudEntity, CloudCallbackHandler)}.
*
* @param ceList
* {@link List} that holds {@link CloudEntity}s to save.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void updateAll(List<CloudEntity> ceList, CloudCallbackHandler<List<CloudEntity>> handler) {
(new BackendCaller<List<CloudEntity>, List<CloudEntity>>(ceList, handler) {
@Override
protected List<CloudEntity> callBackend(List<CloudEntity> ceList) throws IOException {
return CloudBackendAsync.super.updateAll(ceList);
}
}).start();
}
/**
* Reads the specified {@link CloudEntity} asynchronously.
*
* @param ce
* {@link CloudEntity} that has kindName and id to specify the
* CloudEntity on the backend. Other property values will be
* ignored.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void get(CloudEntity ce, CloudCallbackHandler<CloudEntity> handler) {
(new BackendCaller<CloudEntity, CloudEntity>(ce, handler) {
@Override
protected CloudEntity callBackend(CloudEntity ce) throws IOException {
return CloudBackendAsync.super.get(ce.getKindName(), ce.getId());
}
}).start();
}
/**
* Reads the specified multiple {@link CloudEntity}s asynchronously.
*
* @param ceList
* a List of {@link CloudEntity}s that has kindName and id to
* specify the CloudEntity on the backend. Other property values
* will be ignored.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void getAll(List<CloudEntity> ceList, CloudCallbackHandler<List<CloudEntity>> handler) {
(new BackendCaller<List<CloudEntity>, List<CloudEntity>>(ceList, handler) {
@Override
protected List<CloudEntity> callBackend(List<CloudEntity> ceList) throws IOException {
// if the id list is empty, return it as is.
if (ceList.isEmpty()) {
return ceList;
}
// get a List of IDs
List<String> idList = new LinkedList<String>();
for (CloudEntity ce : ceList) {
idList.add(ce.getId());
}
return CloudBackendAsync.super.getAll(idList.get(0), idList);
}
}).start();
}
/**
* Deletes the specified {@link CloudEntity} asynchronously.
*
* @param ce
* {@link CloudEntity} that has kindName and id to specify the
* CloudEntity on the backend. Other property values will be
* ignored.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void delete(CloudEntity ce, CloudCallbackHandler<Void> handler) {
(new BackendCaller<CloudEntity, Void>(ce, handler) {
@Override
protected Void callBackend(CloudEntity ce) throws IOException {
CloudBackendAsync.super.delete(ce.getKindName(), ce.getId());
return null;
}
}).start();
}
/**
* Deletes the specified multiple {@link CloudEntity}s asynchronously.
*
* @param ceList
* a List of {@link CloudEntity}s that has kindName and id to
* specify the CloudEntity on the backend. Other property values
* will be ignored.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void deleteAll(List<CloudEntity> ceList, CloudCallbackHandler<List<CloudEntity>> handler) {
(new BackendCaller<List<CloudEntity>, List<CloudEntity>>(ceList, handler) {
@Override
protected List<CloudEntity> callBackend(List<CloudEntity> ceList) throws IOException {
// if the id list is empty, return it as is.
if (ceList.isEmpty()) {
return ceList;
}
// get a List of IDs
List<String> idList = new LinkedList<String>();
for (CloudEntity ce : ceList) {
idList.add(ce.getId());
}
CloudBackendAsync.super.deleteAllById(idList.get(0), idList);
return null;
}
}).start();
}
/**
* Executes a query with specified {@link CloudQuery}.
*
* @param query
* {@link CloudQuery} to execute.
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void list(CloudQuery query, CloudCallbackHandler<List<CloudEntity>> handler) {
// register the query as continuous query
if (query.isContinuous()) {
CloudQuery ncq = new CloudQuery(query);
ncq.setScope(Scope.PAST);
ContinuousQueryHandler cqh = new ContinuousQueryHandler(handler, ncq, getCredential());
continuousQueries.put(query.getQueryId(), cqh);
}
// execute the query
_list(query, handler, new Handler());
}
private void _list(CloudQuery query, CloudCallbackHandler<List<CloudEntity>> handler, Handler uiThreadHandler) {
(new BackendCaller<CloudQuery, List<CloudEntity>>(query, handler, uiThreadHandler) {
@Override
protected List<CloudEntity> callBackend(CloudQuery query) throws IOException {
// set regId (this may blocks until registration finishes)
if (context != null) {
query.setRegId(GCMReceiver.getRegistrationId(context));
}
// execute query
return CloudBackendAsync.super.list(query);
}
}).start();
}
/**
* Handles notification from Google Cloud Messaging service and invokes a
* query specified by the queryId.
*
* @param queryId
*/
public static void handleQueryMessage(String queryId) {
// retrieve query and handler name for the notification
ContinuousQueryHandler cqh = continuousQueries.get(queryId);
if (cqh == null) {
Log.i(Consts.TAG, "handleQueryMessage: Query not found for ID: " + queryId);
return;
}
// execute the query
CloudBackendAsync cba = new CloudBackendAsync(null);
cba.setCredential(cqh.getCredential());
cba._list(cqh.getQuery(), cqh.getHandler(), cqh.getUiThreadHandler());
}
/**
* Executes a {@link CloudQuery} with specified single property condition.
*
* @param kindName
* a name of Kind to query
* @param propertyName
* property name for filtering
* @param operator
* operator that will be applied to the filtering
* @param propertyValue
* value that will be used in the filtering
* @param order
* {@link Order} of sorting on the specified property (ignored
* when inequality filter is not used as the operator)
* @param limit
* number of maximum entities to be returned
* @param scope
* {@link Scope} of this query
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void listByProperty(String kindName, String propertyName, F.Op operator, Object propertyValue,
CloudQuery.Order order, int limit, Scope scope, CloudCallbackHandler<List<CloudEntity>> handler) {
CloudQuery cq = new CloudQuery(kindName);
cq.setFilter(F.createFilter(operator.name(), propertyName, propertyValue));
cq.setSort(propertyName, order);
cq.setLimit(limit);
cq.setScope(scope);
this.list(cq, handler);
}
/**
* Executes a {@link CloudQuery} that retrieves all entities in the
* specified kind.
*
* @param kindName
* a name of Kind to query
* @param sortPropertyName
* property name for sorting
* @param order
* {@link Order} of sorting on the specified property
* @param limit
* number of maximum entities to be returned
* @param scope
* {@link Scope} of this query
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void listByKind(String kindName, String sortPropertyName, CloudQuery.Order order, int limit, Scope scope,
CloudCallbackHandler<List<CloudEntity>> handler) {
CloudQuery cq = new CloudQuery(kindName);
cq.setSort(sortPropertyName, order);
cq.setLimit(limit);
cq.setScope(scope);
this.list(cq, handler);
}
/**
* Removes a continuous query by specifying a queryId. QueryId can be
* retrived from {@link CloudQuery#getQueryId()}.
*
* @param handler
* {@link CloudCallbackHandler} to remove
*/
public void unsubscribeFromQuery(String queryId) {
continuousQueries.remove(queryId);
}
/**
* Clears all continuous queries.
*/
public void clearAllSubscription() {
continuousQueries.clear();
}
/**
* Executes a {@link CloudQuery} that retrieves the last one entity in the
* specified kind.
*
* @param kindName
* a name of Kind to query
* @param scope
* {@link Scope} of this query
* @param handler
* {@link CloudCallbackHandler} that handles the response.
*/
public void getLastEntityOfKind(String kindName, Scope scope, CloudCallbackHandler<List<CloudEntity>> handler) {
this.listByKind(kindName, CloudEntity.PROP_CREATED_AT, Order.DESC, 1, scope, handler);
}
// a Thread class that will call backend API asynchronously
// and call back the handler on UI thread
private abstract class BackendCaller<PARAM, RESULT> extends Thread {
final Handler uiThreadHandler;
final CloudCallbackHandler<RESULT> handler;
final PARAM param;
private BackendCaller(PARAM param, CloudCallbackHandler<RESULT> crh, Handler uiThreadHandler) {
this.handler = crh;
this.param = param;
this.uiThreadHandler = uiThreadHandler;
}
private BackendCaller(PARAM param, CloudCallbackHandler<RESULT> crh) {
this.handler = crh;
this.param = param;
this.uiThreadHandler = new Handler();
}
@Override
public void run() {
// execute call
RESULT r = null;
IOException ie = null;
try {
r = callBackend(param);
} catch (IOException e) {
Log.i(Consts.TAG, "error: ", e);
ie = e;
}
final RESULT results = r;
final IOException exception = ie;
// if no handler specified, no need to callback
if (handler == null) {
return;
}
// pass the result to the handler on UI thread
uiThreadHandler.post(new Runnable() {
@Override
public void run() {
if (exception == null) {
handler.onComplete(results);
} else {
handler.onError(exception);
}
}
});
}
abstract protected RESULT callBackend(PARAM param) throws IOException;
};
/**
* A class that holds required objects for continuous query callback
*/
protected class ContinuousQueryHandler {
private final CloudCallbackHandler<List<CloudEntity>> handler;
private final Handler uiThreadHandler;
private final CloudQuery query;
private final GoogleAccountCredential credential;
public ContinuousQueryHandler(final CloudCallbackHandler<List<CloudEntity>> handler, final CloudQuery query,
final GoogleAccountCredential credential) {
this.handler = handler;
this.query = query;
this.credential = credential;
uiThreadHandler = new Handler();
}
public CloudCallbackHandler<List<CloudEntity>> getHandler() {
return handler;
}
public Handler getUiThreadHandler() {
return uiThreadHandler;
}
public CloudQuery getQuery() {
return query;
}
public GoogleAccountCredential getCredential() {
return credential;
}
}
}