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