Java tutorial
/*********************************************************************** * * Slimgress: Ingress API for Android * Copyright (C) 2013 Norman Link <norman.link@gmx.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * ***********************************************************************/ package com.norman0406.slimgress.API.Interface; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Date; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.zip.GZIPInputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.params.ClientPNames; import org.apache.http.cookie.Cookie; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.norman0406.slimgress.API.Common.Location; import android.os.Build; import android.util.Log; public class Interface { public enum AuthSuccess { Successful, TokenExpired, UnknownError } private DefaultHttpClient mClient; private String mCookie; // ingress api definitions private static final String mApiVersion = "2013-08-07T00:06:39Z a52083df5202 opt"; private static final String mApiBase = "m-dot-betaspike.appspot.com"; private static final String mApiBaseURL = "https://" + mApiBase + "/"; private static final String mApiLogin = "_ah/login?continue=http://localhost/&auth="; private static final String mApiHandshake = "handshake?json="; private static final String mApiRequest = "rpc/"; public Interface() { mClient = new DefaultHttpClient(); } public AuthSuccess authenticate(final String token) { FutureTask<AuthSuccess> future = new FutureTask<AuthSuccess>(new Callable<AuthSuccess>() { @Override public AuthSuccess call() throws Exception { // see http://blog.notdot.net/2010/05/Authenticating-against-App-Engine-from-an-Android-app // also use ?continue= (?) String login = mApiBaseURL + mApiLogin + token; HttpGet get = new HttpGet(login); try { HttpResponse response = null; synchronized (Interface.this) { mClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false); Log.i("Interface", "executing authentication"); response = mClient.execute(get); } assert (response != null); @SuppressWarnings("unused") String content = EntityUtils.toString(response.getEntity()); response.getEntity().consumeContent(); if (response.getStatusLine().getStatusCode() == 401) { // the token has expired Log.i("Interface", "401: authentication token has expired"); return AuthSuccess.TokenExpired; } else if (response.getStatusLine().getStatusCode() != 302) { // Response should be a redirect Log.i("Interface", "unknown error: " + response.getStatusLine().getReasonPhrase()); return AuthSuccess.UnknownError; } else { // get cookie synchronized (Interface.this) { for (Cookie cookie : mClient.getCookieStore().getCookies()) { if (cookie.getName().equals("SACSID")) { // secure cookie! (ACSID is non-secure http cookie) mCookie = cookie.getValue(); } } } if (mCookie == null) { Log.i("Interface", "authentication token has expired"); return AuthSuccess.TokenExpired; } Log.i("Interface", "authentication successful"); return AuthSuccess.Successful; } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { synchronized (Interface.this) { mClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true); } } return AuthSuccess.Successful; } }); // start thread new Thread(future).start(); // obtain authentication return value AuthSuccess retVal = AuthSuccess.UnknownError; try { retVal = future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return retVal; } public void handshake(final Handshake.Callback callback) { new Thread(new Runnable() { @Override public void run() { JSONObject params = new JSONObject(); try { // set handshake parameters params.put("nemesisSoftwareVersion", mApiVersion); params.put("deviceSoftwareVersion", Build.VERSION.RELEASE); // TODO: /*params.put("activationCode", ""); params.put("tosAccepted", "1"); params.put("a", "");*/ String paramString = params.toString(); paramString = URLEncoder.encode(paramString, "UTF-8"); String handshake = mApiBaseURL + mApiHandshake + paramString; HttpGet get = new HttpGet(handshake); get.setHeader("Accept-Charset", "utf-8"); get.setHeader("Cache-Control", "max-age=0"); // do handshake HttpResponse response = null; synchronized (Interface.this) { Log.i("Interface", "executing handshake"); response = mClient.execute(get); } assert (response != null); HttpEntity entity = response.getEntity(); if (entity != null) { String content = EntityUtils.toString(entity); Header contentType = entity.getContentType(); entity.consumeContent(); // check for content type json if (!contentType.getName().equals("Content-Type") || !contentType.getValue().contains("application/json")) throw new RuntimeException("content type is not json"); content = content.replace("while(1);", ""); // handle handshake data callback.handle(new Handshake(new JSONObject(content))); Log.i("Interface", "handshake finished"); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } } }).start(); } public void request(final Handshake handshake, final String requestString, final Location playerLocation, final JSONObject requestParams, final RequestResult result) throws InterruptedException { if (!handshake.isValid() || handshake.getXSRFToken().length() == 0) throw new RuntimeException("handshake is not valid"); new Thread(new Runnable() { public void run() { // create post String postString = mApiBaseURL + mApiRequest + requestString; HttpPost post = new HttpPost(postString); // set additional parameters JSONObject params = new JSONObject(); if (requestParams != null) { if (requestParams.has("params")) params = requestParams; else { try { params.put("params", requestParams); // add persistent request parameters if (playerLocation != null) { String loc = String.format("%08x,%08x", playerLocation.getLatitude(), playerLocation.getLongitude()); params.getJSONObject("params").put("playerLocation", loc); params.getJSONObject("params").put("location", loc); } params.getJSONObject("params").put("knobSyncTimestamp", getCurrentTimestamp()); JSONArray collectedEnergy = new JSONArray(); // TODO: add collected energy guids params.getJSONObject("params").put("energyGlobGuids", collectedEnergy); } catch (JSONException e) { e.printStackTrace(); } } } else { try { params.put("params", null); } catch (JSONException e) { e.printStackTrace(); } } try { StringEntity entity = new StringEntity(params.toString(), "UTF-8"); entity.setContentType("application/json"); post.setEntity(entity); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // set header post.setHeader("Content-Type", "application/json;charset=UTF-8"); post.setHeader("Accept-Encoding", "gzip"); post.setHeader("User-Agent", "Nemesis (gzip)"); post.setHeader("X-XsrfToken", handshake.getXSRFToken()); post.setHeader("Host", mApiBase); post.setHeader("Connection", "Keep-Alive"); post.setHeader("Cookie", "SACSID=" + mCookie); // execute and get the response. try { HttpResponse response = null; String content = null; synchronized (Interface.this) { response = mClient.execute(post); assert (response != null); if (response.getStatusLine().getStatusCode() == 401) { // token expired or similar //isAuthenticated = false; response.getEntity().consumeContent(); } else { HttpEntity entity = response.getEntity(); // decompress gzip if necessary Header contentEncoding = entity.getContentEncoding(); if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) content = decompressGZIP(entity); else content = EntityUtils.toString(entity); entity.consumeContent(); } } // handle request result if (content != null) { JSONObject json = new JSONObject(content); RequestResult.handleRequest(json, result); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } } }).start(); } private static String decompressGZIP(HttpEntity compressedEntity) throws IOException { final int bufferSize = 8192; InputStream input = compressedEntity.getContent(); GZIPInputStream gzipStream = new GZIPInputStream(input, bufferSize); StringBuilder string = new StringBuilder(); byte[] data = new byte[bufferSize]; int bytesRead; while ((bytesRead = gzipStream.read(data)) != -1) { string.append(new String(data, 0, bytesRead)); } gzipStream.close(); input.close(); return string.toString(); } private long getCurrentTimestamp() { return (new Date()).getTime(); } }