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