com.google.devrel.mobiledevicelab.spi.helper.DeviceLabApiHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devrel.mobiledevicelab.spi.helper.DeviceLabApiHelper.java

Source

/**
 * Copyright 2014 Google Inc. All Rights Reserved.
 * 
 * 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.devrel.mobiledevicelab.spi.helper;

import static com.google.devrel.mobiledevicelab.service.OfyService.ofy;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

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

import com.google.api.server.spi.response.NotFoundException;
import com.google.api.server.spi.response.UnauthorizedException;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.devrel.mobiledevicelab.Constants;
import com.google.devrel.mobiledevicelab.domain.Device;
import com.google.devrel.mobiledevicelab.domain.LabOwner;
import com.google.devrel.mobiledevicelab.domain.Url;
import com.google.devrel.mobiledevicelab.form.DeviceEditForm;
import com.google.devrel.mobiledevicelab.form.DeviceForm;
import com.google.devrel.mobiledevicelab.form.PushUrlForm;
import com.google.devrel.mobiledevicelab.form.ResponseForm;
import com.google.devrel.mobiledevicelab.spi.DeviceLabApi;
import com.googlecode.objectify.Key;

/**
 * 
 * Static methods used by {@link DeviceLabApi}
 *
 */
public class DeviceLabApiHelper {

    private static final Logger log = Logger.getLogger(DeviceLabApiHelper.class.getName());

    /**
     * Creates a Device object, and adds the user as a lab owner if required.
     *
     * @param deviceForm A DeviceForm object sent from the client form.
     * @return Device object just created
     * @throws UnauthorizedException if the given access token and user email do
     *         not match, or we fail to send GCM message
     */
    public static Device saveDevice(final DeviceForm deviceForm) throws UnauthorizedException {
        // Verify token is valid and is for the given user id
        validateTokenForUserId(getTokenJsonFromAccessToken(deviceForm.getToken()), deviceForm.getUserId());

        if (deviceForm.getPlatformId() == 0) {
            // Verify GCM id
            String GCMError = pushUrlToDevice(deviceForm.getCloudMsgId(), "test", "test");
            if (GCMError != null) {
                throw new UnauthorizedException("GCMError");
            }

            LabOwner labOwner = getLabOwner(deviceForm);

            // Check we don't have device already (using cloudMsgId for matching)
            List<Device> devices = ofy().load().type(Device.class).filter("gcmId", deviceForm.getCloudMsgId())
                    .list();
            if (devices.size() > 0) {
                ofy().delete().key(Key.create(Device.class, devices.get(0).getId())).now();

            }
            Device device = new Device(deviceForm.getDeviceName(), deviceForm.getPlatformVersion(),
                    deviceForm.getCloudMsgId(), labOwner.getGroupId());
            ofy().save().entity(device).now();
            return device;
        } else {
            LabOwner labOwner = getLabOwner(deviceForm);

            String token = createChannel(deviceForm);

            Device device = new Device(deviceForm.getDeviceName(), deviceForm.getBrowserClientId(),
                    labOwner.getGroupId(), token);
            ofy().save().entity(device).now();

            ConnectionChannelApiHelper.pushUpdateDevicesToChannel(labOwner);

            return device;
        }
    }

