1 /* 2 * Copyright 2020 The Android Open Source Project 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 android.app.appsearch; 17 18 import android.annotation.FlaggedApi; 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.appsearch.exceptions.AppSearchException; 23 import android.app.appsearch.util.LogUtil; 24 import android.util.Log; 25 26 import com.android.appsearch.flags.Flags; 27 import com.android.internal.util.Preconditions; 28 29 import java.io.IOException; 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 import java.util.Objects; 33 34 /** 35 * Information about the success or failure of an AppSearch call. 36 * 37 * @param <ValueType> The type of result object for successful calls. 38 */ 39 public final class AppSearchResult<ValueType> { 40 private static final String TAG = "AppSearchResult"; 41 42 /** 43 * Result codes from {@link AppSearchSession} methods. 44 * 45 * @hide 46 */ 47 @IntDef( 48 value = { 49 RESULT_OK, 50 RESULT_UNKNOWN_ERROR, 51 RESULT_INTERNAL_ERROR, 52 RESULT_INVALID_ARGUMENT, 53 RESULT_IO_ERROR, 54 RESULT_OUT_OF_SPACE, 55 RESULT_NOT_FOUND, 56 RESULT_INVALID_SCHEMA, 57 RESULT_SECURITY_ERROR, 58 RESULT_DENIED, 59 RESULT_RATE_LIMITED, 60 RESULT_TIMED_OUT 61 }) 62 @Retention(RetentionPolicy.SOURCE) 63 public @interface ResultCode {} 64 65 /** The call was successful. */ 66 public static final int RESULT_OK = 0; 67 68 /** An unknown error occurred while processing the call. */ 69 public static final int RESULT_UNKNOWN_ERROR = 1; 70 71 /** 72 * An internal error occurred within AppSearch, which the caller cannot address. 73 * 74 * <p>This error may be considered similar to {@link IllegalStateException} 75 */ 76 public static final int RESULT_INTERNAL_ERROR = 2; 77 78 /** 79 * The caller supplied invalid arguments to the call. 80 * 81 * <p>This error may be considered similar to {@link IllegalArgumentException}. 82 */ 83 public static final int RESULT_INVALID_ARGUMENT = 3; 84 85 /** 86 * An issue occurred reading or writing to storage. The call might succeed if repeated. 87 * 88 * <p>This error may be considered similar to {@link java.io.IOException}. 89 */ 90 public static final int RESULT_IO_ERROR = 4; 91 92 /** Storage is out of space, and no more space could be reclaimed. */ 93 public static final int RESULT_OUT_OF_SPACE = 5; 94 95 /** An entity the caller requested to interact with does not exist in the system. */ 96 public static final int RESULT_NOT_FOUND = 6; 97 98 /** The caller supplied a schema which is invalid or incompatible with the previous schema. */ 99 public static final int RESULT_INVALID_SCHEMA = 7; 100 101 /** The caller requested an operation it does not have privileges for. */ 102 public static final int RESULT_SECURITY_ERROR = 8; 103 104 /** 105 * The requested operation is denied for the caller. This error is logged and returned for 106 * denylist rejections. 107 */ 108 @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED) 109 public static final int RESULT_DENIED = 9; 110 111 /** 112 * The caller has hit AppSearch's rate limit and the requested operation has been rejected. The 113 * caller is recommended to reschedule tasks with exponential backoff. 114 */ 115 @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED) 116 public static final int RESULT_RATE_LIMITED = 10; 117 118 /** The operation was timed out. */ 119 @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) 120 public static final int RESULT_TIMED_OUT = 11; 121 122 @ResultCode private final int mResultCode; 123 @Nullable private final ValueType mResultValue; 124 @Nullable private final String mErrorMessage; 125 AppSearchResult( @esultCode int resultCode, @Nullable ValueType resultValue, @Nullable String errorMessage)126 private AppSearchResult( 127 @ResultCode int resultCode, 128 @Nullable ValueType resultValue, 129 @Nullable String errorMessage) { 130 mResultCode = resultCode; 131 mResultValue = resultValue; 132 mErrorMessage = errorMessage; 133 } 134 135 /** Returns {@code true} if {@link #getResultCode} equals {@link AppSearchResult#RESULT_OK}. */ isSuccess()136 public boolean isSuccess() { 137 return getResultCode() == RESULT_OK; 138 } 139 140 /** Returns one of the {@code RESULT} constants defined in {@link AppSearchResult}. */ 141 @ResultCode getResultCode()142 public int getResultCode() { 143 return mResultCode; 144 } 145 146 /** 147 * Returns the result value associated with this result, if it was successful. 148 * 149 * <p>See the documentation of the particular {@link AppSearchSession} call producing this 150 * {@link AppSearchResult} for what is placed in the result value by that call. 151 * 152 * @throws IllegalStateException if this {@link AppSearchResult} is not successful. 153 */ 154 @Nullable getResultValue()155 public ValueType getResultValue() { 156 if (!isSuccess()) { 157 throw new IllegalStateException("AppSearchResult is a failure: " + this); 158 } 159 return mResultValue; 160 } 161 162 /** 163 * Returns the error message associated with this result. 164 * 165 * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error 166 * message may be {@code null} even if {@link #isSuccess} is {@code false}. See the 167 * documentation of the particular {@link AppSearchSession} call producing this {@link 168 * AppSearchResult} for what is returned by {@link #getErrorMessage}. 169 */ 170 @Nullable getErrorMessage()171 public String getErrorMessage() { 172 return mErrorMessage; 173 } 174 175 @Override equals(@ullable Object other)176 public boolean equals(@Nullable Object other) { 177 if (this == other) { 178 return true; 179 } 180 if (!(other instanceof AppSearchResult)) { 181 return false; 182 } 183 AppSearchResult<?> otherResult = (AppSearchResult<?>) other; 184 return mResultCode == otherResult.mResultCode 185 && Objects.equals(mResultValue, otherResult.mResultValue) 186 && Objects.equals(mErrorMessage, otherResult.mErrorMessage); 187 } 188 189 @Override hashCode()190 public int hashCode() { 191 return Objects.hash(mResultCode, mResultValue, mErrorMessage); 192 } 193 194 @Override 195 @NonNull toString()196 public String toString() { 197 if (isSuccess()) { 198 return "[SUCCESS]: " + mResultValue; 199 } 200 return "[FAILURE(" + mResultCode + ")]: " + mErrorMessage; 201 } 202 203 /** 204 * Creates a new successful {@link AppSearchResult}. 205 * 206 * @param value An optional value to associate with the successful result of the operation being 207 * performed. 208 */ 209 @NonNull newSuccessfulResult( @ullable ValueType value)210 public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult( 211 @Nullable ValueType value) { 212 return new AppSearchResult<>(RESULT_OK, value, /* errorMessage= */ null); 213 } 214 215 /** 216 * Creates a new failed {@link AppSearchResult}. 217 * 218 * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}. 219 * @param errorMessage An optional string describing the reason or nature of the failure. 220 */ 221 @NonNull newFailedResult( @esultCode int resultCode, @Nullable String errorMessage)222 public static <ValueType> AppSearchResult<ValueType> newFailedResult( 223 @ResultCode int resultCode, @Nullable String errorMessage) { 224 return new AppSearchResult<>(resultCode, /* resultValue= */ null, errorMessage); 225 } 226 227 /** 228 * Creates a new failed {@link AppSearchResult} by a AppSearchResult in another type. 229 * 230 * @hide 231 */ 232 @NonNull newFailedResult( @onNull AppSearchResult<?> otherFailedResult)233 public static <ValueType> AppSearchResult<ValueType> newFailedResult( 234 @NonNull AppSearchResult<?> otherFailedResult) { 235 Preconditions.checkState( 236 !otherFailedResult.isSuccess(), 237 "Cannot convert a success result to a failed result"); 238 return AppSearchResult.newFailedResult( 239 otherFailedResult.getResultCode(), otherFailedResult.getErrorMessage()); 240 } 241 242 /** @hide */ 243 @NonNull throwableToFailedResult( @onNull Throwable t)244 public static <ValueType> AppSearchResult<ValueType> throwableToFailedResult( 245 @NonNull Throwable t) { 246 // Log for traceability. NOT_FOUND is logged at VERBOSE because this error can occur during 247 // the regular operation of the system (b/183550974). Everything else is indicative of an 248 // actual problem and is logged at WARN. 249 if (t instanceof AppSearchException 250 && ((AppSearchException) t).getResultCode() == RESULT_NOT_FOUND) { 251 if (LogUtil.DEBUG) { 252 Log.v(TAG, "Converting throwable to failed result: " + t); 253 } 254 } else { 255 Log.w(TAG, "Converting throwable to failed result.", t); 256 } 257 258 if (t instanceof AppSearchException) { 259 return ((AppSearchException) t).toAppSearchResult(); 260 } 261 262 String exceptionClass = t.getClass().getSimpleName(); 263 @AppSearchResult.ResultCode int resultCode; 264 if (t instanceof IllegalStateException || t instanceof NullPointerException) { 265 resultCode = AppSearchResult.RESULT_INTERNAL_ERROR; 266 } else if (t instanceof IllegalArgumentException) { 267 resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT; 268 } else if (t instanceof IOException) { 269 resultCode = AppSearchResult.RESULT_IO_ERROR; 270 } else if (t instanceof SecurityException) { 271 resultCode = AppSearchResult.RESULT_SECURITY_ERROR; 272 } else { 273 resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR; 274 } 275 return AppSearchResult.newFailedResult(resultCode, exceptionClass + ": " + t.getMessage()); 276 } 277 } 278