org.mobisocial.corral.ContentCorral.java Source code

Java tutorial

Introduction

Here is the source code for org.mobisocial.corral.ContentCorral.java

Source

/*
 * Copyright 2012 The Stanford MobiSocial Laboratory
 *
 * 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 org.mobisocial.corral;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import mobisocial.comm.BluetoothDuplexSocket;
import mobisocial.comm.DuplexSocket;
import mobisocial.comm.StreamDuplexSocket;
import mobisocial.musubi.App;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MObject;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.ObjectManager;
import mobisocial.musubi.objects.StoryObj;
import mobisocial.musubi.provider.MusubiContentProvider;
import mobisocial.musubi.provider.MusubiContentProvider.Provided;
import mobisocial.musubi.ui.util.UiUtil;
import mobisocial.musubi.util.Util;
import mobisocial.socialkit.Obj;
import mobisocial.socialkit.musubi.DbFeed;
import mobisocial.socialkit.musubi.DbObj;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.entity.ContentProducer;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;

/**
 * Stores and retrieves large content associated with objs.
 */
public class ContentCorral {
    private static final String PREF_CORRAL_BT_UUID = "corral_bt";
    public static final int SERVER_PORT = 8225;
    private static final String BT_CORRAL_NAME = "Content Corral";
    private static final String TAG = "ContentCorral";
    private static final boolean DBG = false;

    private BluetoothAcceptThread mBluetoothAcceptThread;
    private HttpAcceptThread mHttpAcceptThread;
    private Context mContext;

    /** 
     * A token generated for this corral instance.
     */
    static SecureRandom sSecureRandom;
    static String MOCK = "mock";
    static final int RAW = 1;
    static final int JSON = 2;
    static final int NEWS = 3;

