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;
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 // TODO use a date somewhat in the future to account for period renewal triggers
88 final long expiration = System.currentTimeMillis();
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 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 + System.currentTimeMillis(),
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 @Override
132 public void onUpgrade(
133 final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {}
134
135 public boolean updateEndpoint(
136 final String instance,
137 final String account,
138 final String transport,
139 final String endpoint,
140 final long expiration) {
141 final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
142 sqLiteDatabase.beginTransaction();
143 final String existingEndpoint;
144 try (final Cursor cursor =
145 sqLiteDatabase.query(
146 "push",
147 new String[] {"endpoint"},
148 "instance=?",
149 new String[] {instance},
150 null,
151 null,
152 null)) {
153 if (cursor != null && cursor.moveToFirst()) {
154 existingEndpoint = cursor.getString(0);
155 } else {
156 existingEndpoint = null;
157 }
158 }
159 final ContentValues contentValues = new ContentValues();
160 contentValues.put("account", account);
161 contentValues.put("transport", transport);
162 contentValues.put("endpoint", endpoint);
163 contentValues.put("expiration", expiration);
164 sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance});
165 sqLiteDatabase.setTransactionSuccessful();
166 sqLiteDatabase.endTransaction();
167 return !endpoint.equals(existingEndpoint);
168 }
169
170 public List<PushTarget> getPushTargets(final String account, final String transport) {
171 final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
172 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
173 try (final Cursor cursor =
174 sqLiteDatabase.query(
175 "push",
176 new String[] {"application", "instance"},
177 "account = ?",
178 new String[] {account},
179 null,
180 null,
181 null)) {
182 while (cursor != null && cursor.moveToNext()) {
183 renewalBuilder.add(
184 new PushTarget(
185 cursor.getString(cursor.getColumnIndexOrThrow("application")),
186 cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
187 }
188 }
189 return renewalBuilder.build();
190 }
191
192 public boolean deleteInstance(final String instance) {
193 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
194 final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance});
195 return rows >= 1;
196 }
197
198 public boolean deleteApplication(final String application) {
199 final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
200 final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application});
201 return rows >= 1;
202 }
203
204 public static class ApplicationEndpoint {
205 public final String application;
206 public final String endpoint;
207
208 public ApplicationEndpoint(String application, String endpoint) {
209 this.application = application;
210 this.endpoint = endpoint;
211 }
212 }
213
214 public static class PushTarget {
215 public final String application;
216 public final String instance;
217
218 public PushTarget(final String application, final String instance) {
219 this.application = application;
220 this.instance = instance;
221 }
222
223 @NotNull
224 @Override
225 public String toString() {
226 return MoreObjects.toStringHelper(this)
227 .add("application", application)
228 .add("instance", instance)
229 .toString();
230 }
231
232 @Override
233 public boolean equals(Object o) {
234 if (this == o) return true;
235 if (o == null || getClass() != o.getClass()) return false;
236 PushTarget that = (PushTarget) o;
237 return Objects.equal(application, that.application)
238 && Objects.equal(instance, that.instance);
239 }
240
241 @Override
242 public int hashCode() {
243 return Objects.hashCode(application, instance);
244 }
245 }
246}