    /**
     * This gets the lab owner, or create it if new
     * 
     * @param deviceForm A DeviceForm object sent from the client form.
     * @return the lab owner
     */
    private static LabOwner getLabOwner(final DeviceForm deviceForm) {
        ofy().clear();
        LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, deviceForm.getUserId())).now();
        if (labOwner == null) {
            List<LabOwner> owners = ofy().load().type(LabOwner.class).order("groupId").list();
            int newGroupId = 0;
            for (LabOwner owner : owners) {
                if (owner.getGroupId() >= newGroupId) {
                    newGroupId = owner.getGroupId() + 1;
                }
            }
            labOwner = new LabOwner(deviceForm.getUserId(), newGroupId);
            ofy().save().entity(labOwner).now();
        }
        return labOwner;
    }

    /**
     * Creates a channel for the device as given in the deviceForm and returns the
     * token associated with the channel
     * 
     * @param deviceForm A DeviceForm object sent from the client form.
     * @return the token for the newly created channel
     */
    private static String createChannel(final DeviceForm deviceForm) {
        return createChannel(deviceForm.getBrowserClientId());
    }

    /**
     * Creates a channel for the given clientId and returns the token associated
     * with the channel
     * 
     * @param clientId The clientId to create the channnel for
     * @return the token for the newly created channel
     */
    private static String createChannel(final String clientId) {
        ChannelService channelService = ChannelServiceFactory.getChannelService();
        return channelService.createChannel(clientId);
    }

    /**
     * Amends the name of a device
     *
     * @param deviceForm A DeviceEditForm object sent from the client form.
     * @return Device object amended
     * @throws NotFoundException if the device isn't found
     * @throws UnauthorizedException if the given access token and user email do
     *         not match, or we fail to send GCM message
     */
    public static Device editDevice(DeviceEditForm deviceForm) throws UnauthorizedException, NotFoundException {
        validateTokenForUserId(getTokenJsonFromAccessToken(deviceForm.getToken()), deviceForm.getUserId());
        Device device = ofy().load().key(Key.create(Device.class, deviceForm.getDeviceId())).now();
        ofy().clear();
        if (device != null) {
            LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, deviceForm.getUserId())).now();
            if (labOwner != null && labOwner.getGroupId() == device.getGroupId()) {
                device.update(deviceForm.getDeviceName());
                ofy().save().entity(device).now();
                ConnectionChannelApiHelper.pushNameToChannel(device.getBrowserClientId(), device.getDeviceName());

                return device;
            } else {
                throw new UnauthorizedException("The device doesn't belong to the group of the lab owner!");
            }
        } else {
            throw new NotFoundException("Device with id " + deviceForm.getDeviceId() + " not found");
        }
    }

    /**
     * This gets all the devices linked to the given lab owner
     *
     * @param accessToken The G+ access token for the lab owner
     * @param userId The G+ user id for the lab owner
     * @return List of matching Devices
     * @throws UnauthorizedException if the given access token and user email do
     *         not match
     */
    public static List<Device> getDevices(final String accessToken, final String userId)
            throws UnauthorizedException {
        validateTokenForUserId(getTokenJsonFromAccessToken(accessToken), userId);
        List<Device> devices = new ArrayList<Device>();
        LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, userId)).now();
        if (labOwner != null) {
            ConnectionChannelApiHelper.cleanUpDisconnectedDevices(labOwner.getGroupId());
            devices = ofy().load().type(Device.class).filter("groupId", labOwner.getGroupId()).list();
        }

        return devices;
    }

    /**
     * This registers the browser of the lab owner to receive updates
     *
     * @param accessToken The G+ access token for the lab owner
     * @param userId The G+ user id for the lab owner
     * @param clientId The clientId for the browser, to use for the Channel API
     *        for automatic refreshing of devices
     * @return The token the browser can then use to connect to the Channel API
     * @throws UnauthorizedException if the given access token and user email do
     *         not match
     */
    public static ResponseForm registerBrowserForUpdates(final String accessToken, final String userId,
            final String clientId) throws UnauthorizedException {
        ofy().clear();
        validateTokenForUserId(getTokenJsonFromAccessToken(accessToken), userId);
        log.info("Register Browser with id " + clientId);
        LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, userId)).now();
        if (!labOwner.containsClientId(clientId)) {
            labOwner.addClientId(clientId);
            ofy().save().entity(labOwner).now();
            log.info("Client Ids " + labOwner.getClientIdsStr());
        }
        String token = createChannel(clientId);
        return new ResponseForm(null, true, token);
    }

    /**
     * This deletes the device with the given id
     *
     * @return status 200 with success = true if successful
     * @throws NotFoundException if the device with the given id is not found
     * @throws UnauthorizedException if the given access token and user email do
     *         not match, or the device doesn't belong to the group of the given
     *         user
     */
    public static ResponseForm deleteDevice(final String accessToken, final String userId, final long deviceId)
            throws NotFoundException, UnauthorizedException {
        validateTokenForUserId(getTokenJsonFromAccessToken(accessToken), userId);
        Device device = ofy().load().key(Key.create(Device.class, deviceId)).now();
        if (device != null) {
            LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, userId)).now();
            if (labOwner != null && labOwner.getGroupId() == device.getGroupId()) {
                ConnectionChannelApiHelper.pushDisconnectToChannel(device.getBrowserClientId());
                ofy().delete().key(Key.create(Device.class, deviceId)).now();

                return new ResponseForm("", true, "Device with id " + deviceId + " deleted");
            } else {
                throw new UnauthorizedException("The device doesn't belong to the group of the lab owner!");
            }
        } else {
            throw new NotFoundException("Device with id " + deviceId + " not found");
        }
    }

    /**
     * This pushes a url to all Android devices associated with the group of the
     * user whose G+ token is provided
     *
     * @param pushForm A PushUrlForm object sent from the client form.
     * @return status 200 if successful, with the number of devices it was pushed
     *         to
     * @throws UnauthorizedException if the given access token and user email do
     *         not match
     */
    public static ResponseForm pushUrl(final PushUrlForm pushUrlForm) throws UnauthorizedException {
        // Verify token is valid and is for the given user id
        validateTokenForUserId(getTokenJsonFromAccessToken(pushUrlForm.getToken()), pushUrlForm.getUserId());

        // Get list of devices
        List<Device> devices = new ArrayList<Device>();
        int androidDevicesSuccessfullyPushedTo = 0;
        int nonAndroidDevicesSuccessfullyPushedTo = 0;
        ofy().clear();
        LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, pushUrlForm.getUserId())).now();
        String error = "";
        if (labOwner != null) {
            devices = ofy().load().type(Device.class).filter("groupId", labOwner.getGroupId()).list();
            if (devices.size() > 0) {
                for (Device device : devices) {
                    String browser = pushUrlForm.getBrowserPackageNameForDeviceId(device.getId());
                    if (browser != null && device.getPlatformId() == 0) {
                        String errorPushing = pushUrlToDevice(device.getGcmId(), pushUrlForm.getUrl(), browser);
                        if (errorPushing == null) {
                            androidDevicesSuccessfullyPushedTo += 1;
                        } else {
                            error += errorPushing + " for device with id " + device.getId() + "\n";
                        }
                    } else if (pushUrlForm.isDeviceInList(device.getId()) && device.getPlatformId() == 1) {
                        boolean channelPushSuccess = ConnectionChannelApiHelper.pushUrlToChannel(pushUrlForm,
                                device);
                        if (channelPushSuccess) {
                            nonAndroidDevicesSuccessfullyPushedTo += 1;
                        } else {
                            error += "Device with name " + device.getDeviceName() + "appears to be disconnected\n";
                        }
                    } else if (browser == null && pushUrlForm.isDeviceInList(device.getId())
                            && device.getPlatformId() == 0) {
                        error += "No package name provided for Android device " + device.getDeviceName() + "\n";
                    }
                }
            }
        }
        log.info("Android devices pushed to: " + androidDevicesSuccessfullyPushedTo);
        log.info("Non Android devices pushed to: " + nonAndroidDevicesSuccessfullyPushedTo);
        ConnectionChannelApiHelper.cleanUpDisconnectedDevices(labOwner.getGroupId());
        ConnectionChannelApiHelper.cleanUpDisconnectedLabs(labOwner);

        List<Url> urls = ofy().load().type(Url.class).filter("groupId", labOwner.getGroupId())
                .filter("url", pushUrlForm.getUrl()).list();
        if (urls.size() > 0) {
            urls.get(0).urlPushed();
        } else {
            Url url = new Url(pushUrlForm.getUrl(), labOwner.getGroupId());
            urls.add(url);
        }
        ofy().save().entity(urls.get(0)).now();

        return new ResponseForm(error, error.equals(""),
                "Url pushed to " + androidDevicesSuccessfullyPushedTo + " Android "
                        + (androidDevicesSuccessfullyPushedTo > 1 ? "devices" : "device") + " and to "
                        + nonAndroidDevicesSuccessfullyPushedTo + " non Android "
                        + (nonAndroidDevicesSuccessfullyPushedTo > 1 ? "devices" : "device"));
    }

    /**
     * This gets all the urls linked to the given lab owner
     *
     * @return List of matching Url
     * @throws UnauthorizedException if the given access token and user email do
     *         not match
     */
    public static List<Url> getUrls(final String accessToken, final String userId) throws UnauthorizedException {
        validateTokenForUserId(getTokenJsonFromAccessToken(accessToken), userId);
        List<Url> urls = new ArrayList<Url>();
        LabOwner labOwner = ofy().load().key(Key.create(LabOwner.class, userId)).now();
        if (labOwner != null) {
            urls = ofy().load().type(Url.class).filter("groupId", labOwner.getGroupId()).order("-lastUsed").list();
        }
        return urls;
    }

    /**
     * Calls Google account API to get the token information
     * 
     * @param accessToken
     * @return the JSON sent back by Google account API
     * @throws UnauthorizedException
     */
    private static String getTokenJsonFromAccessToken(String accessToken) throws UnauthorizedException {
        String tokenJson = "";
        try {
            URL url = new URL("https://accounts.google.com/o/oauth2/tokeninfo?access_token=" + accessToken);
            BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
            String line;

            while ((line = reader.readLine()) != null) {
                tokenJson += line + "\n";
            }
            reader.close();
        } catch (MalformedURLException e) {
            throw new UnauthorizedException("We couldn't verify the access token with Google");
        } catch (IOException e) {
            throw new UnauthorizedException("We couldn't verify the access token with Google");
        }
        if (tokenJson.contains("invalid_token")) {
            throw new UnauthorizedException("The access token is invalid, please refresh the page");
        }
        return tokenJson;
    }

    private static void validateTokenForUserId(String tokenJson, String userId) throws UnauthorizedException {
        if (!tokenJson.contains(userId)) {
            throw new UnauthorizedException("We couldn't verify the user id with Google");
        }
    }

    /**
     * @param gcmId The registration id obtained from GCM service on the device
     * @param urlStr The url to push to the device
     * @param browserPackageName The package name for the browser to push the url
     *        to on the device
     * @return null if success, or error message. Error message can be one of the
     *         following: - as directly received from GCM server, - status code
     *         received from GCm server if not 200 - one of the following
     *         exceptions: JSONException, UnsupportedEncodingException,
     *         MalformedURLException, and IOException
     */
    private static String pushUrlToDevice(String gcmId, String urlStr, String browserPackageName) {
        try {
            JSONObject json = new JSONObject();
            JSONArray array = new JSONArray().put(gcmId);
            json.put("registration_ids", array);
            JSONObject data = new JSONObject();
            data.put("url", urlStr);
            data.put("pkg", browserPackageName);
            json.put("data", data);
            String jsonString = json.toString();
            log.info("JSON payload: " + jsonString);

            com.google.appengine.api.urlfetch.HTTPResponse response;
            URL url;
            HTTPRequest httpRequest;
            String GCM_URL = "https://android.googleapis.com/gcm/send";
            url = new URL(GCM_URL);
            httpRequest = new HTTPRequest(url, HTTPMethod.POST);
            httpRequest.addHeader(new HTTPHeader("Content-Type", "application/json"));
            log.info("ApiKey: " + Constants.API_KEY);
            httpRequest.addHeader(new HTTPHeader("Authorization", "key=" + Constants.API_KEY));
            httpRequest.setPayload(jsonString.getBytes("UTF-8"));
            log.info("Sending POST request to: " + GCM_URL);

            response = URLFetchServiceFactory.getURLFetchService().fetch(httpRequest);
            String responseString = null;
            log.info("Status: " + response.getResponseCode());
            if (response.getContent() != null) {
                responseString = new String(response.getContent(), "UTF-8");
                log.info("Response " + responseString);
            }
            String GCMError = getGCMError(responseString);
            if (response.getResponseCode() == 200 && GCMError == null) {
                return null;
            } else if (response.getResponseCode() == 200 && GCMError != null) {
                return GCMError;
            } else {
                return "Status code: " + response.getResponseCode();
            }

        } catch (JSONException e1) {
            log.info("JSONException" + e1.getMessage());
        } catch (UnsupportedEncodingException e1) {
            log.info("UnsupportedEncodingException" + e1.getMessage());
        } catch (MalformedURLException e1) {
            log.info("MalformedURLException" + e1.getMessage());
        } catch (IOException e1) {
            log.info("IOException" + e1.getMessage());
        }
        return "Exception thrown - see logs";
    }

    /**
     * @param responseFromGCM The string response as received from GCM server
     * @return the error string returned by GCM server, or null if no error
     */
    private static String getGCMError(String responseFromGCM) {
        JSONObject response = new JSONObject(responseFromGCM);
        if (response.has("results")) {
            JSONArray array = response.getJSONArray("results");
            if (array.getJSONObject(0).has("error")) {
                return array.getJSONObject(0).getString("error");
            }
        }
        return null;
    }

}