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