/* Copyright 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.mobilyzer; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; import android.os.AsyncTask; import org.apache.http.HttpVersion; import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.cookie.Cookie; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.HTTP; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.mobilyzer.gcm.GCMManager; import com.mobilyzer.util.Logger; import com.mobilyzer.util.MeasurementJsonConvertor; import com.mobilyzer.util.PhoneUtils; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; import java.util.Vector; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * Handles checkins with the server. */ public class Checkin { private static final int POST_TIMEOUT_MILLISEC = 20 * 1000; private Context context; private Date lastCheckin; private volatile Cookie authCookie = null; private AccountSelector accountSelector = null; PhoneUtils phoneUtils; String gcm_registraion_id; public Checkin(Context context) { phoneUtils = PhoneUtils.getPhoneUtils(); this.context = context; this.gcm_registraion_id=""; } /** Shuts down the checkin thread */ public void shutDown() { if (this.accountSelector != null) { this.accountSelector.shutDown(); } } /** Return a fake authentication cookie for a test server instance */ private Cookie getFakeAuthCookie() { BasicClientCookie cookie = new BasicClientCookie( "dev_appserver_login", "[email protected]:False:185804764220139124118"); cookie.setDomain(".google.com"); cookie.setVersion(1); cookie.setPath("/"); cookie.setSecure(false); return cookie; } public Date lastCheckinTime() { return this.lastCheckin; } public List<MeasurementTask> checkin(ResourceCapManager resourceCapManager, GCMManager gcm) throws IOException { Logger.i("Checkin.checkin() called"); boolean checkinSuccess = false; gcm_registraion_id=gcm.getRegistrationId(); try { JSONObject status = new JSONObject(); DeviceInfo info = phoneUtils.getDeviceInfo(); // TODO(Wenjie): There is duplicated info here, such as device ID. status.put("id", info.deviceId); status.put("manufacturer", info.manufacturer); status.put("model", info.model); status.put("os", info.os); /** * TODO: checkin task don't belongs to any app. So we just fill * request_app field with server task key */ DeviceProperty deviceProperty=phoneUtils.getDeviceProperty(Config.CHECKIN_KEY); deviceProperty.setRegistrationId(gcm.getRegistrationId()); Logger.d("Checkin-> GCMManager: "+gcm.getRegistrationId()); status.put("properties", MeasurementJsonConvertor.encodeToJson(deviceProperty)); if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { resourceCapManager.updateDataUsage(ResourceCapManager.PHONEUTILCOST); } Logger.d(status.toString()); Logger.d("Checkin: "+status.toString()); String result = serviceRequest("checkin", status.toString()); Logger.d("Checkin result: " + result); if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { resourceCapManager.updateDataUsage(result.length()); } // Parse the result Vector<MeasurementTask> schedule = new Vector<MeasurementTask>(); JSONArray jsonArray = new JSONArray(result); for (int i = 0; i < jsonArray.length(); i++) { Logger.d("Parsing index " + i); JSONObject json = jsonArray.optJSONObject(i); Logger.d("Value is " + json); // checkin task must support if (json != null && MeasurementTask.getMeasurementTypes().contains(json.get("type"))) { try { MeasurementTask task = MeasurementJsonConvertor.makeMeasurementTaskFromJson(json); Logger.i(MeasurementJsonConvertor.toJsonString(task.measurementDesc)); schedule.add(task); } catch (IllegalArgumentException e) { Logger.w("Could not create task from JSON: " + e); // Just skip it, and try the next one } } } this.lastCheckin = new Date(); Logger.i("Checkin complete, got " + schedule.size() + " new tasks"); checkinSuccess = true; return schedule; } catch (JSONException e) { Logger.e("Got exception during checkin", e); throw new IOException("There is exception during checkin()"); } catch (IOException e) { Logger.e("Got exception during checkin", e); throw e; } finally { if (!checkinSuccess) { // Failure probably due to authToken expiration. Will authenticate upon next checkin. this.accountSelector.setAuthImmediately(true); this.authCookie = null; } } } /** * Read in the results of tasks completed to date from a file, then clear the file. * * @return The results as a JSONArray, ready for sending to the server. */ private synchronized JSONArray readResultsFromFile() { JSONArray results = new JSONArray(); try { Logger.d("Loading results from disk: "+context.getFilesDir()); FileInputStream inputstream = context.openFileInput("results"); InputStreamReader streamreader = new InputStreamReader(inputstream); BufferedReader bufferedreader = new BufferedReader(streamreader); String line; int count = 0; while ((line = bufferedreader.readLine()) != null) { JSONObject jsonTask; try { jsonTask = new JSONObject(line); count++; results.put(jsonTask); } catch (JSONException e) { Logger.e("", e); } } Logger.i("Got " + count + " results from file"); bufferedreader.close(); streamreader.close(); inputstream.close(); // delete file once done, to avoid uploading results twice context.deleteFile("results"); } catch (FileNotFoundException e) { Logger.e("", e); } catch (IOException e) { Logger.e("", e); } return results; } public void uploadMeasurementResult(Vector<MeasurementResult> finishedTasks, ResourceCapManager resourceCapManager) throws IOException { JSONArray resultArray = readResultsFromFile(); for (MeasurementResult result : finishedTasks) { try { resultArray.put(MeasurementJsonConvertor.encodeToJson(result)); } catch (JSONException e1) { Logger.e("Error when adding " + result); } } JSONArray chunckedArray= new JSONArray(); int i=0; for (;i<resultArray.length();i++){ try { chunckedArray.put(resultArray.getJSONObject(i)); } catch (JSONException e) { Logger.e("Error when adding index " +i + " to array"); } if((i+1)%100==0){ Logger.d("uploading "+chunckedArray.length()+" measurements"); uploadChunkedArray(chunckedArray, resourceCapManager); chunckedArray= new JSONArray(); } } if(i%100!=0){ Logger.d("uploading "+chunckedArray.length()+" measurements"); uploadChunkedArray(chunckedArray, resourceCapManager); } Logger.i("TaskSchedule.uploadMeasurementResult() complete"); } private void uploadChunkedArray(JSONArray resultArray, ResourceCapManager resourceCapManager) throws IOException { Logger.i("uploadChunkedArray uploading: " + resultArray.toString()); if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { resourceCapManager.updateDataUsage(resultArray.toString().length()); } String response = serviceRequest("postmeasurement", resultArray.toString()); try { JSONObject responseJson = new JSONObject(response); if (!responseJson.getBoolean("success")) { throw new IOException("Failure posting measurement result"); } } catch (JSONException e) { throw new IOException(e.getMessage()); } } class NotUIBlockingResultUploader extends AsyncTask<String , Void, String> { @Override protected String doInBackground(String... results) { if(results.length!=1){ return ""; } String r=results[0]; String response=""; try { response=serviceRequest("postmeasurement", r); } catch (IOException e) { Logger.e("Failed to upload local event: "+e.getMessage()); } return response; } } public void uploadSingleMeasurementResult(MeasurementResult result, ResourceCapManager resourceCapManager) throws IOException, InterruptedException, ExecutionException { result.getDeviceProperty().registrationId=gcm_registraion_id; try { JSONArray resultArray= new JSONArray(); resultArray.put(MeasurementJsonConvertor.encodeToJson(result)); Logger.d("Single Measurement result converted to json: "+resultArray.toString()); if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { resourceCapManager.updateDataUsage(resultArray.toString().length()); } String response=new NotUIBlockingResultUploader().execute(resultArray.toString()).get(); // String response = serviceRequest("postmeasurement", resultJson.toString()); try { JSONObject responseJson = new JSONObject(response); if (!responseJson.getBoolean("success")) { throw new IOException("Failure posting single measurement result"); } } catch (JSONException e) { throw new IOException(e.getMessage()); } } catch (JSONException e1) { Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete"); } Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete"); } /** * Used to generate SSL sockets. */ class MySSLSocketFactory extends SSLSocketFactory { SSLContext sslContext = SSLContext.getInstance("TLS"); public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { super(truststore); X509TrustManager tm = new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // Do nothing } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // Do nothing } }; sslContext.init(null, new TrustManager[] { tm }, null); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException { return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); } @Override public Socket createSocket() throws IOException { return sslContext.getSocketFactory().createSocket(); } } /** * Return an appropriately-configured HTTP client. */ private HttpClient getNewHttpClient() { DefaultHttpClient client; try { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null, null); SSLSocketFactory sf = new MySSLSocketFactory(trustStore); sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); HttpParams params = new BasicHttpParams(); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); HttpConnectionParams.setConnectionTimeout(params, POST_TIMEOUT_MILLISEC); HttpConnectionParams.setSoTimeout(params, POST_TIMEOUT_MILLISEC); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory .getSocketFactory(), 80)); registry.register(new Scheme("https", sf, 443)); ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry); client = new DefaultHttpClient(ccm, params); } catch (Exception e) { Logger.w("Unable to create SSL HTTP client", e); client = new DefaultHttpClient(); } // TODO(mdw): For some reason this is not sending the cookie to the // test server, probably because the cookie itself is not properly // initialized. Below I manually set the Cookie header instead. CookieStore store = new BasicCookieStore(); store.addCookie(authCookie); client.setCookieStore(store); return client; } public String serviceRequest(String url, String jsonString) throws IOException { if (this.accountSelector == null) { accountSelector = new AccountSelector(context); } if (!accountSelector.isAnonymous()) { synchronized (this) { if (authCookie == null) { if (!checkGetCookie()) { throw new IOException("No authCookie yet"); } } } } HttpClient client = getNewHttpClient(); String fullurl = (accountSelector.isAnonymous() ? phoneUtils.getAnonymousServerUrl() : phoneUtils.getServerUrl()) + "/" + url; Logger.i("Checking in to " + fullurl); HttpPost postMethod = new HttpPost(fullurl); StringEntity se; try { se = new StringEntity(jsonString); } catch (UnsupportedEncodingException e) { throw new IOException(e.getMessage()); } postMethod.setEntity(se); postMethod.setHeader("Accept", "application/json"); postMethod.setHeader("Content-type", "application/json"); if (!accountSelector.isAnonymous()) { // TODO(mdw): This should not be needed postMethod.setHeader("Cookie", authCookie.getName() + "=" + authCookie.getValue()); } ResponseHandler<String> responseHandler = new BasicResponseHandler(); Logger.i("Sending request: " + fullurl); String result = client.execute(postMethod, responseHandler); return result; } /** * Initiates the process to get the authentication cookie for the user account. * Returns immediately. */ public synchronized void getCookie() { if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) { Logger.i("Setting fakeAuthCookie"); authCookie = getFakeAuthCookie(); return; } if (this.accountSelector == null) { accountSelector = new AccountSelector(context); } try { // Authenticates if there are no ongoing ones if (accountSelector.getCheckinFuture() == null) { accountSelector.authenticate(); } } catch (OperationCanceledException e) { Logger.e("Unable to get auth cookie", e); } catch (AuthenticatorException e) { Logger.e("Unable to get auth cookie", e); } catch (IOException e) { Logger.e("Unable to get auth cookie", e); } } /** * Resets the checkin variables in AccountSelector * */ public void initializeAccountSelector() { accountSelector.resetCheckinFuture(); accountSelector.setAuthImmediately(false); } private synchronized boolean checkGetCookie() { if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) { authCookie = getFakeAuthCookie(); return true; } Future<Cookie> getCookieFuture = accountSelector.getCheckinFuture(); if (getCookieFuture == null) { Logger.i("checkGetCookie called too early"); return false; } if (getCookieFuture.isDone()) { try { authCookie = getCookieFuture.get(); Logger.i("Got authCookie: " + authCookie); return true; } catch (InterruptedException e) { Logger.e("Unable to get auth cookie", e); return false; } catch (ExecutionException e) { Logger.e("Unable to get auth cookie", e); return false; } } else { Logger.i("getCookieFuture is not yet finished"); return false; } } }