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