net.tawacentral.roger.secrets.OnlineAgentManager.java Source code

Java tutorial

Introduction

Here is the source code for net.tawacentral.roger.secrets.OnlineAgentManager.java

Source

// Copyright (c) 2009, 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 net.tawacentral.roger.secrets;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import net.tawacentral.roger.secrets.Secret.LogEntry;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

/**
 * Provides support for Online Sync Agents.
 * 
 * Sync process overview
 * 
 * On each resume a roll call is broadcast. Agents that respond are recorded in
 * a list of available agents.
 * 
 * Sync operations are always initiated from secrets. A sync request is
 * broadcast at the selected (or only) available agent, together with the
 * unencrypted secrets and a one-time validation key. The sync response is
 * validated against the key,and the returned secrets (updated or deleted) are
 * merged with the existing ones.
 * 
 * Multiple concurrent sync requests are not supported. It is the caller's
 * responsibility to ensure there is no active request when calling
 * sendSecrets().
 * 
 * @author Chris Wood
 */
public class OnlineAgentManager extends BroadcastReceiver {
    private static final String LOG_TAG = "OnlineAgentManager";

    private static final String SECRETS_PERMISSION = "net.tawacentral.roger.secrets.permission.SECRETS";

    // broadcast ACTIONS
    private static final String ROLLCALL = "net.tawacentral.roger.secrets.OSA_ROLLCALL";
    private static final String ROLLCALL_RESPONSE = "net.tawacentral.roger.secrets.OSA_ROLLCALL_RESPONSE";
    private static final String SYNC = "net.tawacentral.roger.secrets.SYNC";
    private static final String SYNC_RESPONSE = "net.tawacentral.roger.secrets.SYNC_RESPONSE";
    private static final String SYNC_CANCEL = "net.tawacentral.roger.secrets.SYNC_CANCEL";

    private static final String INTENT_CLASSID = "net.tawacentral.roger.secrets.ClassId";
    private static final String INTENT_DISPLAYNAME = "net.tawacentral.roger.secrets.DisplayName";
    private static final String INTENT_RESPONSEKEY = "net.tawacentral.roger.secrets.ResponseKey";
    private static final String INTENT_SECRETS = "net.tawacentral.roger.secrets.Secrets";

    // for the current request
    private static OnlineSyncAgent requestAgent;
    private static SecretsListActivity responseActivity;
    private static boolean active;

    /*
     * The response key is a randomly generated string that is provided to the
     * OSA as part of the sync request and must be returned in the response in
     * order for it to be accepted. The key is changed when the response is
     * received to ensure that any subsequent or unsolicited responses are
     * rejected.
     */
    private static String responseKey;
    private static final int RESPONSEKEY_LENGTH = 8;

    private static Map<String, OnlineSyncAgent> AVAILABLE_AGENTS = new HashMap<String, OnlineSyncAgent>();

    /*
     * Handle the received broadcast
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(LOG_TAG, "Agent Manager received msg: " + intent.getAction());

        // handle roll call response
        if (intent.getAction().equals(ROLLCALL_RESPONSE) && intent.getExtras() != null) {
            String classId = (String) intent.getExtras().get(INTENT_CLASSID);
            String displayName = (String) intent.getExtras().get(INTENT_DISPLAYNAME);
            if (classId == null || classId.length() == 0 || displayName == null || displayName.length() == 0) {
                // invalid info, so do not add it
                Log.e(LOG_TAG,
                        "Received invalid OSA rollcall resp: classId=" + classId + ",displayName=" + displayName);
            } else {
                AVAILABLE_AGENTS.put(classId, new OnlineSyncAgent(displayName, classId));
                Log.d(LOG_TAG, "Received OSA rollcall resp: " + classId + " " + displayName);
            }

            // handle sync response
        } else if (intent.getAction().equals(SYNC_RESPONSE) && validateResponse(intent)) {
            String secretsString = intent.getStringExtra(INTENT_SECRETS);
            ArrayList<Secret> secrets = null;
            if (secretsString != null) {
                try {
                    secrets = FileUtils.fromJSONSecrets(new JSONObject(secretsString));
                } catch (JSONException e) {
                    Log.e(LOG_TAG, "Received invalid JSON secrets data", e);
                }
            }
            active = false;
            responseActivity.syncSecrets(secrets, requestAgent.getDisplayName());
            requestAgent = null;
            responseKey = generateResponseKey(); // change response key
        }
    }

    /*
     * Validate the sync response from the agent. The key in the response must
     * match the one sent in the original sync request.
     * 
     * @param intent
     * 
     * @return true if response is OK, false otherwise
     */
    private boolean validateResponse(Intent intent) {
        if (intent.getExtras() != null) {
            String classId = (String) intent.getExtras().get(INTENT_CLASSID);
            String responseKey = (String) intent.getExtras().get(INTENT_RESPONSEKEY);
            OnlineSyncAgent agent = AVAILABLE_AGENTS.get(classId);
            if (agent != null) {
                if (agent == requestAgent) { // is a response expected?
                    if (OnlineAgentManager.responseKey != null // does the key match?
                            && OnlineAgentManager.responseKey.equals(responseKey)) {
                        if (active)
                            return true; // if request not cancelled
                        Log.w(LOG_TAG, "SYNC response received from agent " + classId
                                + " after request was cancelled - discarded");
                    } else {
                        Log.w(LOG_TAG,
                                "SYNC response received from agent " + classId + " with invalid response key");
                    }
                } else {
                    Log.w(LOG_TAG, "Unexpected SYNC response received from agent " + classId
                            + " - no request outstanding");
                }
            } else {
                Log.w(LOG_TAG, "SYNC response received from unknown app: " + classId);
            }
        } else {
            Log.w(LOG_TAG, "SYNC response received with no extras");
        }
        return false;
    }

