FacebookService.java

/**
 * Copyright (c) 2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners. 
 * See license text file delivered with this project for more information.
 */

package com.nokia.example.statusshout.engine;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.microedition.content.Invocation;
import javax.microedition.content.Registry;
import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.midlet.MIDlet;

import com.nokia.example.statusshout.ui.ImageUtils;

/**
 * Provides methods for authentication to Facebook and sharing status updates.
 */
public class FacebookService extends OAuthService {
    // Constants
    private static final String TAG = "FacebookService.";
    
    /*
     * TODO: Get your application ID from developers.facebook.com
     * 
     * Note that this Facebook implementation will not work without a valid
     * application ID!
     */
    private static final String APP_ID = ""; // Facebook app client ID
    
    /*
     * See https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/
     * 
     * Note that "m" prefix needs to be used instead of "www"! Otherwise, the
     * app will not be able to intercept the access token!
     */
    private static final String REDIRECT_URL = "https://m.facebook.com/connect/login_success.html";
    
    private static final String OAUTH_URL = "https://facebook.com/dialog/oauth";
    private static final String GRAPH_URL = "https://graph.facebook.com/";
    private static final String ACCESS_TOKEN_STRING = "access_token=";
    
    // The full URL address used for logging in and retrieving the access token.
    // For more information, see https://developers.facebook.com/docs/reference/dialogs/oauth/
    private static final String LOGIN_URL = OAUTH_URL
            + "?client_id=" + APP_ID
            + "&redirect_uri=" + REDIRECT_URL
            + "&display=popup"
            + "&scope=publish_stream,user_status,user_photos,photo_upload" // Permissions
            + "&response_type=token";

    // See http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
    private static final String BOUNDARY = "th15iSmYB0und4RyM3s54G";

    // Members
    private String messageToSend;
    private String imageToShare;

    /**
     * Constructor.
     */
    public FacebookService(MIDlet midlet, ShareListener listener) {
        super(midlet, listener);
    }

    /**
     * @see javax.microedition.content.ResponseListener#invocationResponseNotify(javax.microedition.content.Registry)
     */
    public void invocationResponseNotify(Registry registry) {
        Invocation response = registry.getResponse(true);
        final int responseCode = response.getStatus();
        System.out.println(TAG + "invocationResponseNotify(): Response: " + responseCode);
        
        if (responseCode == Invocation.OK) {
            final String token = parseToken(response.getURL());
            System.out.println(TAG + "invocationResponseNotify(): Parsed token: " + token);
            
            if (token != null && token.length() > 0) {
                // Parsed token seems valid. Store it.
                StatusShoutData.getInstance().setFacebookToken(token);
                
                if (listener != null) {
                    listener.onAuthenticated(this);
                }
                
                // Check if we had pending content to be shared
                if ((messageToSend != null && messageToSend.length() > 0)
                        || (imageToShare != null && imageToShare.length() > 0))
                {
                    // Send the pending message and/or image
                    doShare(messageToSend, imageToShare);
                    messageToSend = null;
                    imageToShare = null;
                }
            }
            else if (listener != null) {
                listener.onError("Failed to parse Facebook token!");
            }
        }
        else if (listener != null) {
            listener.onError("Failed to authenticate. Server response code was "
                    + responseCode + ".");
        }
    }

    /**
     * @see com.nokia.example.statusshout.engine.OAuthService#authenticate()
     */
    public void authenticate() {
        if (APP_ID.length() == 0 && listener != null) {
            listener.onError("No Facebook application ID defined!");
            return;
        }
        
        setAsListener(this);
        createInvocation(LOGIN_URL, REDIRECT_URL); // Implemented in the base class
    }

    /**
     * @see com.nokia.example.statusshout.engine.OAuthService#isValid(java.lang.String)
     */
    public boolean isValid(String token) {
        if (token != null && token.length() > 0) {
            // TODO Do a proper validation
            return true;
        }
        return false;
    }

