19
19
import java .util .Collection ;
20
20
import java .util .Collections ;
21
21
import java .util .HashMap ;
22
- import java .util .HashSet ;
23
22
import java .util .List ;
24
23
import java .util .Map ;
25
- import java .util .Set ;
26
- import java .util .WeakHashMap ;
27
24
import java .util .concurrent .Executor ;
28
25
import java .util .concurrent .Future ;
29
26
30
27
import androidx .annotation .NonNull ;
31
- import androidx .annotation .Nullable ;
32
28
import androidx .annotation .RestrictTo ;
33
29
import androidx .annotation .VisibleForTesting ;
34
30
39
35
*/
40
36
@ RestrictTo (RestrictTo .Scope .LIBRARY_GROUP )
41
37
public class FrequencyLimitManager {
42
-
43
- /*
44
- * A frequency checker will have a strong reference to the list of constraints entities. Once
45
- * the checker is cleaned up this should remove the values from the map.
46
- */
47
- private final Map <ConstraintEntity , List <OccurrenceEntity >> occurrencesMap = new WeakHashMap <>();
48
-
49
- /*
50
- * List of pending occurrences to write to the database.
51
- */
38
+ private final Map <String , List <OccurrenceEntity >> occurrencesMap = new HashMap <>();
39
+ private final Map <String , ConstraintEntity > constraintEntityMap = new HashMap <>();
52
40
private final List <OccurrenceEntity > pendingOccurrences = new ArrayList <>();
53
-
41
+ private final Clock clock ;
54
42
private final Object lock = new Object ();
55
43
private final FrequencyLimitDao dao ;
56
- private final Clock clock ;
57
44
private final Executor executor ;
58
45
59
46
public FrequencyLimitManager (@ NonNull Context context , @ NonNull AirshipRuntimeConfig config ) {
@@ -77,30 +64,40 @@ public FrequencyLimitManager(@NonNull Context context, @NonNull AirshipRuntimeCo
77
64
* @return A future for the checker.
78
65
*/
79
66
@ NonNull
80
- public Future <FrequencyChecker > getFrequencyChecker (@ Nullable final Collection <String > constraintIds ) {
67
+ public Future <FrequencyChecker > getFrequencyChecker (@ NonNull final Collection <String > constraintIds ) {
81
68
final PendingResult <FrequencyChecker > pendingResult = new PendingResult <>();
82
- executor .execute (new Runnable () {
83
- @ Override
84
- public void run () {
85
- try {
86
- final Collection <ConstraintEntity > constraints = fetchConstraints (constraintIds );
87
- FrequencyChecker checker = new FrequencyChecker () {
88
- @ Override
89
- public boolean isOverLimit () {
90
- return FrequencyLimitManager .this .isOverLimit (constraints );
91
- }
69
+ executor .execute (() -> {
70
+ for (String constraintId : constraintIds ) {
71
+ synchronized (lock ) {
72
+ if (constraintEntityMap .containsKey (constraintId )) {
73
+ continue ;
74
+ }
75
+ }
92
76
93
- @ Override
94
- public boolean checkAndIncrement () {
95
- return FrequencyLimitManager .this .checkAndIncrement (constraints );
96
- }
97
- };
98
- pendingResult .setResult (checker );
99
- } catch (Exception e ) {
100
- Logger .error ("Failed to fetch constraints." );
77
+ List <OccurrenceEntity > occurrenceEntities = dao .getOccurrences (constraintId );
78
+ List <ConstraintEntity > constraintEntities = dao .getConstraints (Collections .singletonList (constraintId ));
79
+ if (constraintEntities .size () != 1 ) {
101
80
pendingResult .setResult (null );
81
+ return ;
82
+ }
83
+
84
+ synchronized (lock ) {
85
+ constraintEntityMap .put (constraintId , constraintEntities .get (0 ));
86
+ occurrencesMap .put (constraintId , occurrenceEntities );
102
87
}
103
88
}
89
+
90
+ pendingResult .setResult (new FrequencyChecker () {
91
+ @ Override
92
+ public boolean isOverLimit () {
93
+ return FrequencyLimitManager .this .isOverLimit (constraintIds );
94
+ }
95
+
96
+ @ Override
97
+ public boolean checkAndIncrement () {
98
+ return FrequencyLimitManager .this .checkAndIncrement (constraintIds );
99
+ }
100
+ });
104
101
});
105
102
106
103
return pendingResult ;
@@ -113,130 +110,112 @@ public boolean checkAndIncrement() {
113
110
*/
114
111
public Future <Boolean > updateConstraints (@ NonNull final Collection <FrequencyConstraint > constraints ) {
115
112
final PendingResult <Boolean > pendingResult = new PendingResult <>();
116
- executor .execute (new Runnable () {
117
- @ Override
118
- public void run () {
119
- try {
120
- Collection <ConstraintEntity > constraintEntities = dao .getConstraints ();
121
-
122
- Map <String , ConstraintEntity > constraintEntityMap = new HashMap <>();
123
- for (ConstraintEntity entity : constraintEntities ) {
124
- constraintEntityMap .put (entity .constraintId , entity );
125
- }
113
+ executor .execute (() -> {
114
+ try {
115
+ Collection <ConstraintEntity > constraintEntities = dao .getConstraints ();
116
+
117
+ Map <String , ConstraintEntity > entityMap = new HashMap <>();
118
+ for (ConstraintEntity entity : constraintEntities ) {
119
+ entityMap .put (entity .constraintId , entity );
120
+ }
126
121
127
- for (FrequencyConstraint constraint : constraints ) {
128
- ConstraintEntity entity = new ConstraintEntity ();
129
- entity .constraintId = constraint .getId ();
130
- entity .count = constraint .getCount ();
131
- entity .range = constraint .getRange ();
132
-
133
- ConstraintEntity existing = constraintEntityMap .remove (constraint .getId ());
134
- if (existing != null ) {
135
- if (existing .range != entity .range ) {
136
- dao .delete (existing );
137
- dao .insert (entity );
138
- } else {
139
- dao .update (entity );
122
+ for (FrequencyConstraint constraint : constraints ) {
123
+ ConstraintEntity entity = new ConstraintEntity ();
124
+ entity .constraintId = constraint .getId ();
125
+ entity .count = constraint .getCount ();
126
+ entity .range = constraint .getRange ();
127
+
128
+ ConstraintEntity existing = entityMap .remove (constraint .getId ());
129
+ if (existing != null ) {
130
+ if (existing .range != entity .range ) {
131
+ dao .delete (existing );
132
+ dao .insert (entity );
133
+
134
+ synchronized (lock ) {
135
+ occurrencesMap .put (constraint .getId (), new ArrayList <>());
136
+
137
+ if (entityMap .containsKey (constraint .getId ())) {
138
+ constraintEntityMap .put (constraint .getId (), entity );
139
+ }
140
140
}
141
141
} else {
142
- dao .insert (entity );
142
+ dao .update (entity );
143
+
144
+ synchronized (lock ) {
145
+ if (entityMap .containsKey (constraint .getId ())) {
146
+ constraintEntityMap .put (constraint .getId (), entity );
147
+ }
148
+ }
143
149
}
150
+ } else {
151
+ dao .insert (entity );
144
152
}
145
-
146
- dao .delete (constraintEntityMap .keySet ());
147
- pendingResult .setResult (true );
148
- } catch (Exception e ) {
149
- Logger .error (e , "Failed to update constraints" );
150
- pendingResult .setResult (false );
151
153
}
154
+
155
+ dao .delete (entityMap .keySet ());
156
+ pendingResult .setResult (true );
157
+ } catch (Exception e ) {
158
+ Logger .error (e , "Failed to update constraints" );
159
+ pendingResult .setResult (false );
152
160
}
153
161
});
154
162
155
163
return pendingResult ;
156
164
}
157
165
158
- private boolean checkAndIncrement (@ NonNull Collection <ConstraintEntity > constraints ) {
159
- if (constraints .isEmpty ()) {
166
+ private boolean checkAndIncrement (@ NonNull Collection <String > constraintIds ) {
167
+ if (constraintIds .isEmpty ()) {
160
168
return true ;
161
169
}
162
170
163
171
synchronized (lock ) {
164
- if (isOverLimit (constraints )) {
172
+ if (isOverLimit (constraintIds )) {
165
173
return false ;
166
174
}
167
- recordOccurrence (getConstraintIds ( constraints ) );
175
+ recordOccurrence (constraintIds );
168
176
return true ;
169
177
}
170
178
}
171
179
172
- private boolean isOverLimit (@ NonNull Collection <ConstraintEntity > constraints ) {
173
- if (constraints .isEmpty ()) {
180
+ private boolean isOverLimit (@ NonNull Collection <String > constraintIds ) {
181
+ if (constraintIds .isEmpty ()) {
174
182
return false ;
175
183
}
176
184
177
185
synchronized (lock ) {
178
- for (ConstraintEntity constraint : constraints ) {
179
- if (isConstraintOverLimit (constraint )) {
186
+ for (String constraintId : constraintIds ) {
187
+ if (isConstraintOverLimit (constraintId )) {
180
188
return true ;
181
189
}
182
190
}
183
191
return false ;
184
192
}
185
193
}
186
194
187
- private void recordOccurrence (@ NonNull Set <String > constraintIds ) {
195
+ private void recordOccurrence (@ NonNull Collection <String > constraintIds ) {
188
196
if (constraintIds .isEmpty ()) {
189
197
return ;
190
198
}
191
199
192
200
long timeMillis = clock .currentTimeMillis ();
193
201
194
- for (String id : constraintIds ) {
195
- OccurrenceEntity occurrence = new OccurrenceEntity ();
196
- occurrence .parentConstraintId = id ;
197
- occurrence .timeStamp = timeMillis ;
202
+ synchronized (lock ) {
203
+ for (String id : constraintIds ) {
204
+ OccurrenceEntity occurrence = new OccurrenceEntity ();
205
+ occurrence .parentConstraintId = id ;
206
+ occurrence .timeStamp = timeMillis ;
198
207
199
- pendingOccurrences .add (occurrence );
208
+ pendingOccurrences .add (occurrence );
200
209
201
- // Update any constraints that are still active
202
- for (Map .Entry <ConstraintEntity , List <OccurrenceEntity >> entry : occurrencesMap .entrySet ()) {
203
- ConstraintEntity constraint = entry .getKey ();
204
- if (constraint != null && id .equals (constraint .constraintId )) {
205
- entry .getValue ().add (occurrence );
210
+ if (occurrencesMap .get (id ) == null ) {
211
+ occurrencesMap .put (id , new ArrayList <>());
206
212
}
213
+ occurrencesMap .get (id ).add (occurrence );
207
214
}
208
215
}
209
216
210
217
// Save to database
211
- executor .execute (new Runnable () {
212
- @ Override
213
- public void run () {
214
- writePendingOccurrences ();
215
- }
216
- });
217
- }
218
-
219
- @ NonNull
220
- private Collection <ConstraintEntity > fetchConstraints (@ Nullable Collection <String > constraintIds ) {
221
- if (constraintIds == null || constraintIds .isEmpty ()) {
222
- return Collections .emptyList ();
223
- }
224
-
225
- Collection <ConstraintEntity > constraints = dao .getConstraints (constraintIds );
226
-
227
- for (ConstraintEntity constraint : constraints ) {
228
- List <OccurrenceEntity > occurrences = dao .getOccurrences (constraint .constraintId );
229
- synchronized (lock ) {
230
- for (OccurrenceEntity entity : pendingOccurrences ) {
231
- if (entity .parentConstraintId .equals (constraint .constraintId )) {
232
- occurrences .add (entity );
233
- }
234
- }
235
- occurrencesMap .put (constraint , occurrences );
236
- }
237
- }
238
-
239
- return constraints ;
218
+ executor .execute (this ::writePendingOccurrences );
240
219
}
241
220
242
221
private void writePendingOccurrences () {
@@ -251,28 +230,26 @@ private void writePendingOccurrences() {
251
230
dao .insert (occurrence );
252
231
} catch (SQLiteException e ) {
253
232
Logger .verbose (e );
233
+ synchronized (lock ) {
234
+ pendingOccurrences .add (occurrence );
235
+ }
254
236
}
255
237
}
256
238
}
257
239
258
- private boolean isConstraintOverLimit (@ NonNull ConstraintEntity constraint ) {
259
- List <OccurrenceEntity > occurrences = occurrencesMap .get (constraint );
260
-
261
- if (occurrences == null || occurrences .size () < constraint .count ) {
262
- return false ;
263
- }
240
+ private boolean isConstraintOverLimit (@ NonNull String constraintId ) {
241
+ synchronized (lock ) {
242
+ List <OccurrenceEntity > occurrences = occurrencesMap .get (constraintId );
243
+ ConstraintEntity constraint = constraintEntityMap .get (constraintId );
264
244
265
- long timeSinceOccurrence = clock . currentTimeMillis () - occurrences . get ( occurrences .size () - constraint .count ). timeStamp ;
266
- return timeSinceOccurrence <= constraint . range ;
267
- }
245
+ if ( constraint == null || occurrences == null || occurrences .size () < constraint .count ) {
246
+ return false ;
247
+ }
268
248
269
- @ NonNull
270
- private Set <String > getConstraintIds (@ NonNull Collection <ConstraintEntity > constraints ) {
271
- Set <String > constraintIds = new HashSet <>();
272
- for (ConstraintEntity constraint : constraints ) {
273
- constraintIds .add (constraint .constraintId );
249
+ // Sort the occurrences by timestamp
250
+ Collections .sort (occurrences , new OccurrenceEntity .Comparator ());
251
+ long timeSinceOccurrence = clock .currentTimeMillis () - occurrences .get (occurrences .size () - constraint .count ).timeStamp ;
252
+ return timeSinceOccurrence <= constraint .range ;
274
253
}
275
- return constraintIds ;
276
254
}
277
-
278
255
}
0 commit comments