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