    /**
     * Shares a message and/or an image.
     * 
     * @param message The message to share.
     * @param imageUri The URI of the image to share.
     */
    public void share(final String message, final String imageUri) {
        System.out.println(TAG + "share(): \"" + message + "\", " + imageUri);
        
        if ((message == null || message.length() == 0)
            && (imageUri == null || imageUri.length() == 0))
        {
            if (listener != null) {
                listener.onError("Nothing to share! Note that image sharing is not implemented.");
            }
            
            return;
        }
         
        if (!isValid(StatusShoutData.getInstance().getFacebookToken())) {
            // No valid token. Save the message to be sent after authentication
            // and authenticate first.
            messageToSend = message;
            imageToShare = imageUri;
            authenticate();
        }
        else {
            doShare(message, imageUri);
        }
    }

    /**
     * Shares the given message to Facebook feed. Note that this method does not
     * validate the message content nor the token.
     * 
     * See https://developers.facebook.com/docs/reference/api/publishing/ for
     * more information on publishing in Facebook.
     * 
     * @param message The message to share.
     * @param imageUri The image to share.
     */
    private void doShare(String message, final String imageUri) {
        if (listener != null) {
            listener.onSending();
        }
        
        final boolean hasImageToShare = (imageUri != null && imageUri.length() > 0);
        final String accessToken = StatusShoutData.getInstance().getFacebookToken();
        StringBuffer sb = new StringBuffer();
        sb.append("access_token=").append(accessToken);
        
        if (message != null) {
            sb.append("&message=");
            
            if (hasImageToShare) {
                // We are putting the message into the URL and thus, we need to
                // replace any space characters with "%20".
                for (int i = 0; i < message.length(); ++i) {
                    if (message.charAt(i) == ' ') {
                        sb.append("%20");
                    }
                    else {
                        sb.append(message.charAt(i));
                    }
                }
            }
            else {
                sb.append(message);
            }
        }
        
        final String content = sb.toString();
        final String contentLength = String.valueOf(content.getBytes().length);
        
        new Thread() {
            public void run() {
                String url = GRAPH_URL;
                
                if (hasImageToShare) {
                    url += "me/photos?" + content;
                    System.out.println(TAG + "doShare(): URL: " + url);
                }
                else {
                    url += "me/feed";
                    System.out.println(TAG + "doShare(): URL: " + url);
                    System.out.println(TAG + "doShare(): Content: " + content);
                }
                
                HttpConnection connection = null;
                ByteArrayOutputStream bos = null;
                
                try {
                    connection = (HttpConnection) Connector.open(url, Connector.READ_WRITE);
                    
                    if (connection != null) {
                        connection.setRequestMethod(HttpConnection.POST);
                        
                        if (hasImageToShare) {
                            connection.setRequestProperty("Content-type",
                                    "multipart/form-data; boundary=" + BOUNDARY);
                            bos = new ByteArrayOutputStream();
                            bos.write(getBoundaryMessage(imageUri, "image/jpeg").getBytes());
                            bos.write(ImageUtils.localImageToByteArray(imageUri));
                            bos.write(new String("\r\n--" + BOUNDARY + "--\r\n").getBytes());
                        }
                        else {
                            System.out.println(TAG + "doShare(): Content length: " + contentLength);
                            connection.setRequestProperty("Content-length", contentLength);
                        }
                        
                    }
                }
                catch (IOException e) {
                    System.out.println(TAG + "doShare(): " + e.toString());
                    
                    if (listener != null) {
                        listener.onError("Failed to open connection: " + e.toString());
                    }
                }
                catch (SecurityException e) {
                    // User did not allow accessing network
                    connection = null;
                }
                catch (Exception e) {
                    if (listener != null) {
                        listener.onError("Failed to open connection: " + e.toString());
                    }
                    
                    connection = null;
                }
                
                int responseCode = HttpConnection.HTTP_UNAVAILABLE;
                
                if (connection != null) {
                    try {
                        OutputStream outputStream = connection.openDataOutputStream();
                        
                        if (outputStream != null) {
                            if (hasImageToShare) {
                                outputStream.write(bos.toByteArray());
                            }
                            else {
                                outputStream.write(content.getBytes());
                            }
                            
                            outputStream.flush();
                            
                            if (bos != null) {
                                bos.close();
                            }
                            
                            outputStream.close();
                            responseCode = connection.getResponseCode();
                            System.out.println(TAG + "doShare(): Response code: " + responseCode);
                        }
                    }
                    catch (IOException e) {
                        System.out.println(TAG + "doShare(): " + e.toString());
                        
                        if (listener != null) {
                            listener.onError("Failed to post content: " + e.toString());
                        }
                    }
                    catch (SecurityException e) { 
                    }
                    catch (NullPointerException e) {
                        // This may happen if permission is denied by user
                    }
                    
                    // Get the response message
                    DataInputStream inputStream = null;
                    String responseMessage = new String();
                    
                    try {
                        inputStream = new DataInputStream(connection.openInputStream());
                        int ch;
                        
                        while ((ch = inputStream.read()) != -1) {
                            responseMessage += ((char) ch);
                        }
                            
                        System.out.println(TAG + "doShare(): Response message : " + responseMessage);
                        inputStream.close();
                    }
                    catch (Exception e) {
                        System.out.println(TAG + "doShare(): " + e.toString());
                    }
                    
                    if (listener != null) {
                        if (responseCode == HttpConnection.HTTP_OK) {
                            listener.onSuccess("Message posted successfully!");
                        }
                        else {
                            listener.onError("Failed to share to Facebook. Code "
                                    + responseCode + ", message: "
                                    + parseResponseMessage(responseMessage));
                        }
                    }
                }
                
                try {
                    connection.close();
                }
                catch (Exception e) {
                }
            }
        }.start();
    }
 