    /**
     * Generate a new response key
     * 
     * @return String response key
     */
    public static String generateResponseKey() {
        SecureRandom random = new SecureRandom();
        byte[] keyBytes = new byte[RESPONSEKEY_LENGTH];
        random.nextBytes(keyBytes);
        return new String(keyBytes);
    }

    /**
     * Get available agents
     * 
     * @return collection of installed agents
     */
    public static Collection<OnlineSyncAgent> getAvailableAgents() {
        return Collections.unmodifiableCollection(AVAILABLE_AGENTS.values());
    }

    /**
     * Sends out the rollcall broadcast and will keep track of all OSAs that
     * respond.
     * 
     * Forget previous agents - only ones that respond are considered available.
     * 
     * @param context
     */
    public static void sendRollCallBroadcast(Context context) {
        AVAILABLE_AGENTS.clear();
        Intent broadcastIntent = new Intent(ROLLCALL);
        context.sendBroadcast(broadcastIntent, SECRETS_PERMISSION);
        Log.d(LOG_TAG, "sent broadcast");
    }

    /**
     * Sends secrets to the specified OSA.
     * 
     * Returns true if secrets are successfully sent, but makes no guarantees that
     * the secrets were received.
     * 
     * A one-time key is sent to the OSA and must be returned in the reply for it
     * to be considered valid.
     * 
     * @param agent
     * @param secrets
     * @param activity
     * @return true if secrets were sent
     */
    public static boolean sendSecrets(OnlineSyncAgent agent, ArrayList<Secret> secrets,
            SecretsListActivity activity) {
        requestAgent = agent;
        responseActivity = activity;
        responseKey = generateResponseKey();
        try {
            Intent secretsIntent = new Intent(SYNC);
            secretsIntent.setPackage(agent.getClassId());
            secretsIntent.putExtra(INTENT_RESPONSEKEY, OnlineAgentManager.responseKey);
            String secretString = FileUtils.toJSONSecrets(secrets).toString();
            secretsIntent.putExtra(INTENT_SECRETS, secretString);

            activity.sendBroadcast(secretsIntent, SECRETS_PERMISSION);
            Log.d(LOG_TAG, "Secrets sent to OSA " + agent.getClassId());
            active = true;
            return true;
        } catch (Exception e) {
            Log.e(LOG_TAG, "Error sending secrets to OSA", e);
            // ignore the exception, false will be returned below
        }
        return false;
    }

    /**
     * Test for active request
     * @return true if active
     */
    public static boolean isActive() {
        return active;
    }

    /**
     * Cancel the active request
     */
    public static void cancel() {
        OnlineAgentManager.active = false;
        Intent secretsIntent = new Intent(SYNC_CANCEL);
        secretsIntent.setPackage(requestAgent.getClassId());
        responseActivity.sendBroadcast(secretsIntent, SECRETS_PERMISSION);
    }

    /* Helper functions */

    /**
     * Add, update or delete the current secrets in the given collection.
     *
     * Assumes that the collection sort sequences are the same.
     * 
     * @param secrets 
     *          - target secrets collection
     * @param changedSecrets
     *          - added, changed or deleted secrets
     */
    public static void syncSecrets(ArrayList<Secret> secrets, ArrayList<Secret> changedSecrets) {
        for (Secret changedSecret : changedSecrets) {
            boolean done = false;

            for (int i = 0; i < secrets.size(); i++) {
                Secret existingSecret = secrets.get(i);
                int compare = changedSecret.compareTo(existingSecret);
                if (compare < 0 && !changedSecret.isDeleted()) {
                    secrets.add(i, changedSecret);
                    done = true;
                    Log.d(LOG_TAG, "syncSecrets: added '" + changedSecret.getDescription() + "'");
                    break;
                } else if (compare == 0) {
                    if (changedSecret.isDeleted()) {
                        secrets.remove(existingSecret);
                        Log.d(LOG_TAG, "syncSecrets: removed '" + changedSecret.getDescription() + "'");
                    } else {
                        existingSecret.update(changedSecret, LogEntry.SYNCED);
                        Log.d(LOG_TAG, "syncSecrets: updated '" + changedSecret.getDescription() + "'");
                    }

                    done = true;
                    break;
                }
            }

            if (!done && !changedSecret.isDeleted())
                secrets.add(changedSecret);
            Log.d(LOG_TAG, "syncSecrets: added '" + changedSecret.getDescription() + "'");
        }
    }

}