1package eu.siacs.conversations.persistance;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6import android.database.sqlite.SQLiteDatabase;
7import android.database.sqlite.SQLiteOpenHelper;
8import android.util.Log;
9import androidx.annotation.NonNull;
10import androidx.annotation.Nullable;
11import com.google.common.base.MoreObjects;
12import com.google.common.base.Objects;
13import com.google.common.base.Optional;
14import com.google.common.collect.ImmutableList;
15import eu.siacs.conversations.Config;
16import eu.siacs.conversations.services.UnifiedPushBroker;
17import java.util.List;
18
19public class UnifiedPushDatabase extends SQLiteOpenHelper {
20 private static final String DATABASE_NAME = "unified-push-distributor";
21 private static final int DATABASE_VERSION = 1;
22
23 private static UnifiedPushDatabase instance;
24
25 public static UnifiedPushDatabase getInstance(final Context context) {
26 synchronized (UnifiedPushDatabase.class) {
27 if (instance == null) {
28 instance = new UnifiedPushDatabase(context.getApplicationContext());
29 }
30 return instance;
31 }
32 }
33
34 private UnifiedPushDatabase(@Nullable Context context) {
35 super(context, DATABASE_NAME, null, DATABASE_VERSION);
36 }
37
38 @Override
39 public void onCreate(final SQLiteDatabase sqLiteDatabase) {
40 sqLiteDatabase.execSQL(
41 "CREATE TABLE push (account TEXT, transport TEXT, application TEXT NOT NULL,"
42 + " instance TEXT NOT NULL UNIQUE, endpoint TEXT, expiration NUMBER DEFAULT"
43 + " 0)");
44 }
45
46 public boolean register(final String application, final String instance) {
47 final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
48 sqLiteDatabase.beginTransaction();
49 final Optional<String> existingApplication;
50 try (final Cursor cursor =
51 sqLiteDatabase.query(
52 "push",
53 new String[] {"application"},
54 "instance=?",
55 new String[] {instance},
56 null,
57 null,
58 null)) {
59 if (cursor != null && cursor.moveToFirst()) {
60 existingApplication = Optional.of(cursor.getString(0));
61 } else {
62 existingApplication = Optional.absent();
63 }
64 }
65 if (existingApplication.isPresent()) {
66 sqLiteDatabase.setTransactionSuccessful();
67 sqLiteDatabase.endTransaction();
68 return application.equals(existingApplication.get());
69 }
70 final ContentValues contentValues = new ContentValues();
71 contentValues.put("application", application);
72 contentValues.put("instance", instance);
73 contentValues.put("expiration", 0);
74 final long inserted = sqLiteDatabase.insert("push", null, contentValues);
75 if (inserted > 0) {
76 Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db");
77 }
78 sqLiteDatabase.setTransactionSuccessful();
79 sqLiteDatabase.endTransaction();
80 return true;
81 }
82
83 public List<PushTarget> getRenewals(final String account, final String transport) {
84 final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
85 final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW;
86 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
87 try (final Cursor cursor =
88 sqLiteDatabase.query(
89 "push",
90 new String[] {"application", "instance"},
91 "account <> ? OR transport <> ? OR expiration < " + expiration,
92 new String[] {account, transport},
93 null,
94 null,
95 null)) {
96 while (cursor != null && cursor.moveToNext()) {
97 renewalBuilder.add(
98 new PushTarget(
99 cursor.getString(cursor.getColumnIndexOrThrow("application")),
100 cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
101 }
102 }
103 return renewalBuilder.build();
104 }
105
106 public ApplicationEndpoint getEndpoint(
107 final String account, final String transport, final String instance) {
108 final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW;
109 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
110 try (final Cursor cursor =
111 sqLiteDatabase.query(
112 "push",
113 new String[] {"application", "endpoint"},
114 "account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL"
115 + " AND expiration >= "
116 + expiration,
117 new String[] {account, transport, instance},
118 null,
119 null,
120 null)) {
121 if (cursor != null && cursor.moveToFirst()) {
122 return new ApplicationEndpoint(
123 cursor.getString(cursor.getColumnIndexOrThrow("application")),
124 cursor.getString(cursor.getColumnIndexOrThrow("endpoint")));
125 }
126 }
127 return null;
128 }
129
130 public List<PushTarget> deletePushTargets() {
131 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
132 final ImmutableList.Builder<PushTarget> builder = new ImmutableList.Builder<>();
133 try (final Cursor cursor =
134 sqLiteDatabase.query(
135 "push",
136 new String[] {"application", "instance"},
137 null,
138 null,
139 null,
140 null,
141 null)) {
142 if (cursor != null && cursor.moveToFirst()) {
143 builder.add(
144 new PushTarget(
145 cursor.getString(cursor.getColumnIndexOrThrow("application")),
146 cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
147 }
148 } catch (final Exception e) {
149 Log.d(Config.LOGTAG, "unable to retrieve push targets", e);
150 return builder.build();
151 }
152 sqLiteDatabase.delete("push", null, null);
153 return builder.build();
154 }
155
156 public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) {
157 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
158 try (final Cursor cursor =
159 sqLiteDatabase.rawQuery(
160 "SELECT EXISTS(SELECT endpoint FROM push WHERE account = ? AND transport ="
161 + " ?)",
162 new String[] {
163 transport.account.getUuid(), transport.transport.toString()
164 })) {
165 if (cursor != null && cursor.moveToFirst()) {
166 return cursor.getInt(0) > 0;
167 }
168 }
169 return false;
170 }
171
172 @Override
173 public void onUpgrade(
174 final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {}
175
176 public boolean updateEndpoint(
177 final String instance,
178 final String account,
179 final String transport,
180 final String endpoint,
181 final long expiration) {
182 final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
183 sqLiteDatabase.beginTransaction();
184 final String existingEndpoint;
185 try (final Cursor cursor =
186 sqLiteDatabase.query(
187 "push",
188 new String[] {"endpoint"},
189 "instance=?",
190 new String[] {instance},
191 null,
192 null,
193 null)) {
194 if (cursor != null && cursor.moveToFirst()) {
195 existingEndpoint = cursor.getString(0);
196 } else {
197 existingEndpoint = null;
198 }
199 }
200 final ContentValues contentValues = new ContentValues();
201 contentValues.put("account", account);
202 contentValues.put("transport", transport);
203 contentValues.put("endpoint", endpoint);
204 contentValues.put("expiration", expiration);
205 sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance});
206 sqLiteDatabase.setTransactionSuccessful();
207 sqLiteDatabase.endTransaction();
208 return !endpoint.equals(existingEndpoint);
209 }
210
211 public List<PushTarget> getPushTargets(final String account, final String transport) {
212 final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
213 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
214 try (final Cursor cursor =
215 sqLiteDatabase.query(
216 "push",
217 new String[] {"application", "instance"},
218 "account = ?",
219 new String[] {account},
220 null,
221 null,
222 null)) {
223 while (cursor != null && cursor.moveToNext()) {
224 renewalBuilder.add(
225 new PushTarget(
226 cursor.getString(cursor.getColumnIndexOrThrow("application")),
227 cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
228 }
229 }
230 return renewalBuilder.build();
231 }
232
233 public boolean deleteInstance(final String instance) {
234 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
235 final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance});
236 return rows >= 1;
237 }
238
239 public boolean deleteApplication(final String application) {
240 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
241 final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application});
242 return rows >= 1;
243 }
244
245 public static class ApplicationEndpoint {
246 public final String application;
247 public final String endpoint;
248
249 public ApplicationEndpoint(String application, String endpoint) {
250 this.application = application;
251 this.endpoint = endpoint;
252 }
253 }
254
255 public static class PushTarget {
256 public final String application;
257 public final String instance;
258
259 public PushTarget(final String application, final String instance) {
260 this.application = application;
261 this.instance = instance;
262 }
263
264 @NonNull
265 @Override
266 public String toString() {
267 return MoreObjects.toStringHelper(this)
268 .add("application", application)
269 .add("instance", instance)
270 .toString();
271 }
272
273 @Override
274 public boolean equals(Object o) {
275 if (this == o) return true;
276 if (o == null || getClass() != o.getClass()) return false;
277 PushTarget that = (PushTarget) o;
278 return Objects.equal(application, that.application)
279 && Objects.equal(instance, that.instance);
280 }
281
282 @Override
283 public int hashCode() {
284 return Objects.hashCode(application, instance);
285 }
286 }
287}