Skip to content

Commit 7996fe3

Browse files
authored
Merge pull request #1724 from OneSignal/user-model/operation-recover
[User Model] Successfully recover from failing operations
2 parents 3453d43 + 24201b4 commit 7996fe3

File tree

17 files changed

+309
-69
lines changed

17 files changed

+309
-69
lines changed
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
package com.onesignal.common
22

33
object NetworkUtils {
4+
enum class ResponseStatusType {
5+
INVALID,
6+
RETRYABLE,
7+
UNAUTHORIZED,
8+
MISSING,
9+
CONFLICT
10+
}
11+
412
var MAX_NETWORK_REQUEST_ATTEMPT_COUNT = 3
5-
val NO_RETRY_NETWROK_REQUEST_STATUS_CODES = intArrayOf(401, 402, 403, 404, 410)
613

7-
fun shouldRetryNetworkRequest(statusCode: Int): Boolean {
8-
for (code in NO_RETRY_NETWROK_REQUEST_STATUS_CODES)
9-
if (statusCode == code) return false
10-
return true
14+
fun getResponseStatusType(statusCode: Int): ResponseStatusType {
15+
return when (statusCode) {
16+
400, 402 -> ResponseStatusType.INVALID
17+
401, 403 -> ResponseStatusType.UNAUTHORIZED
18+
404, 410 -> ResponseStatusType.MISSING
19+
409 -> ResponseStatusType.CONFLICT
20+
else -> ResponseStatusType.RETRYABLE
21+
}
1122
}
1223
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/IModelStore.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ interface IModelStore<TModel> :
3636
*/
3737
fun add(model: TModel, tag: String = ModelChangeTags.NORMAL)
3838

39+
/**
40+
* Add a new model to this model store. Once added, any changes to the
41+
* model will trigger calls to an [IModelStoreChangeHandler] that has
42+
* subscribed to this model store. This same instance is also retrievable
43+
* via [get] based on [Model.id].
44+
*
45+
* @param index The index to add it under
46+
* @param model The model being added to the model store.
47+
* @param tag The tag which identifies how/why the model is being added.
48+
*/
49+
fun add(index: Int, model: TModel, tag: String = ModelChangeTags.NORMAL)
50+
3951
/**
4052
* Retrieve the model associated to the id provided.
4153
*

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ abstract class ModelStore<TModel>(
4646
addItem(model, tag)
4747
}
4848

49+
override fun add(index: Int, model: TModel, tag: String) {
50+
val oldModel = _models.firstOrNull { it.id == model.id }
51+
if (oldModel != null) {
52+
removeItem(oldModel, tag)
53+
}
54+
55+
addItem(model, tag, index)
56+
}
57+
4958
override fun list(): Collection<TModel> {
5059
return _models
5160
}
@@ -86,8 +95,12 @@ abstract class ModelStore<TModel>(
8695
}
8796
}
8897

89-
private fun addItem(model: TModel, tag: String) {
90-
_models.add(model)
98+
private fun addItem(model: TModel, tag: String, index: Int? = null) {
99+
if (index != null) {
100+
_models.add(index, model)
101+
} else {
102+
_models.add(model)
103+
}
91104

92105
// listen for changes to this model
93106
model.subscribe(this)

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/services/ServiceRegistration.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ class ServiceRegistrationReflection<T>(
5454

5555
override fun resolve(provider: IServiceProvider): Any? {
5656
if (obj != null) {
57-
Logging.info("${ServiceProvider.indent}Already instantiated: $obj")
57+
Logging.debug("${ServiceProvider.indent}Already instantiated: $obj")
5858
return obj
5959
}
6060

6161
// use reflection to try to instantiate the thing
6262
for (constructor in clazz.constructors) {
6363
if (doesHaveAllParameters(constructor, provider)) {
64-
Logging.info("${ServiceProvider.indent}Found constructor: $constructor")
64+
Logging.debug("${ServiceProvider.indent}Found constructor: $constructor")
6565
var paramList: MutableList<Any?> = mutableListOf()
6666

6767
for (param in constructor.genericParameterTypes) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,17 @@ fun suspendifyOnThread(priority: Int = -1, block: suspend () -> Unit) {
6262
}
6363
}
6464
}
65+
66+
/**
67+
* Allows a non suspending function to create a scope that can
68+
* call suspending functions. This is a nonblocking call, which
69+
* means the scope will run on a background thread. This will
70+
* return immediately!!!
71+
*/
72+
fun suspendifyOnThread(name: String, priority: Int = -1, block: suspend () -> Unit) {
73+
thread(name = name, priority = priority) {
74+
runBlocking {
75+
block()
76+
}
77+
}
78+
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ internal class HttpClient(
9393
}
9494

9595
try {
96-
Logging.debug("HttpClient: Making request to: $url")
9796
con = _connectionFactory.newHttpURLConnection(url)
9897

9998
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1465
@@ -124,12 +123,14 @@ internal class HttpClient(
124123

125124
if (jsonBody != null) {
126125
val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody)
127-
Logging.debug("HttpClient: $method SEND JSON: $strJsonBody")
126+
Logging.debug("HttpClient: ${method ?: "GET"} $url - $strJsonBody")
128127

129128
val sendBytes = strJsonBody.toByteArray(charset("UTF-8"))
130129
con.setFixedLengthStreamingMode(sendBytes.size)
131130
val outputStream = con.outputStream
132131
outputStream.write(sendBytes)
132+
} else {
133+
Logging.debug("HttpClient: ${method ?: "GET"} $url")
133134
}
134135

135136
if (cacheKey != null) {
@@ -143,24 +144,21 @@ internal class HttpClient(
143144

144145
// Network request is made from getResponseCode()
145146
httpResponse = con.responseCode
146-
Logging.verbose("HttpClient: After con.getResponseCode to: $url")
147147

148148
when (httpResponse) {
149149
HttpURLConnection.HTTP_NOT_MODIFIED -> {
150150
val cachedResponse = _prefs.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_HTTP_CACHE_PREFIX + cacheKey)
151-
Logging.debug("HttpClient: " + (method ?: "GET") + " - Using Cached response due to 304: " + cachedResponse)
151+
Logging.debug("HttpClient: ${method ?: "GET"} $url - Using Cached response due to 304: " + cachedResponse)
152152

153153
// TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT?
154154
retVal = HttpResponse(httpResponse, cachedResponse)
155155
}
156-
HttpURLConnection.HTTP_ACCEPTED, HttpURLConnection.HTTP_OK -> {
157-
Logging.debug("HttpClient: Successfully finished request to: $url")
158-
156+
HttpURLConnection.HTTP_ACCEPTED, HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_OK -> {
159157
val inputStream = con.inputStream
160158
val scanner = Scanner(inputStream, "UTF-8")
161159
val json = if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else ""
162160
scanner.close()
163-
Logging.debug("HttpClient: " + (method ?: "GET") + " RECEIVED JSON: " + json)
161+
Logging.debug("HttpClient: ${method ?: "GET"} $url - STATUS: $httpResponse JSON: " + json)
164162

165163
if (cacheKey != null) {
166164
val eTag = con.getHeaderField("etag")
@@ -175,7 +173,7 @@ internal class HttpClient(
175173
retVal = HttpResponse(httpResponse, json)
176174
}
177175
else -> {
178-
Logging.debug("HttpClient: Failed request to: $url")
176+
Logging.debug("HttpClient: ${method ?: "GET"} $url - FAILED STATUS: $httpResponse")
179177

180178
var inputStream = con.errorStream
181179
if (inputStream == null) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/IOperationExecutor.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ class ExecutionResponse(
3434
* The map of id translations that should be applied to any outstanding operations.
3535
* Within the map the key is the local Id, the value is the remote Id.
3636
*/
37-
val idTranslations: Map<String, String>? = null
37+
val idTranslations: Map<String, String>? = null,
38+
39+
/**
40+
* When specified, any operations that should be prepended to the operation repo.
41+
*/
42+
val operations: List<Operation>? = null
3843
)
3944

4045
enum class ExecutionResult {
@@ -56,5 +61,11 @@ enum class ExecutionResult {
5661
/**
5762
* The operation failed and should not be tried again.
5863
*/
59-
FAIL_NORETRY
64+
FAIL_NORETRY,
65+
66+
/**
67+
* The operation failed because the request was not authorized. The operation can be
68+
* retried if authorization can be achieved.
69+
*/
70+
FAIL_UNAUTHORIZED
6071
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal class OperationRepo(
4848
}
4949

5050
override fun start() {
51-
suspendifyOnThread {
51+
suspendifyOnThread(name = "OpRepo") {
5252
processQueueForever()
5353
}
5454
}
@@ -165,6 +165,7 @@ internal class OperationRepo(
165165
ops.forEach { _operationModelStore.remove(it.operation.id) }
166166
ops.forEach { it.waiter?.wake(true) }
167167
}
168+
ExecutionResult.FAIL_UNAUTHORIZED, // TODO: Need to provide callback for app to reset JWT. For now, fail with no retry.
168169
ExecutionResult.FAIL_NORETRY -> {
169170
Logging.error("Operation execution failed without retry: $operations")
170171
// on failure we remove the operation from the store and wake any waiters
@@ -187,6 +188,19 @@ internal class OperationRepo(
187188
}
188189
}
189190
}
191+
192+
// if there are operations provided on the result, we need to enqueue them at the
193+
// beginning of the queue.
194+
if (response.operations != null) {
195+
synchronized(_queue) {
196+
for (op in response.operations.reversed()) {
197+
op.id = UUID.randomUUID().toString()
198+
val queueItem = OperationQueueItem(op)
199+
_queue.add(0, queueItem)
200+
_operationModelStore.add(0, queueItem.operation)
201+
}
202+
}
203+
}
190204
} catch (e: Throwable) {
191205
Logging.log(LogLevel.ERROR, "Error attempting to execute operation: $ops", e)
192206

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.onesignal.user.internal.backend.IUserBackendService
1111
import com.onesignal.user.internal.backend.impl.IdentityBackendService
1212
import com.onesignal.user.internal.backend.impl.SubscriptionBackendService
1313
import com.onesignal.user.internal.backend.impl.UserBackendService
14+
import com.onesignal.user.internal.builduser.IRebuildUserService
15+
import com.onesignal.user.internal.builduser.impl.RebuildUserService
1416
import com.onesignal.user.internal.identity.IdentityModelStore
1517
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
1618
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
@@ -49,6 +51,7 @@ internal class UserModule : IModule {
4951
builder.register<SubscriptionManager>().provides<ISubscriptionManager>()
5052

5153
// User
54+
builder.register<RebuildUserService>().provides<IRebuildUserService>()
5255
builder.register<UserBackendService>().provides<IUserBackendService>()
5356
builder.register<UpdateUserOperationExecutor>()
5457
.provides<UpdateUserOperationExecutor>()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.onesignal.user.internal.builduser
2+
3+
import com.onesignal.core.internal.operations.Operation
4+
5+
interface IRebuildUserService {
6+
/**
7+
* Retrieve the list of operations for rebuilding a user, if the
8+
* [onesignalId] provided represents the current user.
9+
*
10+
* @param appId The id of the app.
11+
* @param onesignalId The id of the user to retrieve operations for.
12+
*
13+
* @return the list of operations if [onesignalId] represents the current
14+
* user, null otherwise.
15+
*/
16+
fun getRebuildOperationsIfCurrentUser(appId: String, onesignalId: String): List<Operation>?
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.onesignal.user.internal.builduser.impl
2+
3+
import com.onesignal.core.internal.config.ConfigModelStore
4+
import com.onesignal.core.internal.operations.Operation
5+
import com.onesignal.user.internal.builduser.IRebuildUserService
6+
import com.onesignal.user.internal.identity.IdentityModel
7+
import com.onesignal.user.internal.identity.IdentityModelStore
8+
import com.onesignal.user.internal.operations.CreateSubscriptionOperation
9+
import com.onesignal.user.internal.operations.LoginUserOperation
10+
import com.onesignal.user.internal.operations.RefreshUserOperation
11+
import com.onesignal.user.internal.properties.PropertiesModel
12+
import com.onesignal.user.internal.properties.PropertiesModelStore
13+
import com.onesignal.user.internal.subscriptions.SubscriptionModel
14+
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
15+
16+
class RebuildUserService(
17+
private val _identityModelStore: IdentityModelStore,
18+
private val _propertiesModelStore: PropertiesModelStore,
19+
private val _subscriptionsModelStore: SubscriptionModelStore,
20+
private val _configModelStore: ConfigModelStore
21+
) : IRebuildUserService {
22+
23+
override fun getRebuildOperationsIfCurrentUser(appId: String, onesignalId: String): List<Operation>? {
24+
// make a copy of the current models
25+
val identityModel = IdentityModel()
26+
identityModel.initializeFromModel(null, _identityModelStore.model)
27+
28+
val propertiesModel = PropertiesModel()
29+
propertiesModel.initializeFromModel(null, _propertiesModelStore.model)
30+
31+
val subscriptionModels = mutableListOf<SubscriptionModel>()
32+
for (activeSubscriptionModel in _subscriptionsModelStore.list()) {
33+
val subscriptionModel = SubscriptionModel()
34+
subscriptionModel.initializeFromModel(null, activeSubscriptionModel)
35+
subscriptionModels.add(subscriptionModel)
36+
}
37+
38+
// if the current models are no longer the onesignalId that needs rebuilding, we are done.
39+
if (identityModel.onesignalId != onesignalId) {
40+
return null
41+
}
42+
43+
// rebuild the user. Rebuilding is essentially the push subscription.
44+
val operations = mutableListOf<Operation>()
45+
46+
operations.add(LoginUserOperation(appId, onesignalId, identityModel.externalId))
47+
val pushSubscription = subscriptionModels.firstOrNull { it.id == _configModelStore.model.pushSubscriptionId }
48+
if (pushSubscription != null) {
49+
operations.add(CreateSubscriptionOperation(appId, onesignalId, pushSubscription.id, pushSubscription.type, pushSubscription.optedIn, pushSubscription.address, pushSubscription.status))
50+
}
51+
operations.add(RefreshUserOperation(appId, onesignalId))
52+
return operations
53+
}
54+
}

0 commit comments

Comments
 (0)