    static UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        sMatcher.addURI(MOCK, "raw/#", RAW);
        sMatcher.addURI(MOCK, "json/#", JSON);
        sMatcher.addURI(MOCK, "news", NEWS);
    }

    static final Map<String, AccessScope> sAppTokens = new HashMap<String, AccessScope>();
    public static final String PICTURE_SUBFOLDER = "Pictures/Musubi";
    public static final String HTML_SUBFOLDER = "Musubi/HTML";
    public static final String FILES_SUBFOLDER = "Musubi/Files";
    public static final String APPS_SUBFOLDER = "Musubi/Apps";

    public ContentCorral(Context context) {
        mContext = context;
    }

    public void start() {
        startHttpServer();
        startBluetoothService();
    }

    private void startBluetoothService() {
        if (mBluetoothAcceptThread != null)
            return;
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter == null || !adapter.isEnabled()) {
            return;
        }

        mBluetoothAcceptThread = new BluetoothAcceptThread(adapter, getLocalBluetoothServiceUuid(mContext));
        mBluetoothAcceptThread.start();
    }

    /**
     * Starts the simple image server
     */
    private synchronized void startHttpServer() {
        if (mHttpAcceptThread != null)
            return;

        /*String ip = getLocalIpAddress();
        if (ip == null) {
        Log.w(TAG, "No wifi ip address; corral not loaded.");
        return;
        }*/
        mHttpAcceptThread = new HttpAcceptThread(SERVER_PORT, true);
        mHttpAcceptThread.start();
    }

    public synchronized void stop() {
        if (mHttpAcceptThread != null) {
            mHttpAcceptThread.cancel();
            mHttpAcceptThread = null;
        }
    }

    static class AccessScope {
        public String appId;
        public long objId;

        public AccessScope(String appId, long objId) {
            this.appId = appId;
            this.objId = objId;
        }
    }

    public static synchronized String registerForAccessToken(String appId, long objId) {
        String appToken = new BigInteger(130, initializeSecureRandom()).toString(32);
        sAppTokens.put(appToken, new AccessScope(appId, objId));
        return appToken;
    }

    public static synchronized void unregisterAppToken(String appToken) {
        sAppTokens.remove(appToken);
    }

    static SecureRandom initializeSecureRandom() {
        if (sSecureRandom == null) {
            sSecureRandom = new SecureRandom();
        }
        return sSecureRandom;
    }

    public static Uri storeContent(Context context, Uri contentUri) {
        return storeContent(context, contentUri, context.getContentResolver().getType(contentUri));
    }

    public static Uri storeContent(Context context, byte[] raw, String type) {
        File contentDir;
        if (type != null && (type.startsWith("image/") || type.startsWith("video/"))) {
            contentDir = new File(Environment.getExternalStorageDirectory(), PICTURE_SUBFOLDER);
        } else {
            contentDir = new File(Environment.getExternalStorageDirectory(), FILES_SUBFOLDER);
        }

        if (!contentDir.exists() && !contentDir.mkdirs()) {
            Log.e(TAG, "failed to create musubi corral directory");
            return null;
        }
        int timestamp = (int) (System.currentTimeMillis() / 1000L);
        String ext = CorralDownloadClient.extensionForType(type);
        String fname = timestamp + "-" + "webapp-raw" + "." + ext;
        File copy = new File(contentDir, fname);
        FileOutputStream out = null;
        try {
            contentDir.mkdirs();
            out = new FileOutputStream(copy);
            out.write(raw);
            return Uri.fromFile(copy);
        } catch (IOException e) {
            Log.w(TAG, "Error copying file", e);
            if (copy.exists()) {
                copy.delete();
            }
            return null;
        } finally {
            try {
                if (out != null)
                    out.close();
            } catch (IOException e) {
                Log.e(TAG, "failed to close handle on store corral content", e);
            }
        }
    }

    public static Uri storeContent(Context context, Uri contentUri, String type) {
        File contentDir;
        if (type != null && (type.startsWith("image/") || type.startsWith("video/"))) {
            contentDir = new File(Environment.getExternalStorageDirectory(), PICTURE_SUBFOLDER);
        } else {
            contentDir = new File(Environment.getExternalStorageDirectory(), FILES_SUBFOLDER);
        }

        if (!contentDir.exists() && !contentDir.mkdirs()) {
            Log.e(TAG, "failed to create musubi corral directory");
            return null;
        }
        int timestamp = (int) (System.currentTimeMillis() / 1000L);
        String ext = CorralDownloadClient.extensionForType(type);
        String fname = timestamp + "-" + contentUri.getLastPathSegment() + "." + ext;
        File copy = new File(contentDir, fname);
        FileOutputStream out = null;
        InputStream in = null;
        try {
            contentDir.mkdirs();
            in = context.getContentResolver().openInputStream(contentUri);
            BufferedInputStream bin = new BufferedInputStream(in);
            byte[] buff = new byte[1024];
            out = new FileOutputStream(copy);
            int r;
            while ((r = bin.read(buff)) > 0) {
                out.write(buff, 0, r);
            }
            bin.close();
            return Uri.fromFile(copy);
        } catch (IOException e) {
            Log.w(TAG, "Error copying file", e);
            if (copy.exists()) {
                copy.delete();
            }
            return null;
        } finally {
            try {
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
                Log.e(TAG, "failed to close handle on store corral content", e);
            }
        }
    }

    private class HttpAcceptThread extends Thread {
        // The local server socket
        private final ServerSocket mmServerSocket;

        public HttpAcceptThread(int port, boolean allowRemote) {
            ServerSocket tmp = null;

            // Create a new listening server socket
            try {
                tmp = new ServerSocket(port);
                if (!allowRemote) {
                    tmp.bind(new InetSocketAddress("127.0.0.1", port));
                } else {
                    tmp.bind(new InetSocketAddress("0.0.0.0", port));
                }
            } catch (IOException e) {
                System.err.println("Could not open server socket");
                e.printStackTrace(System.err);
            }
            mmServerSocket = tmp;
        }

        public void run() {
            if (mmServerSocket == null) {
                return;
            }

            // Log.d(TAG, "BEGIN mAcceptThread" + this);
            setName("AcceptThread");
            Socket socket = null;

            // Listen to the server socket always
            while (true) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    if (DBG)
                        Log.d(TAG, "corral waiting for client...");
                    socket = mmServerSocket.accept();
                    if (DBG)
                        Log.d(TAG, "corral client connected!");
                } catch (SocketException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                } catch (IOException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket == null) {
                    break;
                }

                DuplexSocket duplex;
                try {
                    duplex = new StreamDuplexSocket(socket.getInputStream(), socket.getOutputStream());
                } catch (IOException e) {
                    Log.e(TAG, "Failed to connect to socket", e);
                    return;
                }
                HttpConnectedThread conThread = new HttpConnectedThread(socket, duplex);
                conThread.start();
            }
            Log.d(TAG, "END mAcceptThread");
        }

        public void cancel() {
            Log.d(TAG, "cancel " + this);
            try {
                mmServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of server failed", e);
            }
        }
    }

    private class BluetoothAcceptThread extends Thread {
        // The local server socket
        private final BluetoothServerSocket mmServerSocket;

        public BluetoothAcceptThread(BluetoothAdapter adapter, UUID coralUuid) {
            BluetoothServerSocket tmp = null;

            // Create a new listening server socket
            try {
                try {
                    if (DBG)
                        Log.d(TAG, "Bluetooth corral listening on " + adapter.getAddress() + ":" + coralUuid);
                    tmp = adapter.listenUsingRfcommWithServiceRecord(BT_CORRAL_NAME, coralUuid);
                } catch (NoSuchMethodError e) {
                    // Let's not deal with pairing UI.
                }
            } catch (IOException e) {
                Log.e(TAG, "Could not open bt server socket");
                e.printStackTrace(System.err);
            } catch (NoSuchMethodError e) {
                Log.e(TAG, "Bluetooth Corral not available for this Android version.");
            }
            mmServerSocket = tmp;
        }

        public void run() {
            if (mmServerSocket == null) {
                return;
            }

            // Log.d(TAG, "BEGIN mAcceptThread" + this);
            setName("AcceptThread");
            BluetoothSocket socket = null;

            // Listen to the server socket always
            while (true) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    if (DBG)
                        Log.d(TAG, "Corral bluetooth server waiting for client...");
                    socket = mmServerSocket.accept();
                    if (DBG)
                        Log.d(TAG, "Corral bluetooth server connected!");
                } catch (SocketException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                } catch (IOException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket == null) {
                    break;
                }

                DuplexSocket duplex = new BluetoothDuplexSocket(socket);
                CorralConnectedThread conThread = new CorralConnectedThread(duplex);
                conThread.start();
            }
            Log.d(TAG, "END mAcceptThread");
        }

        @SuppressWarnings("unused")
        public void cancel() {
            Log.d(TAG, "cancel " + this);
            try {
                mmServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of server failed", e);
            }
        }
    }

    /**
     * This thread runs during a connection with a remote device. It supports
     * incoming and outgoing transmissions over HTTP.
     */
    private class HttpConnectedThread extends Thread {
        private final DuplexSocket mmDuplexSocket;
        private final Socket mmRealSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private final int BUFFER_LENGTH = 1024;

        public HttpConnectedThread(Socket socket, DuplexSocket streams) {
            // Log.d(TAG, "create ConnectedThread");
            mmRealSocket = socket;
            mmDuplexSocket = streams;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            Log.d(TAG, "BEGIN mConnectedThread");
            byte[] buffer = new byte[BUFFER_LENGTH];
            int bytes;

            if (mmInStream == null || mmOutStream == null)
                return;

            // Read header information, determine connection type
            try {
                bytes = mmInStream.read(buffer);
                if (DBG)
                    Log.d(TAG, "read " + bytes + " header bytes");
                String header = new String(buffer, 0, bytes);
                if (DBG)
                    Log.d(TAG, header);
                // determine request type
                if (header.startsWith("GET ")) {
                    doGetRequest(header);
                }
            } catch (Exception e) {
                Log.e(TAG, "Error reading connection header", e);
            }

            // No longer listening.
            cancel();
        }

        class CorralHttpRequest {
            final String mRequest;
            final Map<String, String> mHeaders = new HashMap<String, String>();
            String mMethod;
            String mPath;

            public CorralHttpRequest(String httpRequest) {
                mRequest = httpRequest;
                parseRequest();
            }

            void parseRequest() {
                String[] headers = mRequest.split("\r\n");
                if (headers.length == 0) {
                    throw new IllegalArgumentException("Bad http request");
                }
                for (int i = 1; i < headers.length; i++) {
                    String h = headers[i];
                    int col = h.indexOf(':');
                    if (col > -1) {
                        String v = h.substring(col + 1).trim();
                        mHeaders.put(h.substring(0, col).trim().toLowerCase(), v);
                    }
                }

                String[] request = headers[0].split(" ");
                if (request.length == 0) {
                    throw new IllegalArgumentException("No method");
                }
                mMethod = request[0];
                mPath = request[1];
            }
        }

        void handleRaw(Uri targetUri) {
            long objId = Long.parseLong(targetUri.getLastPathSegment());
            String ticket = targetUri.getQueryParameter("ticket");
            if (ticket == null) {
                notAuthorized();
                return;
            }
            AccessScope scope = sAppTokens.get(ticket);
            if (scope == null || scope.objId != objId) {
                // TODO: grant access based on appId/feed etc.
                notAuthorized();
                return;
            }

            Uri uri = MusubiContentProvider.uriForItem(Provided.OBJS_ID, objId);
            String[] projection = new String[] { DbObj.COL_RAW, DbObj.COL_JSON };
            String selection = DbObj.COL_ID + "=?";
            String[] selectionArgs = new String[] { Long.toString(objId) };
            String sortOrder = null;
            Cursor obj = mContext.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
            try {
                if (obj.moveToFirst()) {
                    try {
                        String type = "application/octet";
                        byte[] bytes = obj.getBlob(0);
                        String jsonSrc = obj.getString(1);
                        try {
                            JSONObject json = new JSONObject(jsonSrc);
                            if (json.has(CorralDownloadClient.OBJ_MIME_TYPE)) {
                                type = json.getString(CorralDownloadClient.OBJ_MIME_TYPE);
                            }
                        } catch (JSONException e) {
                        }

                        sendRaw(type, bytes);
                        mmOutStream.close();
                    } catch (IOException e) {
                        if (DBG)
                            Log.w(TAG, "corral http", e);
                    }
                } else {
                    try {
                        mmOutStream.write(header("HTTP/1.1 404 NOT FOUND"));
                        mmOutStream.close();
                    } catch (IOException e) {
                        if (DBG)
                            Log.w(TAG, "corral http", e);
                    }
                }
            } finally {
                obj.close();
            }
            return;
        }

        void handleNews(Uri targetUri) {
            if (DBG)
                Log.d(TAG, "reading the news");
            PrintWriter pw = new PrintWriter(mmOutStream);
            if (!"/127.0.0.1".equals(mmRealSocket.getLocalAddress().toString())) {
                pw.append("HTTP/1.1 403 FORBIDDEN\r\n\r\n");
                return;
            }

            Uri uri = MusubiContentProvider.uriForDir(Provided.OBJECTS);
            String[] projection = new String[] { MObject.COL_ID };
            String selection = "type=?";
            String[] selectionArgs = new String[] { StoryObj.TYPE };
            String sortOrder = DbObj.COL_ID + " DESC LIMIT 10";
            Cursor c = mContext.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
            try {
                PulseFeed pulse = new PulseFeed(mContext, c);
                String news = pulse.toJson().toString();
                if (DBG)
                    Log.d(TAG, "News feed: " + news);

                pw.append("HTTP/1.1 200 OK\r\n");
                pw.append("Content-Type: application/json\r\n");
                pw.append("Content-Length: " + news.getBytes().length + "\r\n");
                pw.append("\r\n");
                pw.append(news); // TODO: memory management; streaming interface
                pw.close();
                mmOutStream.close();
            } catch (IOException e) {
                if (DBG)
                    Log.d(TAG, "io", e);
            } finally {
                c.close();
            }

            /*Log.d(TAG, "mocking pulse data");
            String PULSE_ASSET = "pulse.txt";
            try {
            String content = IOUtils.toString(mContext.getResources().getAssets().open(PULSE_ASSET));
            pw.append("HTTP/1.1 200 OK\r\n");
            pw.append("Content-Type: application/json\r\n");
            pw.append("Content-Length: " + content.getBytes().length);
            pw.append("\r\n\r\n");
            pw.append(content);
            pw.close();
            mmOutStream.close();
            } catch (IOException e) {
            Log.e(TAG, "failed to pulse", e);
            }*/
        }

        void handleApp(Uri targetUri) {
            if (targetUri.getPath().contains("..")) {
                return;
            }
            File appFolder = new File(Environment.getExternalStorageDirectory(), APPS_SUBFOLDER);
            String filePath;
            if (targetUri.getPathSegments().size() == 2) {
                filePath = targetUri.getPathSegments().get(1) + "/index.html";
            } else {
                filePath = targetUri.getPath();
                filePath = filePath.replaceFirst("/app", "");
            }
            try {
                FileInputStream fileInputStream = new FileInputStream(new File(appFolder, filePath));
                mmOutStream.write(header("HTTP/1.1 200 OK"));
                //mmOutStream.write(header("Content-Type: " + type));
                mmOutStream.write(header("Content-Length: " + fileInputStream.available()));
                mmOutStream.write(header(""));
                IOUtils.copy(fileInputStream, mmOutStream);
            } catch (IOException e) {
                Log.e(TAG, "Error sending app file", e);
            }
        }

        /**
         * Handles an HTTP GET request for objects, raw content,
         * and Corral images.
         */
        private void doGetRequest(String httpRequest) {
            CorralHttpRequest request = new CorralHttpRequest(httpRequest);
            if (!"GET".equals(request.mMethod)) {
                throw new IllegalArgumentException();
            }

            Uri targetUri = Uri.parse("content://" + MOCK + request.mPath);

            int match = sMatcher.match(targetUri);
            boolean handled = true;
            switch (match) {
            case RAW:
                handleRaw(targetUri);
                break;
            case NEWS:
                handleNews(targetUri);
                break;
            default:
                handled = false;
            }

            if (handled) {
                return;
            }

            if (request.mPath.startsWith("/app")) {
                handleApp(targetUri);
                return;
            }

            // Old-School:

            if (targetUri.getQueryParameter("content") == null) {
                try {
                    mmOutStream.write(header("HTTP/1.1 404 NOT FOUND\r\n\r\n"));
                    mmOutStream.close();
                } catch (IOException e) {
                }
                return;
            }

            if (targetUri.getQueryParameter("hash") == null) {
                notAuthorized();
                return;
            }

            // Verify the hash is for an obj with the given filepath.
            // TODO: This is not secure. Require challenge/response authentication.
            String universalHashStr = targetUri.getQueryParameter("hash");
            String contentPath = targetUri.getQueryParameter("content");
            byte[] universalHashBytes = Util.convertToByteArray(universalHashStr);
            ObjectManager om = new ObjectManager(App.getDatabaseSource(mContext));
            long objId = om.getObjectIdForHash(universalHashBytes);
            if (objId == -1) {
                try {
                    mmOutStream.write(header("HTTP/1.1 410 GONE\r\n\r\n"));
                    mmOutStream.close();
                } catch (IOException e) {
                }
                return;
            }

            JSONObject json;
            MObject obj = om.getObjectForId(objId);
            if (obj.json_ == null) {
                notAuthorized();
                return;
            }
            try {
                json = new JSONObject(obj.json_);
            } catch (JSONException e) {
                notAuthorized();
                return;
            }

            String localPath = json.optString(CorralDownloadClient.OBJ_LOCAL_URI);
            if (!contentPath.equals(localPath)) {
                try {
                    mmOutStream.write(header("HTTP/1.1 400 BAD REQUEST\r\n\r\n"));
                    mmOutStream.close();
                } catch (IOException e) {
                }
                return;
            }

            // OK to download:
            Uri requestPath = Uri.parse(contentPath);
            String scheme = requestPath.getScheme();
            if ("content".equals(scheme) || "file".equals(scheme)) {
                if (DBG)
                    Log.d(TAG, "Retrieving for " + requestPath.getAuthority());
                if (MusubiContentProvider.AUTHORITY.equals(requestPath.getAuthority())) {
                    if (requestPath.getQueryParameter("obj") != null) {
                        int objIndex = Integer.parseInt(requestPath.getQueryParameter("obj"));
                        // sendObj(requestPath, objIndex);
                        Log.w(TAG, "Unknown corral request");
                    } else {
                        Log.w(TAG, "Unknown corral request");
                    }
                } else {
                    sendContent(requestPath);
                }
            }
        }

        class PulseFeed {
            final IdentitiesManager mIdentityManager;
            final JSONArray mEntries;

            public PulseFeed(Context context, Cursor c) {
                mIdentityManager = new IdentitiesManager(App.getDatabaseSource(context));
                mEntries = new JSONArray();

                try {
                    while (c.moveToNext()) {
                        mEntries.put(getEntry(c.getLong(0)));
                    }
                } catch (JSONException e) {
                    throw new IllegalArgumentException(e);
                }
            }

            public JSONObject toJson() {
                try {
                    JSONObject pulse = new JSONObject();
                    pulse.put("responseData", getResponse());
                    return pulse;
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
            }

            JSONObject getEntry(long id) throws JSONException {
                JSONArray categories = new JSONArray();
                categories.put("friends");
                JSONObject entry = new JSONObject();
                // Pulse: title, link, author, publishedDate, contentSnippet, content, categories
                // Musubi: title, text, favicon_length, original_url, [raw]

                DbObj story = App.getMusubi(mContext).objForId(id);
                MIdentity senderId = mIdentityManager.getIdentityForId(story.getSenderId());
                String sender = UiUtil.safeNameForIdentity(senderId);
                JSONObject meta = story.getJson();
                entry.put("title", meta.optString(StoryObj.TITLE));
                entry.put("link", meta.optString(StoryObj.ORIGINAL_URL));
                entry.put("author", sender);
                entry.put("publishDate", new Date(story.getTimestamp()));
                entry.put("contentSnippet", meta.optString(StoryObj.TEXT));
                entry.put("content", meta.optString(StoryObj.TEXT));
                entry.put("categories", categories);

                return entry;
            }

            JSONObject getResponse() throws JSONException {
                JSONObject response = new JSONObject();
                response.put("feed", getFeed());
                return response;
            }

            JSONObject getFeed() throws JSONException {
                JSONObject feed = new JSONObject();
                feed.put("feedUrl", "content://org.musubi.db/news");
                feed.put("title", "Musubi");
                feed.put("link", "http://127.0.0.1:" + SERVER_PORT + "/news");
                feed.put("author", "Stanford");
                feed.put("description", "Stories from Friends");
                feed.put("type", "rss20");
                feed.put("entries", getEntries());
                return feed;
            }

            JSONArray getEntries() {
                return mEntries;
            }
        }

        void notAuthorized() {
            try {
                mmOutStream.write(header("HTTP/1.1 401 UNAUTHORIZED\r\n\r\n"));
                mmOutStream.close();
            } catch (IOException e) {
            }
        }

        void sendRaw(String type, byte[] bytes) throws IOException {
            mmOutStream.write(header("HTTP/1.1 200 OK"));
            mmOutStream.write(header("Content-Type: " + type));
            mmOutStream.write(header("Content-Length: " + bytes.length));
            mmOutStream.write(header(""));

            InputStream in = new ByteArrayInputStream(bytes);
            byte[] buf = new byte[1024];
            int r;
            while ((r = in.read(buf)) > 0) {
                mmOutStream.write(buf, 0, r);
            }
            ;
        }

        private void sendContent(Uri requestPath) {
            InputStream in;
            try {
                // img = Uri.withAppendedPath(Images.Media.EXTERNAL_CONTENT_URI,
                // imgId);
                in = mContext.getContentResolver().openInputStream(requestPath);
            } catch (Exception e) {
                Log.d(TAG, "Error opening file", e);
                return;
            }

            try {
                byte[] buffer = new byte[4096];
                int r = 0;

                // Gross way to get length. What's the right way??
                int size = 0;
                while ((r = in.read(buffer)) > 0) {
                    size += r;
                }
                String type = mContext.getContentResolver().getType(requestPath);
                if (type == null) {
                    int p = requestPath.toString().lastIndexOf(".");
                    if (p > 0) {
                        String ext = requestPath.toString().substring(p + 1);
                        type = CorralDownloadClient.typeForExtension(ext);
                    }
                }
                in = mContext.getContentResolver().openInputStream(requestPath);
                mmOutStream.write(header("HTTP/1.1 200 OK"));
                if (type != null) {
                    mmOutStream.write(header("Content-Type: " + type));
                }
                mmOutStream.write(header("Content-Length: " + size));
                // mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
                mmOutStream.write(header(""));

                while ((r = in.read(buffer)) > 0) {
                    mmOutStream.write(buffer, 0, r);
                }

            } catch (Exception e) {
                Log.e(TAG, "Error sending file", e);
            } finally {
                try {
                    mmOutStream.close();
                } catch (IOException e) {
                }
            }
        }

        private void sendObj(Uri requestPath, int objIndex) {
            InputStream in;
            byte[] bytes;
            try {
                Uri uri;
                try {
                    uri = DbFeed.uriForId(Long.parseLong(requestPath.getPath().substring(1)));
                } catch (NumberFormatException e) {
                    return;
                }
                String[] projection = new String[] { DbObj.COL_ID, MObject.COL_JSON };
                String selection = MObject.COL_RENDERABLE + " = 1";
                String[] selectionArgs = null;
                String sortOrder = DbObj.COL_ID + " ASC";
                Cursor cursor = mContext.getContentResolver().query(uri, projection, selection, selectionArgs,
                        sortOrder);
                try {

                    if (!cursor.moveToPosition(objIndex)) {
                        Log.d(TAG, "No obj found for " + uri);
                        return;
                    }
                    String jsonStr = cursor.getString(1);
                    bytes = jsonStr.getBytes();
                    in = new ByteArrayInputStream(bytes);
                } finally {
                    cursor.close();
                }
            } catch (Exception e) {
                Log.d(TAG, "Error opening obj", e);
                return;
            }

            try {
                byte[] buffer = new byte[4096];
                int r = 0;

                mmOutStream.write(header("HTTP/1.1 200 OK"));
                mmOutStream.write(header("Content-Type: text/plain"));
                mmOutStream.write(header("Content-Length: " + bytes.length));
                // mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
                mmOutStream.write(header(""));

                while ((r = in.read(buffer)) > 0) {
                    Log.d(TAG, "sending: " + new String(buffer));
                    mmOutStream.write(buffer, 0, r);
                }

            } catch (Exception e) {
                Log.e(TAG, "Error sending file", e);
            } finally {
                try {
                    mmOutStream.close();
                } catch (IOException e) {
                }
            }
        }

        private void sendObjs(Uri requestPath) {
            // TODO: hard-coded limit of 30 in place.
            InputStream in;
            byte[] bytes;
            StringBuilder jsonArrayBuilder = new StringBuilder("[");
            try {
                Uri uri;
                try {
                    uri = DbFeed.uriForId(Long.parseLong(requestPath.getPath().substring(1)));
                } catch (NumberFormatException e) {
                    return;
                }
                String[] projection = new String[] { DbObj.COL_ID, MObject.COL_JSON };
                String selection = MObject.COL_RENDERABLE + " = 1";
                String[] selectionArgs = null;
                String sortOrder = DbObj.COL_ID + " ASC LIMIT 30";
                Cursor cursor = mContext.getContentResolver().query(uri, projection, selection, selectionArgs,
                        sortOrder);

                try {
                    if (!cursor.moveToFirst()) {
                        Log.d(TAG, "No objs found for " + uri);
                        return;
                    }
                    jsonArrayBuilder.append(cursor.getString(1));
                    while (!cursor.isLast()) {
                        cursor.moveToNext();
                        String jsonStr = cursor.getString(1);
                        jsonArrayBuilder.append(",").append(jsonStr);
                    }
                } finally {
                    cursor.close();
                }
            } catch (Exception e) {
                Log.d(TAG, "Error opening obj", e);
                return;
            }

            bytes = jsonArrayBuilder.append("]").toString().getBytes();
            in = new ByteArrayInputStream(bytes);
            try {
                byte[] buffer = new byte[4096];
                int r = 0;

                mmOutStream.write(header("HTTP/1.1 200 OK"));
                mmOutStream.write(header("Content-Type: text/plain"));
                mmOutStream.write(header("Content-Length: " + bytes.length));
                // mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
                mmOutStream.write(header(""));

                while ((r = in.read(buffer)) > 0) {
                    Log.d(TAG, "sending: " + new String(buffer));
                    mmOutStream.write(buffer, 0, r);
                }

            } catch (Exception e) {
                Log.e(TAG, "Error sending file", e);
            } finally {
                try {
                    mmOutStream.close();
                } catch (IOException e) {
                }
            }
        }

        public void cancel() {
            try {
                mmDuplexSocket.close();
            } catch (IOException e) {
            }
        }
    }

    private byte[] header(String str) {
        return (str + "\r\n").getBytes();
    }

    public static String getLocalIpAddress() {
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en
                    .hasMoreElements();) {
                NetworkInterface intf = en.nextElement();
                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress()) {
                        // not ready for IPv6, apparently.
                        if (!inetAddress.getHostAddress().contains(":")) {
                            return inetAddress.getHostAddress().toString();
                        }
                    }
                }
            }
        } catch (SocketException ex) {

        }
        return null;
    }

    /**
     * This thread runs during a connection with a remote device. It supports
     * incoming and outgoing transmissions over HTTP.
     */
    private class CorralConnectedThread extends Thread {
        private final DuplexSocket mmSocket;

        private final InputStream mmInStream;

        private final OutputStream mmOutStream;

        private final int BUFFER_LENGTH = 1024;

        public CorralConnectedThread(DuplexSocket socket) {
            if (DBG)
                Log.d(TAG, "create CorralConnectedThread");

            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            Log.d(TAG, "BEGIN CorralConnectedThread");
            byte[] buffer = new byte[BUFFER_LENGTH];
            int bytes;

            if (mmInStream == null || mmOutStream == null)
                return;

            // Read header information, determine connection type
            try {
                PosiServerProtocol protocol = new PosiServerProtocol(mmSocket);
                CorralRequestHandler handler = protocol.getRequestHandler();

                /**
                 * TODO: SNEP-like protocol here, for ObjEx. Remember, we have
                 * authenticated objs, ndef does not. server: NONCE CHALLENGE
                 * client: AUTHED REQUEST server: AUTHED RESPONSE
                 */
                bytes = mmInStream.read(buffer);
                Log.d(TAG, "read " + bytes + " header bytes");
                String header = new String(buffer, 0, bytes);

                /**
                 * Your task is to find out which friends are nearby. We're just
                 * going to try to connect to all of their CORRAL_BLUETOOTH
                 * ports and send a quick HELLO. First visual is to show this in
                 * a "nearby" list. We'll easily up-convert to groups. Dumb
                 * algorithm for now just iterates over MACs and tries to
                 * connect, following protocol.
                 */

                // TODO
                Log.d(TAG, "BJD BLUETOOTH CORRAL NOT READY: ObjEx needs defining.");
            } catch (Exception e) {
                Log.e(TAG, "Error reading connection header", e);
            }

            // No longer listening.
            cancel();
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
            }
        }
    }

    public static UUID getLocalBluetoothServiceUuid(Context c) {
        SharedPreferences prefs = c.getSharedPreferences("main", 0);
        if (!prefs.contains(PREF_CORRAL_BT_UUID)) {
            UUID btUuid = UUID.randomUUID();
            prefs.edit().putString(PREF_CORRAL_BT_UUID, btUuid.toString()).commit();
        }
        String uuidStr = prefs.getString(PREF_CORRAL_BT_UUID, null);
        return (uuidStr == null) ? null : UUID.fromString(uuidStr);
    }

    static class PosiServerProtocol {
        public static final int POSI_MARKER = 0x504f5349;

        public static final int POSI_VERSION = 0x01;

        static SecureRandom sSecureRandom;

        private final DuplexSocket mmDuplexSocket;

        public PosiServerProtocol(DuplexSocket socket) {
            if (sSecureRandom == null) {
                sSecureRandom = new SecureRandom();
            }
            mmDuplexSocket = socket;
        }

        private byte[] getHeader() {
            byte[] header = new byte[16];
            ByteBuffer buffer = ByteBuffer.wrap(header);
            buffer.putInt(POSI_MARKER);
            buffer.putInt(POSI_VERSION);
            buffer.putLong(sSecureRandom.nextLong());
            return header;
        }

        // TODO:
        public CorralRequestHandler getRequestHandler() throws IOException {
            if (DBG)
                Log.d(TAG, "Getting request handler for posi session");
            OutputStream out = mmDuplexSocket.getOutputStream();
            byte[] header = getHeader();
            if (DBG)
                Log.d(TAG, "Writing header " + new String(header));
            out.write(header);
            if (DBG)
                Log.d(TAG, "Flushing header bytes");
            out.flush();
            if (DBG)
                Log.d(TAG, "Done writing header.");
            /**
             * TODO: SignedObj obj = ObjDecoder.decode(readObj()) Authenticate
             * signer and select protocol. Authentication verifies nonce and
             * ensures timestamp is more recent than the users' last transmitted
             * obj's timestamp.
             */
            return new NonceRequestHandler();
        }
    }

    /**
     * The trivial request handler that sends a nonce and hangs up.
     */
    static class NonceRequestHandler implements CorralRequestHandler {
        // Does nothing.
    }

    interface CorralRequestHandler {
    }

    /**
     * Makes a webapp available offline via the Corral.
     */
    public static Uri cacheWebApp(Uri appUri) {
        String appName = getWebappCacheName(appUri);
        return cacheWebApp(appUri, appName);
    }

    /**
     * Makes a webapp available offline via the Corral.
     */
    public static Uri cacheWebApp(Uri appUri, String name) {
        File localAppDir = new File(
                new File(Environment.getExternalStorageDirectory(), ContentCorral.APPS_SUBFOLDER), name);
        try {
            FileUtils.deleteDirectory(localAppDir);
        } catch (IOException e) {
        }
        localAppDir.mkdirs();
        String indexPath = getIndexPath(appUri);
        try {
            downloadFile(localAppDir, appUri, indexPath);
            scrapePage(localAppDir, appUri.buildUpon().path("").build(), indexPath);
        } catch (IOException e) {
            Log.e(TAG, "Failed to cache webapp", e);
        }
        return getWebappCacheUrl(appUri, localAppDir.getName());
    }

    public static Uri getWebappCacheUrl(Uri webapp) {
        return getWebappCacheUrl(webapp, getWebappCacheName(webapp));
    }

    public static Uri getWebappCacheUrl(Uri webapp, String localName) {
        File appPath = new File(Environment.getExternalStorageDirectory(), APPS_SUBFOLDER);
        appPath = new File(appPath, localName);
        if (appPath.exists()) {
            return Uri.parse("http://127.0.0.1:" + SERVER_PORT + "/app/" + localName + getIndexPath(webapp));
        }
        return null;
    }

    public static String getWebappCacheName(Uri webapp) {
        return Util.convertToHex(Util.sha256(webapp.toString().getBytes())).substring(0, 10);
    }

    private static Pattern sScriptRegex = Pattern.compile("<\\s*script\\s+[^>]+>", Pattern.CASE_INSENSITIVE);
    private static Pattern sSrcRegex = Pattern.compile("\\bsrc\\s*=\\s*(\"[^\"]+\"|'[^']+')",
            Pattern.CASE_INSENSITIVE);

    private static void scrapePage(File storageDir, Uri baseUri, String relativePath) throws IOException {
        File sourceFile = new File(storageDir, relativePath);
        String page = IOUtils.toString(new FileInputStream(sourceFile));
        Matcher matcher = sScriptRegex.matcher(page);
        int offset = 0;
        while (matcher.find(offset)) {
            try {
                String tag = matcher.group();
                Matcher srcMatcher = sSrcRegex.matcher(tag);
                if (!srcMatcher.find())
                    continue;
                String srcPath = srcMatcher.group(1);
                srcPath = srcPath.substring(1, srcPath.length() - 1);
                srcPath = StringEscapeUtils.unescapeHtml4(srcPath);
                //srcPath = absoluteToRelative(baseUri, srcPath);
                if (!srcPath.contains("://")) {
                    Uri absolutePath = getAbsoluteUri(baseUri, relativePath, srcPath);
                    downloadFile(storageDir, absolutePath, absolutePath.getPath());
                }
            } finally {
                offset = matcher.end();
            }
        }
    }

    /**
     * If the given path is provisioned by baseUri, returns a localized
     * representation of that path.
     */
    private String absoluteToRelative(Uri baseUri, String path) {
        if (path.startsWith(baseUri.toString())) {
            path = path.substring(baseUri.toString().length());
            Log.d(TAG, "TRUNCATED TO " + path);
        }
        return path;
    }

    private static String getIndexPath(Uri uri) {
        String seg = uri.getLastPathSegment();
        if (seg.endsWith(".html") || seg.endsWith(".htm") || seg.endsWith(".php")) {
            return uri.getPath();
        }
        return uri.buildUpon().appendPath("index.html").build().getPath();
    }

    private static Uri getAbsoluteUri(Uri baseUri, String parentFile, String src) {
        if (src.startsWith("/")) {
            return baseUri.buildUpon().path(src).build();
        }

        Uri.Builder builder = baseUri.buildUpon();
        String[] parentPath = parentFile.split("/");
        for (int i = 0; i < parentPath.length - 1; i++) {
            builder.appendPath(parentPath[i]);
        }

        String[] srcParts = src.split("/");
        for (String part : srcParts) {
            builder.appendPath(part);
        }
        return builder.build();
    }

    private static void downloadFile(File storageDir, Uri page, String localPath) throws IOException {
        URL url = new URL(page.toString());
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());

        File outFile = new File(storageDir, localPath);
        outFile.getParentFile().mkdirs();
        FileOutputStream outStream = new FileOutputStream(outFile);

        byte[] buffer = new byte[2048];
        int readLength;
        while ((readLength = inputStream.read(buffer)) > 0) {
            outStream.write(buffer, 0, readLength);
        }

        outStream.close();
        inputStream.close();
    }
}