summaryrefslogtreecommitdiff
path: root/java/com/google/android/libraries/mobiledatadownload/downloader/offroad/ExceptionHandler.java
blob: b096bd6b2d93da6a4c89ea38ed0a4db3cd7a524e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/*
 * Copyright 2022 Google LLC
 *
 * 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.android.libraries.mobiledatadownload.downloader.offroad;

import com.google.android.downloader.RequestException;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;

/**
 * Handles mapping exceptions from Downloader2 into the equivalent MDD {@link DownloadException}.
 *
 * <p>Common exceptions are parsed and handled by default, but the underlying network stack may
 * include special Exceptions and/or error codes that need to be parsed. If this is the case, a
 * {@link NetworkStackExceptionHandler} can be provided to perform this parsing.
 */
public final class ExceptionHandler {
  /**
   * The maximum amount of attempts we recurse before stopping in {@link
   * #mapExceptionToDownloadResultCode}.
   */
  private static final int EXCEPTION_TO_CODE_RECURSION_LIMIT = 5;

  /** The handler of underlying network stack failures. */
  private final NetworkStackExceptionHandler internalExceptionHandler;

  private ExceptionHandler(NetworkStackExceptionHandler internalExceptionHandler) {
    this.internalExceptionHandler = internalExceptionHandler;
  }

  /** Convenience method to return a new ExceptionHandler with default handling. */
  public static ExceptionHandler withDefaultHandling() {
    return new ExceptionHandler(new NetworkStackExceptionHandler() {});
  }

  /** Return a new instance with specific handling for a network stack. */
  public static ExceptionHandler withNetworkStackHandling(
      NetworkStackExceptionHandler internalExceptionHandler) {
    return new ExceptionHandler(internalExceptionHandler);
  }

  /**
   * Map given failure to a {@link DownloadException}.
   *
   * <p>For most cases, this method does not need to be overridden.
   *
   * <p><em>NOTE:</em> If the given throwable is already a {@link DownloadException}, it is returned
   * immediately. In this case, the given message will <b>not</b> be used (the message from the
   * given throwable will be used instead).
   *
   * @param message top-level message that should be used for the returned {@link DownloadException}
   * @param throwable generic throwable that should be mapped to {@link DownloadException}
   * @return {@link DownloadException} that wraps around given throwable with appropriate {@link
   *     DownloadResultCode}
   */
  public DownloadException mapToDownloadException(String message, Throwable throwable) {
    if (throwable instanceof DownloadException) {
      // Exception is already an MDD DownloadException -- return it.
      return (DownloadException) throwable;
    }

    DownloadResultCode code = mapExceptionToDownloadResultCode(throwable, /* iteration= */ 0);

    return DownloadException.builder()
        .setMessage(message)
        .setDownloadResultCode(code)
        .setCause(throwable)
        .build();
  }

  /**
   * Map exception to {@link DownloadResultCode}.
   *
   * @param throwable the exception to map to a {@link DownloadResultCode}
   */
  private DownloadResultCode mapExceptionToDownloadResultCode(Throwable throwable, int iteration) {
    // Check recursion limit and return unknown error if it is hit.
    if (iteration >= EXCEPTION_TO_CODE_RECURSION_LIMIT) {
      return DownloadResultCode.UNKNOWN_ERROR;
    }

    DownloadResultCode networkStackMapperResult =
        internalExceptionHandler.mapFromNetworkStackException(throwable);
    if (!networkStackMapperResult.equals(DownloadResultCode.UNKNOWN_ERROR)) {
      // network stack mapper returned known result code -- return it instead of performing common
      // mapping.
      return networkStackMapperResult;
    }

    if (throwable instanceof DownloadException) {
      // exception in the chain is already an MDD DownloadException -- use its code
      return ((DownloadException) throwable).getDownloadResultCode();
    }

    if (throwable instanceof RequestException) {
      // Check error details for http status code error.
      if (((RequestException) throwable).getErrorDetails().getHttpStatusCode() != -1) {
        // error code has an associated http status code, mark it as HTTP_ERROR
        return DownloadResultCode.ANDROID_DOWNLOADER_HTTP_ERROR;
      }
    }

    if (throwable.getCause() != null) {
      // Exception has an underlying cause -- attempt mapping on that cause.
      return mapExceptionToDownloadResultCode(throwable.getCause(), iteration + 1);
    }

    if (throwable instanceof com.google.android.downloader.DownloadException) {
      // If DownloadException is not wrapping anything, we can't determine the error further -- mark
      // it as a general Downloader2 error.
      return DownloadResultCode.ANDROID_DOWNLOADER2_ERROR;
    }

    // We couldn't parse any common errors, return an unknown error
    return DownloadResultCode.UNKNOWN_ERROR;
  }

  /**
   * Interface to handle parsing exceptions from an underlying network stack.
   *
   * <p>If an underlying network stack is used which can throw special exceptions or has an error
   * code map, consider implementing this to provide better handling of exceptions.
   */
  public static interface NetworkStackExceptionHandler {
    /**
     * Map Custom Exception to {@link DownloadResultCode}.
     *
     * <p>Underlying network stacks may have specific exceptions that can be used to determine the
     * best DownloadResultCode. This method should be overridden to check for such exceptions.
     *
     * <p>If a known {@link DownloadResultCode} is returned (i.e not UNKNOWN_ERROR), it will be
     * used.
     *
     * <p>By default, an UNKNOWN_ERROR is returned.
     */
    default DownloadResultCode mapFromNetworkStackException(Throwable throwable) {
      return DownloadResultCode.UNKNOWN_ERROR;
    }
  }
}