1 /* 2 * Copyright 2013, Leanplum, Inc. All rights reserved. 3 * 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 package com.leanplum.internal; 23 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.os.AsyncTask; 27 28 import com.leanplum.Leanplum; 29 30 import org.json.JSONException; 31 import org.json.JSONObject; 32 33 import java.io.EOFException; 34 import java.io.File; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.net.HttpURLConnection; 39 import java.net.SocketTimeoutException; 40 import java.net.URL; 41 import java.util.ArrayList; 42 import java.util.Date; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Locale; 46 import java.util.Map; 47 import java.util.Stack; 48 49 /** 50 * Leanplum request class. 51 * 52 * @author Andrew First 53 */ 54 public class Request { 55 private static final long DEVELOPMENT_MIN_DELAY_MS = 100; 56 private static final long DEVELOPMENT_MAX_DELAY_MS = 5000; 57 private static final long PRODUCTION_DELAY = 60000; 58 private static final String LEANPLUM = "__leanplum__"; 59 60 private static String appId; 61 private static String accessKey; 62 private static String deviceId; 63 private static String userId; 64 private static final Map<String, Boolean> fileTransferStatus = new HashMap<>(); 65 private static int pendingDownloads; 66 private static NoPendingDownloadsCallback noPendingDownloadsBlock; 67 68 // The token is saved primarily for legacy SharedPreferences decryption. This could 69 // likely be removed in the future. 70 private static String token = null; 71 private static final Object lock = Request.class; 72 private static final Map<File, Long> fileUploadSize = new HashMap<>(); 73 private static final Map<File, Double> fileUploadProgress = new HashMap<>(); 74 private static String fileUploadProgressString = ""; 75 private static long lastSendTimeMs; 76 private static final Object uploadFileLock = new Object(); 77 78 private final String httpMethod; 79 private final String apiMethod; 80 private final Map<String, Object> params; 81 private ResponseCallback response; 82 private ErrorCallback error; 83 private boolean sent; 84 85 private static ApiResponseCallback apiResponse; 86 setAppId(String appId, String accessKey)87 public static void setAppId(String appId, String accessKey) { 88 Request.appId = appId; 89 Request.accessKey = accessKey; 90 } 91 setDeviceId(String deviceId)92 public static void setDeviceId(String deviceId) { 93 Request.deviceId = deviceId; 94 } 95 setUserId(String userId)96 public static void setUserId(String userId) { 97 Request.userId = userId; 98 } 99 setToken(String token)100 public static void setToken(String token) { 101 Request.token = token; 102 } 103 token()104 public static String token() { 105 return token; 106 } 107 loadToken()108 public static void loadToken() { 109 Context context = Leanplum.getContext(); 110 SharedPreferences defaults = context.getSharedPreferences( 111 LEANPLUM, Context.MODE_PRIVATE); 112 String token = defaults.getString(Constants.Defaults.TOKEN_KEY, null); 113 if (token == null) { 114 return; 115 } 116 setToken(token); 117 } 118 saveToken()119 public static void saveToken() { 120 Context context = Leanplum.getContext(); 121 SharedPreferences defaults = context.getSharedPreferences( 122 LEANPLUM, Context.MODE_PRIVATE); 123 SharedPreferences.Editor editor = defaults.edit(); 124 editor.putString(Constants.Defaults.TOKEN_KEY, Request.token()); 125 try { 126 editor.apply(); 127 } catch (NoSuchMethodError e) { 128 editor.commit(); 129 } 130 } 131 appId()132 public static String appId() { 133 return appId; 134 } 135 deviceId()136 public static String deviceId() { 137 return deviceId; 138 } 139 userId()140 public static String userId() { 141 return Request.userId; 142 } 143 Request(String httpMethod, String apiMethod, Map<String, Object> params)144 public Request(String httpMethod, String apiMethod, Map<String, Object> params) { 145 this.httpMethod = httpMethod; 146 this.apiMethod = apiMethod; 147 this.params = params != null ? params : new HashMap<String, Object>(); 148 149 // Make sure the Handler is initialized on the main thread. 150 OsHandler.getInstance(); 151 } 152 get(String apiMethod, Map<String, Object> params)153 public static Request get(String apiMethod, Map<String, Object> params) { 154 Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ? 155 Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE; 156 Log.log(level, "Will call API method " + apiMethod + " with arguments " + params); 157 return RequestFactory.getInstance().createRequest("GET", apiMethod, params); 158 } 159 post(String apiMethod, Map<String, Object> params)160 public static Request post(String apiMethod, Map<String, Object> params) { 161 Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ? 162 Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE; 163 Log.log(level, "Will call API method " + apiMethod + " with arguments " + params); 164 return RequestFactory.getInstance().createRequest("POST", apiMethod, params); 165 } 166 onResponse(ResponseCallback response)167 public void onResponse(ResponseCallback response) { 168 this.response = response; 169 } 170 onError(ErrorCallback error)171 public void onError(ErrorCallback error) { 172 this.error = error; 173 } 174 onApiResponse(ApiResponseCallback apiResponse)175 public void onApiResponse(ApiResponseCallback apiResponse) { 176 Request.apiResponse = apiResponse; 177 } 178 createArgsDictionary()179 private Map<String, Object> createArgsDictionary() { 180 Map<String, Object> args = new HashMap<>(); 181 args.put(Constants.Params.DEVICE_ID, deviceId); 182 args.put(Constants.Params.USER_ID, userId); 183 args.put(Constants.Params.ACTION, apiMethod); 184 args.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION); 185 args.put(Constants.Params.DEV_MODE, Boolean.toString(Constants.isDevelopmentModeEnabled)); 186 args.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0)); 187 if (token != null) { 188 args.put(Constants.Params.TOKEN, token); 189 } 190 args.putAll(params); 191 return args; 192 } 193 saveRequestForLater(Map<String, Object> args)194 private static void saveRequestForLater(Map<String, Object> args) { 195 synchronized (lock) { 196 Context context = Leanplum.getContext(); 197 SharedPreferences preferences = context.getSharedPreferences( 198 LEANPLUM, Context.MODE_PRIVATE); 199 SharedPreferences.Editor editor = preferences.edit(); 200 int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0); 201 String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, count); 202 editor.putString(itemKey, JsonConverter.toJson(args)); 203 count++; 204 editor.putInt(Constants.Defaults.COUNT_KEY, count); 205 try { 206 editor.apply(); 207 } catch (NoSuchMethodError e) { 208 editor.commit(); 209 } 210 } 211 } 212 send()213 public void send() { 214 this.sendEventually(); 215 if (Constants.isDevelopmentModeEnabled) { 216 long currentTimeMs = System.currentTimeMillis(); 217 long delayMs; 218 if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > DEVELOPMENT_MAX_DELAY_MS) { 219 delayMs = DEVELOPMENT_MIN_DELAY_MS; 220 } else { 221 delayMs = (lastSendTimeMs + DEVELOPMENT_MAX_DELAY_MS) - currentTimeMs; 222 } 223 OsHandler.getInstance().postDelayed(new Runnable() { 224 @Override 225 public void run() { 226 try { 227 sendIfConnected(); 228 } catch (Throwable t) { 229 Util.handleException(t); 230 } 231 } 232 }, delayMs); 233 } 234 } 235 236 /** 237 * Wait 1 second for potential other API calls, and then sends the call synchronously if no other 238 * call has been sent within 1 minute. 239 */ sendIfDelayed()240 public void sendIfDelayed() { 241 sendEventually(); 242 OsHandler.getInstance().postDelayed(new Runnable() { 243 @Override 244 public void run() { 245 try { 246 sendIfDelayedHelper(); 247 } catch (Throwable t) { 248 Util.handleException(t); 249 } 250 } 251 }, 1000); 252 } 253 254 /** 255 * Sends the call synchronously if no other call has been sent within 1 minute. 256 */ sendIfDelayedHelper()257 private void sendIfDelayedHelper() { 258 if (Constants.isDevelopmentModeEnabled) { 259 send(); 260 } else { 261 long currentTimeMs = System.currentTimeMillis(); 262 if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > PRODUCTION_DELAY) { 263 sendIfConnected(); 264 } 265 } 266 } 267 sendIfConnected()268 public void sendIfConnected() { 269 if (Util.isConnected()) { 270 this.sendNow(); 271 } else { 272 this.sendEventually(); 273 Log.i("Device is offline, will send later"); 274 triggerErrorCallback(new Exception("Not connected to the Internet")); 275 } 276 } 277 triggerErrorCallback(Exception e)278 private void triggerErrorCallback(Exception e) { 279 if (error != null) { 280 error.error(e); 281 } 282 if (apiResponse != null) { 283 List<Map<String, Object>> requests = getUnsentRequests(); 284 apiResponse.response(requests, null); 285 } 286 } 287 288 @SuppressWarnings("BooleanMethodIsAlwaysInverted") attachApiKeys(Map<String, Object> dict)289 private boolean attachApiKeys(Map<String, Object> dict) { 290 if (appId == null || accessKey == null) { 291 Log.e("API keys are not set. Please use Leanplum.setAppIdForDevelopmentMode or " 292 + "Leanplum.setAppIdForProductionMode."); 293 return false; 294 } 295 dict.put(Constants.Params.APP_ID, appId); 296 dict.put(Constants.Params.CLIENT_KEY, accessKey); 297 dict.put(Constants.Params.CLIENT, Constants.CLIENT); 298 return true; 299 } 300 301 public interface ResponseCallback { response(JSONObject response)302 void response(JSONObject response); 303 } 304 305 public interface ApiResponseCallback { response(List<Map<String, Object>> requests, JSONObject response)306 void response(List<Map<String, Object>> requests, JSONObject response); 307 } 308 309 public interface ErrorCallback { error(Exception e)310 void error(Exception e); 311 } 312 313 public interface NoPendingDownloadsCallback { noPendingDownloads()314 void noPendingDownloads(); 315 } 316 parseResponseJson(JSONObject responseJson, List<Map<String, Object>> requestsToSend, Exception error)317 private void parseResponseJson(JSONObject responseJson, List<Map<String, Object>> requestsToSend, 318 Exception error) { 319 if (apiResponse != null) { 320 apiResponse.response(requestsToSend, responseJson); 321 } 322 323 if (responseJson != null) { 324 Exception lastResponseError = null; 325 int numResponses = Request.numResponses(responseJson); 326 for (int i = 0; i < numResponses; i++) { 327 JSONObject response = Request.getResponseAt(responseJson, i); 328 if (!Request.isResponseSuccess(response)) { 329 String errorMessage = Request.getResponseError(response); 330 if (errorMessage == null || errorMessage.length() == 0) { 331 errorMessage = "API error"; 332 } else if (errorMessage.startsWith("App not found")) { 333 errorMessage = "No app matching the provided app ID was found."; 334 Constants.isInPermanentFailureState = true; 335 } else if (errorMessage.startsWith("Invalid access key")) { 336 errorMessage = "The access key you provided is not valid for this app."; 337 Constants.isInPermanentFailureState = true; 338 } else if (errorMessage.startsWith("Development mode requested but not permitted")) { 339 errorMessage = "A call to Leanplum.setAppIdForDevelopmentMode " 340 + "with your production key was made, which is not permitted."; 341 Constants.isInPermanentFailureState = true; 342 } else { 343 errorMessage = "API error: " + errorMessage; 344 } 345 Log.e(errorMessage); 346 if (i == numResponses - 1) { 347 lastResponseError = new Exception(errorMessage); 348 } 349 } 350 } 351 352 if (lastResponseError == null) { 353 lastResponseError = error; 354 } 355 356 if (lastResponseError != null && this.error != null) { 357 this.error.error(lastResponseError); 358 } else if (this.response != null) { 359 this.response.response(responseJson); 360 } 361 } else if (error != null && this.error != null) { 362 this.error.error(error); 363 } 364 } 365 sendNow()366 private void sendNow() { 367 if (Constants.isTestMode) { 368 return; 369 } 370 if (appId == null) { 371 Log.e("Cannot send request. appId is not set."); 372 return; 373 } 374 if (accessKey == null) { 375 Log.e("Cannot send request. accessKey is not set."); 376 return; 377 } 378 379 this.sendEventually(); 380 381 final List<Map<String, Object>> requestsToSend = popUnsentRequests(); 382 if (requestsToSend.isEmpty()) { 383 return; 384 } 385 386 final Map<String, Object> multiRequestArgs = new HashMap<>(); 387 multiRequestArgs.put(Constants.Params.DATA, jsonEncodeUnsentRequests(requestsToSend)); 388 multiRequestArgs.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION); 389 multiRequestArgs.put(Constants.Params.ACTION, Constants.Methods.MULTI); 390 multiRequestArgs.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0)); 391 if (!this.attachApiKeys(multiRequestArgs)) { 392 return; 393 } 394 395 Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() { 396 @Override 397 protected Void doInBackground(Void... params) { 398 JSONObject result = null; 399 HttpURLConnection op = null; 400 try { 401 try { 402 op = Util.operation( 403 Constants.API_HOST_NAME, 404 Constants.API_SERVLET, 405 multiRequestArgs, 406 httpMethod, 407 Constants.API_SSL, 408 Constants.NETWORK_TIMEOUT_SECONDS); 409 410 result = Util.getJsonResponse(op); 411 int statusCode = op.getResponseCode(); 412 413 Exception errorException = null; 414 if (statusCode >= 400) { 415 errorException = new Exception("HTTP error " + statusCode); 416 if (statusCode == 408 || (statusCode >= 500 && statusCode <= 599)) { 417 pushUnsentRequests(requestsToSend); 418 } 419 } else { 420 if (result != null) { 421 int numResponses = Request.numResponses(result); 422 if (numResponses != requestsToSend.size()) { 423 Log.w("Sent " + requestsToSend.size() + 424 " requests but only" + " received " + numResponses); 425 } 426 } else { 427 errorException = new Exception("Response JSON is null."); 428 } 429 } 430 parseResponseJson(result, requestsToSend, errorException); 431 } catch (JSONException e) { 432 Log.e("Error parsing JSON response: " + e.toString() + "\n" + 433 Log.getStackTraceString(e)); 434 parseResponseJson(null, requestsToSend, e); 435 } catch (Exception e) { 436 pushUnsentRequests(requestsToSend); 437 Log.e("Unable to send request: " + e.toString() + "\n" + 438 Log.getStackTraceString(e)); 439 parseResponseJson(result, requestsToSend, e); 440 } finally { 441 if (op != null) { 442 op.disconnect(); 443 } 444 } 445 } catch (Throwable t) { 446 Util.handleException(t); 447 } 448 return null; 449 } 450 }); 451 } 452 sendEventually()453 public void sendEventually() { 454 if (Constants.isTestMode) { 455 return; 456 } 457 if (!sent) { 458 sent = true; 459 Map<String, Object> args = createArgsDictionary(); 460 saveRequestForLater(args); 461 } 462 } 463 popUnsentRequests()464 static List<Map<String, Object>> popUnsentRequests() { 465 return getUnsentRequests(true); 466 } 467 getUnsentRequests()468 static List<Map<String, Object>> getUnsentRequests() { 469 return getUnsentRequests(false); 470 } 471 getUnsentRequests(boolean remove)472 private static List<Map<String, Object>> getUnsentRequests(boolean remove) { 473 List<Map<String, Object>> requestData = new ArrayList<>(); 474 475 synchronized (lock) { 476 lastSendTimeMs = System.currentTimeMillis(); 477 478 Context context = Leanplum.getContext(); 479 SharedPreferences preferences = context.getSharedPreferences( 480 LEANPLUM, Context.MODE_PRIVATE); 481 SharedPreferences.Editor editor = preferences.edit(); 482 483 int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0); 484 if (count == 0) { 485 return new ArrayList<>(); 486 } 487 if (remove) { 488 editor.remove(Constants.Defaults.COUNT_KEY); 489 } 490 491 for (int i = 0; i < count; i++) { 492 String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i); 493 Map<String, Object> requestArgs; 494 try { 495 requestArgs = JsonConverter.mapFromJson(new JSONObject( 496 preferences.getString(itemKey, "{}"))); 497 requestData.add(requestArgs); 498 } catch (JSONException e) { 499 e.printStackTrace(); 500 } 501 if (remove) { 502 editor.remove(itemKey); 503 } 504 } 505 if (remove) { 506 try { 507 editor.apply(); 508 } catch (NoSuchMethodError e) { 509 editor.commit(); 510 } 511 } 512 } 513 514 requestData = removeIrrelevantBackgroundStartRequests(requestData); 515 return requestData; 516 } 517 518 /** 519 * In various scenarios we can end up batching a big number of requests (e.g. device is offline, 520 * background sessions), which could make the stored API calls batch look something like: 521 * <p> 522 * <code>start(B), start(B), start(F), track, start(B), track, start(F), resumeSession</code> 523 * <p> 524 * where <code>start(B)</code> indicates a start in the background, and <code>start(F)</code> 525 * one in the foreground. 526 * <p> 527 * In this case the first two <code>start(B)</code> can be dropped because they don't contribute 528 * any relevant information for the batch call. 529 * <p> 530 * Essentially we drop every <code>start(B)</code> call, that is directly followed by any kind of 531 * a <code>start</code> call. 532 * 533 * @param requestData A list of the requests, stored on the device. 534 * @return A list of only these requests, which contain relevant information for the API call. 535 */ removeIrrelevantBackgroundStartRequests( List<Map<String, Object>> requestData)536 private static List<Map<String, Object>> removeIrrelevantBackgroundStartRequests( 537 List<Map<String, Object>> requestData) { 538 List<Map<String, Object>> relevantRequests = new ArrayList<>(); 539 540 int requestCount = requestData.size(); 541 if (requestCount > 0) { 542 for (int i = 0; i < requestCount; i++) { 543 Map<String, Object> currentRequest = requestData.get(i); 544 if (i < requestCount - 1 545 && Constants.Methods.START.equals(requestData.get(i + 1).get(Constants.Params.ACTION)) 546 && Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION)) 547 && Boolean.TRUE.toString().equals(currentRequest.get(Constants.Params.BACKGROUND))) { 548 continue; 549 } 550 relevantRequests.add(currentRequest); 551 } 552 } 553 554 return relevantRequests; 555 } 556 jsonEncodeUnsentRequests(List<Map<String, Object>> requestData)557 private static String jsonEncodeUnsentRequests(List<Map<String, Object>> requestData) { 558 Map<String, Object> data = new HashMap<>(); 559 data.put(Constants.Params.DATA, requestData); 560 return JsonConverter.toJson(data); 561 } 562 pushUnsentRequests(List<Map<String, Object>> requestData)563 private static void pushUnsentRequests(List<Map<String, Object>> requestData) { 564 if (requestData == null) { 565 return; 566 } 567 for (Map<String, Object> args : requestData) { 568 Object retryCountString = args.get("retryCount"); 569 int retryCount; 570 if (retryCountString != null) { 571 retryCount = Integer.parseInt(retryCountString.toString()) + 1; 572 } else { 573 retryCount = 1; 574 } 575 args.put("retryCount", Integer.toString(retryCount)); 576 saveRequestForLater(args); 577 } 578 } 579 getSizeAsString(int bytes)580 private static String getSizeAsString(int bytes) { 581 if (bytes < (1 << 10)) { 582 return bytes + " B"; 583 } else if (bytes < (1 << 20)) { 584 return (bytes >> 10) + " KB"; 585 } else { 586 return (bytes >> 20) + " MB"; 587 } 588 } 589 printUploadProgress()590 private static void printUploadProgress() { 591 int totalFiles = fileUploadSize.size(); 592 int sentFiles = 0; 593 int totalBytes = 0; 594 int sentBytes = 0; 595 for (Map.Entry<File, Long> entry : fileUploadSize.entrySet()) { 596 File file = entry.getKey(); 597 long fileSize = entry.getValue(); 598 double fileProgress = fileUploadProgress.get(file); 599 if (fileProgress == 1) { 600 sentFiles++; 601 } 602 sentBytes += (int) (fileSize * fileProgress); 603 totalBytes += fileSize; 604 } 605 String progressString = "Uploading resources. " + 606 sentFiles + '/' + totalFiles + " files completed; " + 607 getSizeAsString(sentBytes) + '/' + getSizeAsString(totalBytes) + " transferred."; 608 if (!fileUploadProgressString.equals(progressString)) { 609 fileUploadProgressString = progressString; 610 Log.i(progressString); 611 } 612 } 613 sendFilesNow(final List<String> filenames, final List<InputStream> streams)614 public void sendFilesNow(final List<String> filenames, final List<InputStream> streams) { 615 if (Constants.isTestMode) { 616 return; 617 } 618 final Map<String, Object> dict = createArgsDictionary(); 619 if (!attachApiKeys(dict)) { 620 return; 621 } 622 final List<File> filesToUpload = new ArrayList<>(); 623 624 // First set up the files for upload 625 for (int i = 0; i < filenames.size(); i++) { 626 String filename = filenames.get(i); 627 if (filename == null || Boolean.TRUE.equals(fileTransferStatus.get(filename))) { 628 continue; 629 } 630 File file = new File(filename); 631 long size; 632 try { 633 size = streams.get(i).available(); 634 } catch (IOException e) { 635 size = file.length(); 636 } catch (NullPointerException e) { 637 // Not good. Can't read asset. 638 Log.e("Unable to read file " + filename); 639 continue; 640 } 641 fileTransferStatus.put(filename, true); 642 filesToUpload.add(file); 643 fileUploadSize.put(file, size); 644 fileUploadProgress.put(file, 0.0); 645 } 646 if (filesToUpload.size() == 0) { 647 return; 648 } 649 650 printUploadProgress(); 651 652 // Now upload the files 653 Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() { 654 @Override 655 protected Void doInBackground(Void... params) { 656 synchronized (uploadFileLock) { // Don't overload app and server with many upload tasks 657 JSONObject result; 658 HttpURLConnection op = null; 659 660 try { 661 op = Util.uploadFilesOperation( 662 Constants.Params.FILE, 663 filesToUpload, 664 streams, 665 Constants.API_HOST_NAME, 666 Constants.API_SERVLET, 667 dict, 668 httpMethod, 669 Constants.API_SSL, 670 60); 671 672 if (op != null) { 673 result = Util.getJsonResponse(op); 674 int statusCode = op.getResponseCode(); 675 if (statusCode != 200) { 676 throw new Exception("Leanplum: Error sending request: " + statusCode); 677 } 678 if (Request.this.response != null) { 679 Request.this.response.response(result); 680 } 681 } else { 682 if (error != null) { 683 error.error(new Exception("Leanplum: Unable to read file.")); 684 } 685 } 686 } catch (JSONException e) { 687 Log.e("Unable to convert to JSON.", e); 688 if (error != null) { 689 error.error(e); 690 } 691 } catch (SocketTimeoutException e) { 692 Log.e("Timeout uploading files. Try again or limit the number of files " + 693 "to upload with parameters to syncResourcesAsync."); 694 if (error != null) { 695 error.error(e); 696 } 697 } catch (Exception e) { 698 Log.e("Unable to send file.", e); 699 if (error != null) { 700 error.error(e); 701 } 702 } finally { 703 if (op != null) { 704 op.disconnect(); 705 } 706 } 707 708 for (File file : filesToUpload) { 709 fileUploadProgress.put(file, 1.0); 710 } 711 printUploadProgress(); 712 713 return null; 714 } 715 } 716 }); 717 718 // TODO: Upload progress 719 } 720 downloadFile(final String path, final String url)721 void downloadFile(final String path, final String url) { 722 if (Constants.isTestMode) { 723 return; 724 } 725 if (Boolean.TRUE.equals(fileTransferStatus.get(path))) { 726 return; 727 } 728 pendingDownloads++; 729 Log.i("Downloading resource " + path); 730 fileTransferStatus.put(path, true); 731 final Map<String, Object> dict = createArgsDictionary(); 732 dict.put(Constants.Keys.FILENAME, path); 733 if (!attachApiKeys(dict)) { 734 return; 735 } 736 737 Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() { 738 @Override 739 protected Void doInBackground(Void... params) { 740 try { 741 downloadHelper(Constants.API_HOST_NAME, Constants.API_SERVLET, path, url, dict); 742 } catch (Throwable t) { 743 Util.handleException(t); 744 } 745 return null; 746 } 747 }); 748 // TODO: Download progress 749 } 750 downloadHelper(String hostName, String servlet, final String path, final String url, final Map<String, Object> dict)751 private void downloadHelper(String hostName, String servlet, final String path, final String url, 752 final Map<String, Object> dict) { 753 HttpURLConnection op = null; 754 URL originalURL = null; 755 try { 756 if (url == null) { 757 op = Util.operation( 758 hostName, 759 servlet, 760 dict, 761 httpMethod, 762 Constants.API_SSL, 763 Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS); 764 } else { 765 op = Util.createHttpUrlConnection(url, httpMethod, url.startsWith("https://"), 766 Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS); 767 } 768 originalURL = op.getURL(); 769 op.connect(); 770 int statusCode = op.getResponseCode(); 771 if (statusCode != 200) { 772 throw new Exception("Leanplum: Error sending request to: " + hostName + 773 ", HTTP status code: " + statusCode); 774 } 775 Stack<String> dirs = new Stack<>(); 776 String currentDir = path; 777 while ((currentDir = new File(currentDir).getParent()) != null) { 778 dirs.push(currentDir); 779 } 780 while (!dirs.isEmpty()) { 781 String directory = FileManager.fileRelativeToDocuments(dirs.pop()); 782 boolean isCreated = new File(directory).mkdir(); 783 if (!isCreated) { 784 Log.w("Failed to create directory: ", directory); 785 } 786 } 787 788 FileOutputStream out = new FileOutputStream( 789 new File(FileManager.fileRelativeToDocuments(path))); 790 Util.saveResponse(op, out); 791 pendingDownloads--; 792 if (Request.this.response != null) { 793 Request.this.response.response(null); 794 } 795 if (pendingDownloads == 0 && noPendingDownloadsBlock != null) { 796 noPendingDownloadsBlock.noPendingDownloads(); 797 } 798 } catch (Exception e) { 799 if (e instanceof EOFException) { 800 if (op != null && !op.getURL().equals(originalURL)) { 801 downloadHelper(null, op.getURL().toString(), path, url, new HashMap<String, Object>()); 802 return; 803 } 804 } 805 Log.e("Error downloading resource:" + path, e); 806 pendingDownloads--; 807 if (error != null) { 808 error.error(e); 809 } 810 if (pendingDownloads == 0 && noPendingDownloadsBlock != null) { 811 noPendingDownloadsBlock.noPendingDownloads(); 812 } 813 } finally { 814 if (op != null) { 815 op.disconnect(); 816 } 817 } 818 } 819 numPendingDownloads()820 public static int numPendingDownloads() { 821 return pendingDownloads; 822 } 823 onNoPendingDownloads(NoPendingDownloadsCallback block)824 public static void onNoPendingDownloads(NoPendingDownloadsCallback block) { 825 noPendingDownloadsBlock = block; 826 } 827 828 numResponses(JSONObject response)829 public static int numResponses(JSONObject response) { 830 if (response == null) { 831 return 0; 832 } 833 try { 834 return response.getJSONArray("response").length(); 835 } catch (JSONException e) { 836 Log.e("Could not parse JSON response.", e); 837 return 0; 838 } 839 } 840 getResponseAt(JSONObject response, int index)841 public static JSONObject getResponseAt(JSONObject response, int index) { 842 try { 843 return response.getJSONArray("response").getJSONObject(index); 844 } catch (JSONException e) { 845 Log.e("Could not parse JSON response.", e); 846 return null; 847 } 848 } 849 getLastResponse(JSONObject response)850 public static JSONObject getLastResponse(JSONObject response) { 851 int numResponses = numResponses(response); 852 if (numResponses > 0) { 853 return getResponseAt(response, numResponses - 1); 854 } else { 855 return null; 856 } 857 } 858 isResponseSuccess(JSONObject response)859 public static boolean isResponseSuccess(JSONObject response) { 860 if (response == null) { 861 return false; 862 } 863 try { 864 return response.getBoolean("success"); 865 } catch (JSONException e) { 866 Log.e("Could not parse JSON response.", e); 867 return false; 868 } 869 } 870 getResponseError(JSONObject response)871 public static String getResponseError(JSONObject response) { 872 if (response == null) { 873 return null; 874 } 875 try { 876 JSONObject error = response.optJSONObject("error"); 877 if (error == null) { 878 return null; 879 } 880 return error.getString("message"); 881 } catch (JSONException e) { 882 Log.e("Could not parse JSON response.", e); 883 return null; 884 } 885 } 886 } 887