    /**
     * Parses the authentication token from the given string.
     * 
     * @param input The string containing the token.
     * @return The authentication token.
     */
    private String parseToken(String input) {
        System.out.println(TAG + "parseToken(): " + input);
        
        if (input == null) {
            return null;
        }
        
        int start = 0;
        int end = 0;
        start = input.indexOf(ACCESS_TOKEN_STRING);
        
        if (start == -1) {
            return null;
        }
        else {
            start += ACCESS_TOKEN_STRING.length();
        }
        
        end = input.indexOf("&", start);
        String token = null;
        
        try {
            token = input.substring(start, (end != -1) ? end : input.length());
        }
        catch (StringIndexOutOfBoundsException e) {
        }
        
        return token;
    }

    /**
     * Creates a boundary message (for sharing an image in the Facebook).
     * 
     * @param imageUri The URI of the image to share.
     * @param mimeType The MIME type of the image.
     * @return The boundary message as a String.
     */
    private String getBoundaryMessage(final String imageUri, final String mimeType) {
        // Crop the filename
        final int index = imageUri.lastIndexOf('/') + 1; 
        final String filename = imageUri.substring(index);
        
        StringBuffer sb = new StringBuffer("--").append(BOUNDARY).append("\r\n"); 
        sb.append("Content-Disposition: form-data; name=\"").append("upload_field")
          .append("\"; filename=\"").append(filename).append("\"\r\n")  
          .append("Content-Type: ").append(mimeType).append("\r\n\r\n"); 
        
        return sb.toString(); 
    }

    /**
     * Parses and returns the message content from the given response. 
     * @param response The response.
     * @return The message content.
     */
    private String parseResponseMessage(final String response) {
        if (response == null) {
            return null;
        }
        
        final int startIndex = response.indexOf("message\":\"") + 10;
        final int endIndex = response.indexOf('"', startIndex);
        String message = null;
        
        try {
            message = response.substring(startIndex, endIndex);
        }
        catch (StringIndexOutOfBoundsException e) {
            message = "(n/a)";
        }
        
        return message;
    }
}