package com.tomclaw.mandarin.core; import android.app.Service; import android.content.ContentResolver; import android.content.ContentValues; import android.database.ContentObserver; import android.database.Cursor; import android.text.TextUtils; import com.tomclaw.mandarin.core.exceptions.AccountNotFoundException; import com.tomclaw.mandarin.im.AccountRoot; import com.tomclaw.mandarin.util.GsonSingleton; import com.tomclaw.mandarin.util.Logger; import com.tomclaw.mandarin.util.QueryBuilder; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Created with IntelliJ IDEA. * User: solkin * Date: 6/9/13 * Time: 7:27 PM */ public class RequestDispatcher { private static final long PENDING_REQUEST_DELAY = 4000; /** * Variables */ private Service service; private final SessionHolder sessionHolder; private final ContentResolver contentResolver; private int requestType; private DispatcherRunnable runnable; private ThreadPoolExecutor executor; private RequestObserver requestObserver; private volatile String executingRequestTag; public RequestDispatcher(Service service, SessionHolder sessionHolder, int requestType) { this.service = service; // Session holder. this.sessionHolder = sessionHolder; // Request type. this.requestType = requestType; // Variables. contentResolver = service.getContentResolver(); // Initializing executor and observer. initExecutor(); runnable = new DispatcherRunnable(); requestObserver = new RequestObserver(); } private void initExecutor() { executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(3)); } public void startObservation() { // Registering created observers. contentResolver.registerContentObserver( Settings.REQUEST_RESOLVER_URI, true, requestObserver); contentResolver.registerContentObserver( Settings.ACCOUNT_RESOLVER_URI, true, requestObserver); // Almost done. Starting. notifyQueue(); } /** * Stops task with specified tag. * * @param tag - tag of the task needs to be stopped. */ public boolean stopRequest(String tag) { // First of all, check that task is executing or in queue. if (TextUtils.equals(tag, executingRequestTag)) { // Task is executing this moment. // Interrupt thread as faster as it can be! // Task will receive interrupt exception. executor.shutdownNow(); initExecutor(); notifyQueue(); return true; } else { // Huh... Task is only in scheduled queue. // We can simply mark is as delayed "REQUEST_LATER". ContentValues contentValues = new ContentValues(); contentValues.put(GlobalProvider.REQUEST_STATE, Request.REQUEST_LATER); contentResolver.update(Settings.REQUEST_RESOLVER_URI, contentValues, GlobalProvider.REQUEST_TAG + "='" + tag + "'", null); return false; } } private class DispatcherRunnable implements Runnable { @Override public void run() { QueryBuilder queryBuilder = new QueryBuilder(); queryBuilder.columnEquals(GlobalProvider.REQUEST_TYPE, requestType).and() .columnNotEquals(GlobalProvider.REQUEST_STATE, Request.REQUEST_LATER); // Registering created observers. Cursor requestCursor = queryBuilder.query(contentResolver, Settings.REQUEST_RESOLVER_URI); // Check for we are ready to dispatch. if (requestCursor == null) { log("Something strange! Request or account cursor is null."); return; } try { dispatch(requestCursor); } finally { requestCursor.close(); } } @SuppressWarnings("unchecked") private void dispatch(Cursor requestCursor) { // Yeah, we are ready. log("Dispatching requests."); int requests = 0; // Checking for at least one request in database. if (requestCursor.moveToFirst()) { requests = requestCursor.getCount(); log("Found requests: " + requests); do { log("Request..."); // Obtain necessary column index. int rowColumnIndex = requestCursor.getColumnIndex(GlobalProvider.ROW_AUTO_ID); int classColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_CLASS); int sessionColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_SESSION); int persistentColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_PERSISTENT); int accountColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_ACCOUNT_DB_ID); int stateColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_STATE); int bundleColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_BUNDLE); int tagColumnIndex = requestCursor.getColumnIndex(GlobalProvider.REQUEST_TAG); /** * Если сессия совпадает, то постоянство задачи значения не имеет. * Если задача непостоянная, сессия отличается, то задача отклоняется. * Если задача постоянная, сессия отличается, то надо смотреть на статус: * В очереди: задача отправляется, как и в случае "обработано". * Обновляется ключ сессии приложения. * Обработано: задача может быть не доставленной до сервера, переотправить. * Обновляется ключ сессии приложения. * Отправлено: задача была отправлена, не нуждается в отправке, хотя ответа явно * не будет и задачу можно удалить. */ // Obtain values. int requestDbId = requestCursor.getInt(rowColumnIndex); boolean isPersistent = requestCursor.getInt(persistentColumnIndex) == 1; String requestAppSession = requestCursor.getString(sessionColumnIndex); int requestState = requestCursor.getInt(stateColumnIndex); // Checking for session is equals. if (TextUtils.equals(requestAppSession, CoreService.getAppSession())) { if (requestState != Request.REQUEST_PENDING) { log("Processed request of current session."); requests--; continue; } log("Normal request and will be processed now."); } else { boolean isDecline = false; boolean isBreak = false; // Checking for query is persistent. if (isPersistent) { switch (requestState) { case Request.REQUEST_PENDING: { // Persistent request, might be processed at anytime. log("Persistent request, might be processed at anytime."); break; } case Request.REQUEST_SENT: { // Request sent, processed by server, // but we have no answer. Decline. log("Request sent, processed by server, " + "but we have no answer. Decline."); isDecline = true; break; } } } else { // Decline request. isDecline = true; log("Another session and not persistent request."); } // Checking for request is obsolete and must be declined. if (isDecline) { contentResolver.delete(Settings.REQUEST_RESOLVER_URI, GlobalProvider.ROW_AUTO_ID + "='" + requestDbId + "'", null); requests--; break; } } String requestClass = requestCursor.getString(classColumnIndex); int requestAccountDbId = requestCursor.getInt(accountColumnIndex); String requestBundle = requestCursor.getString(bundleColumnIndex); String requestTag = requestCursor.getString(tagColumnIndex); log("Request received: " + "class = " + requestClass + "; " + "session = " + requestAppSession + "; " + "persistent = " + isPersistent + "; " + "account = " + requestAccountDbId + "; " + "state = " + requestState + "; " + "bundle = " + requestBundle + ""); int requestResult = Request.REQUEST_DELETE; Request request = null; try { // Obtain account root and request class (type). AccountRoot accountRoot = sessionHolder.getAccount(requestAccountDbId); // Checking for account online. if (accountRoot.isOffline()) { // Account is offline now. Let's send this request later. requests--; continue; } // Preparing request. request = (Request) GsonSingleton.getInstance().fromJson( requestBundle, Class.forName(requestClass)); executingRequestTag = requestTag; requestResult = request.onRequest(accountRoot, service); } catch (AccountNotFoundException e) { log("RequestDispatcher: account not found by request db id. " + "Cancelling."); } catch (Throwable ex) { log("Exception while loading request class: " + requestClass, ex); } finally { executingRequestTag = null; } // Checking for request result. if (requestResult == Request.REQUEST_DELETE) { // Result is delete-type. log("Result is delete-type"); contentResolver.delete(Settings.REQUEST_RESOLVER_URI, GlobalProvider.ROW_AUTO_ID + "='" + requestDbId + "'", null); requests--; } else if (requestResult == Request.REQUEST_PENDING) { // Request wasn't completed. We'll retry request a little bit later. log("Request wasn't completed. We'll retry request a little bit later."); break; } else if (requestResult == Request.REQUEST_SKIP) { // Request wasn't completed. We'll retry request after queue is released. log("Request wasn't completed. We'll retry request after queue is released."); } else { // Updating this request. log("Updating this request"); String requestJson = GsonSingleton.getInstance().toJson(request); ContentValues contentValues = new ContentValues(); contentValues.put(GlobalProvider.REQUEST_STATE, requestResult); contentValues.put(GlobalProvider.REQUEST_BUNDLE, requestJson); contentResolver.update(Settings.REQUEST_RESOLVER_URI, contentValues, GlobalProvider.ROW_AUTO_ID + "='" + requestDbId + "'", null); } } while (requestCursor.moveToNext()); } log("Dispatching completed, pending requests: " + requests); if (requests > 0) { // Pending guarantee dispatching after delay. log("Pending guarantee dispatching after delay"); try { Thread.sleep(PENDING_REQUEST_DELAY); } catch (InterruptedException ignored) { } notifyQueue(); } } } private void log(String message) { Logger.log("rd[" + requestType + "]: " + message); } private void log(String message, Throwable exception) { Logger.log("rd[" + requestType + "]: " + message, exception); } public void notifyQueue() { try { executor.submit(runnable); log("Queue notification accepted."); } catch (RejectedExecutionException ignored) { // All right, this is useless task. log("Queue notification received, but we already have notification."); } } private class RequestObserver extends ContentObserver { /** * Creates a content observer. */ public RequestObserver() { super(null); } @Override public void onChange(boolean selfChange) { Logger.log("RequestDispatcher: onChange [selfChange = " + selfChange + "]"); notifyQueue(); } } }