1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.downloader.offroad; 17 18 import com.google.android.downloader.RequestException; 19 import com.google.android.libraries.mobiledatadownload.DownloadException; 20 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 21 22 /** 23 * Handles mapping exceptions from Downloader2 into the equivalent MDD {@link DownloadException}. 24 * 25 * <p>Common exceptions are parsed and handled by default, but the underlying network stack may 26 * include special Exceptions and/or error codes that need to be parsed. If this is the case, a 27 * {@link NetworkStackExceptionHandler} can be provided to perform this parsing. 28 */ 29 public final class ExceptionHandler { 30 /** 31 * The maximum amount of attempts we recurse before stopping in {@link 32 * #mapExceptionToDownloadResultCode}. 33 */ 34 private static final int EXCEPTION_TO_CODE_RECURSION_LIMIT = 5; 35 36 /** The handler of underlying network stack failures. */ 37 private final NetworkStackExceptionHandler internalExceptionHandler; 38 ExceptionHandler(NetworkStackExceptionHandler internalExceptionHandler)39 private ExceptionHandler(NetworkStackExceptionHandler internalExceptionHandler) { 40 this.internalExceptionHandler = internalExceptionHandler; 41 } 42 43 /** Convenience method to return a new ExceptionHandler with default handling. */ withDefaultHandling()44 public static ExceptionHandler withDefaultHandling() { 45 return new ExceptionHandler(new NetworkStackExceptionHandler() {}); 46 } 47 48 /** Return a new instance with specific handling for a network stack. */ withNetworkStackHandling( NetworkStackExceptionHandler internalExceptionHandler)49 public static ExceptionHandler withNetworkStackHandling( 50 NetworkStackExceptionHandler internalExceptionHandler) { 51 return new ExceptionHandler(internalExceptionHandler); 52 } 53 54 /** 55 * Map given failure to a {@link DownloadException}. 56 * 57 * <p>For most cases, this method does not need to be overridden. 58 * 59 * <p><em>NOTE:</em> If the given throwable is already a {@link DownloadException}, it is returned 60 * immediately. In this case, the given message will <b>not</b> be used (the message from the 61 * given throwable will be used instead). 62 * 63 * @param message top-level message that should be used for the returned {@link DownloadException} 64 * @param throwable generic throwable that should be mapped to {@link DownloadException} 65 * @return {@link DownloadException} that wraps around given throwable with appropriate {@link 66 * DownloadResultCode} 67 */ mapToDownloadException(String message, Throwable throwable)68 public DownloadException mapToDownloadException(String message, Throwable throwable) { 69 if (throwable instanceof DownloadException) { 70 // Exception is already an MDD DownloadException -- return it. 71 return (DownloadException) throwable; 72 } 73 74 DownloadResultCode code = mapExceptionToDownloadResultCode(throwable, /* iteration= */ 0); 75 76 return DownloadException.builder() 77 .setMessage(message) 78 .setDownloadResultCode(code) 79 .setCause(throwable) 80 .build(); 81 } 82 83 /** 84 * Map exception to {@link DownloadResultCode}. 85 * 86 * @param throwable the exception to map to a {@link DownloadResultCode} 87 */ mapExceptionToDownloadResultCode(Throwable throwable, int iteration)88 private DownloadResultCode mapExceptionToDownloadResultCode(Throwable throwable, int iteration) { 89 // Check recursion limit and return unknown error if it is hit. 90 if (iteration >= EXCEPTION_TO_CODE_RECURSION_LIMIT) { 91 return DownloadResultCode.UNKNOWN_ERROR; 92 } 93 94 DownloadResultCode networkStackMapperResult = 95 internalExceptionHandler.mapFromNetworkStackException(throwable); 96 if (!networkStackMapperResult.equals(DownloadResultCode.UNKNOWN_ERROR)) { 97 // network stack mapper returned known result code -- return it instead of performing common 98 // mapping. 99 return networkStackMapperResult; 100 } 101 102 if (throwable instanceof DownloadException) { 103 // exception in the chain is already an MDD DownloadException -- use its code 104 return ((DownloadException) throwable).getDownloadResultCode(); 105 } 106 107 if (throwable instanceof RequestException) { 108 // Check error details for http status code error. 109 if (((RequestException) throwable).getErrorDetails().getHttpStatusCode() != -1) { 110 // error code has an associated http status code, mark it as HTTP_ERROR 111 return DownloadResultCode.ANDROID_DOWNLOADER_HTTP_ERROR; 112 } 113 } 114 115 if (throwable.getCause() != null) { 116 // Exception has an underlying cause -- attempt mapping on that cause. 117 return mapExceptionToDownloadResultCode(throwable.getCause(), iteration + 1); 118 } 119 120 if (throwable instanceof com.google.android.downloader.DownloadException) { 121 // If DownloadException is not wrapping anything, we can't determine the error further -- mark 122 // it as a general Downloader2 error. 123 return DownloadResultCode.ANDROID_DOWNLOADER2_ERROR; 124 } 125 126 // We couldn't parse any common errors, return an unknown error 127 return DownloadResultCode.UNKNOWN_ERROR; 128 } 129 130 /** 131 * Interface to handle parsing exceptions from an underlying network stack. 132 * 133 * <p>If an underlying network stack is used which can throw special exceptions or has an error 134 * code map, consider implementing this to provide better handling of exceptions. 135 */ 136 public static interface NetworkStackExceptionHandler { 137 /** 138 * Map Custom Exception to {@link DownloadResultCode}. 139 * 140 * <p>Underlying network stacks may have specific exceptions that can be used to determine the 141 * best DownloadResultCode. This method should be overridden to check for such exceptions. 142 * 143 * <p>If a known {@link DownloadResultCode} is returned (i.e not UNKNOWN_ERROR), it will be 144 * used. 145 * 146 * <p>By default, an UNKNOWN_ERROR is returned. 147 */ mapFromNetworkStackException(Throwable throwable)148 default DownloadResultCode mapFromNetworkStackException(Throwable throwable) { 149 return DownloadResultCode.UNKNOWN_ERROR; 150 } 151 } 152 } 153