b5317ad3469e7e3f187e1d86a374a814907b9aeb
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / utils / MLDB.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.utils;
19
20 import android.annotation.TargetApi;
21 import android.app.Application;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.database.Cursor;
25 import android.database.MatrixCursor;
26 import android.database.SQLException;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.database.sqlite.SQLiteOpenHelper;
29 import android.os.Build;
30 import android.provider.FontsContract;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.AutoCompleteTextView;
34 import android.widget.FilterQueryProvider;
35 import android.widget.SimpleCursorAdapter;
36
37 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
38 import net.ktnx.mobileledger.model.Data;
39
40 import org.jetbrains.annotations.NonNls;
41
42 import java.io.BufferedReader;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.InputStreamReader;
46 import java.util.Locale;
47
48 public final class MLDB {
49     public static final String ACCOUNTS_TABLE = "accounts";
50     public static final String DESCRIPTION_HISTORY_TABLE = "description_history";
51     public static final String OPT_LAST_SCRAPE = "last_scrape";
52     @NonNls
53     public static final String OPT_PROFILE_UUID = "profile_uuid";
54     private static final String NO_PROFILE = "-";
55     private static MobileLedgerDatabase dbHelper;
56     private static Application context;
57     private static void checkState() {
58         if (context == null)
59             throw new IllegalStateException("First call init with a valid context");
60     }
61     public static SQLiteDatabase getDatabase() {
62         checkState();
63
64         SQLiteDatabase db;
65
66         db = dbHelper.getWritableDatabase();
67
68         db.execSQL("pragma case_sensitive_like=ON;");
69         return db;
70     }
71     static public int getIntOption(String name, int default_value) {
72         String s = getOption(name, String.valueOf(default_value));
73         try {
74             return Integer.parseInt(s);
75         }
76         catch (Exception e) {
77             Log.d("db", "returning default int value of " + name, e);
78             return default_value;
79         }
80     }
81     static public long getLongOption(String name, long default_value) {
82         String s = getOption(name, String.valueOf(default_value));
83         try {
84             return Long.parseLong(s);
85         }
86         catch (Exception e) {
87             Log.d("db", "returning default long value of " + name, e);
88             return default_value;
89         }
90     }
91     static public String getOption(String name, String default_value) {
92         Log.d("db", "about to fetch option " + name);
93         SQLiteDatabase db = getDatabase();
94         try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
95                 new String[]{NO_PROFILE, name}))
96         {
97             if (cursor.moveToFirst()) {
98                 String result = cursor.getString(0);
99
100                 if (result == null) result = default_value;
101
102                 Log.d("db", "option " + name + "=" + result);
103                 return result;
104             }
105             else return default_value;
106         }
107         catch (Exception e) {
108             Log.d("db", "returning default value for " + name, e);
109             return default_value;
110         }
111     }
112     static public void setOption(String name, String value) {
113         Log.d("option", String.format("%s := %s", name, value));
114         SQLiteDatabase db = MLDB.getDatabase();
115         db.execSQL("insert or replace into options(profile, name, value) values(?, ?, ?);",
116                 new String[]{NO_PROFILE, name, value});
117     }
118     static public void setLongOption(String name, long value) {
119         setOption(name, String.valueOf(value));
120     }
121     @TargetApi(Build.VERSION_CODES.N)
122     public static void hookAutocompletionAdapter(final Context context,
123                                                  final AutoCompleteTextView view,
124                                                  final String table, final String field,
125                                                  final boolean profileSpecific) {
126         hookAutocompletionAdapter(context, view, table, field, profileSpecific, null, null);
127     }
128     @TargetApi(Build.VERSION_CODES.N)
129     public static void hookAutocompletionAdapter(final Context context,
130                                                  final AutoCompleteTextView view,
131                                                  final String table, final String field,
132                                                  final boolean profileSpecific, final View nextView,
133                                                  final DescriptionSelectedCallback callback) {
134         String[] from = {field};
135         int[] to = {android.R.id.text1};
136         SimpleCursorAdapter adapter =
137                 new SimpleCursorAdapter(context, android.R.layout.simple_dropdown_item_1line, null,
138                         from, to, 0);
139         adapter.setStringConversionColumn(1);
140
141         FilterQueryProvider provider = constraint -> {
142             if (constraint == null) return null;
143
144             String str = constraint.toString().toUpperCase();
145             Log.d("autocompletion", "Looking for " + str);
146             String[] col_names = {FontsContract.Columns._ID, field};
147             MatrixCursor c = new MatrixCursor(col_names);
148
149             String sql;
150             String[] params;
151             if (profileSpecific) {
152                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
153                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
154                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 else 9 end " +
155                                     "FROM %s " +
156                                     "WHERE profile=? AND %s_upper LIKE '%%'||?||'%%' " +
157                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
158                 params = new String[]{str, str, str, Data.profile.get().getUuid(), str};
159             }
160             else {
161                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
162                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
163                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + "else 9 end " +
164                                     "FROM %s " + "WHERE %s_upper LIKE '%%'||?||'%%' " +
165                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
166                 params = new String[]{str, str, str, str};
167             }
168             Log.d("autocompletion", sql);
169             SQLiteDatabase db = MLDB.getDatabase();
170
171             try (Cursor matches = db.rawQuery(sql, params)) {
172                 int i = 0;
173                 while (matches.moveToNext()) {
174                     String match = matches.getString(0);
175                     int order = matches.getInt(1);
176                     Log.d("autocompletion", String.format("match: %s |%d", match, order));
177                     c.newRow().add(i++).add(match);
178                 }
179             }
180
181             return c;
182
183         };
184
185         adapter.setFilterQueryProvider(provider);
186
187         view.setAdapter(adapter);
188
189         if (nextView != null) {
190             view.setOnItemClickListener((parent, itemView, position, id) -> {
191                 nextView.requestFocus(View.FOCUS_FORWARD);
192                 if (callback != null) {
193                     callback.descriptionSelected(String.valueOf(view.getText()));
194                 }
195             });
196         }
197     }
198     public static synchronized void init(Application context) {
199         MLDB.context = context;
200         if (dbHelper != null) throw new IllegalStateException("It appears init() was already called");
201         dbHelper = new MobileLedgerDatabase(context);
202     }
203     public static synchronized void done() {
204         if (dbHelper != null) {
205             Log.d("db", "Closing DB helper");
206             dbHelper.close();
207             dbHelper = null;
208         }
209     }
210 }
211
212 class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable {
213     private static final String DB_NAME = "MoLe.db";
214     private static final int LATEST_REVISION = 20;
215     private static final String CREATE_DB_SQL = "create-db.sql";
216
217     private final Application mContext;
218
219     public MobileLedgerDatabase(Application context) {
220         super(context, DB_NAME, null, LATEST_REVISION);
221         Log.d("db", "creating helper instance");
222         mContext = context;
223         super.setWriteAheadLoggingEnabled(true);
224     }
225
226     @Override
227     public void onCreate(SQLiteDatabase db) {
228         Log.d("db", "onCreate called");
229         applyRevisionFile(db, CREATE_DB_SQL);
230     }
231
232     @Override
233     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
234         Log.d("db", "onUpgrade called");
235         for (int i = oldVersion + 1; i <= newVersion; i++) applyRevision(db, i);
236     }
237
238     private void applyRevision(SQLiteDatabase db, int rev_no) {
239         String rev_file = String.format(Locale.US, "sql_%d", rev_no);
240
241         applyRevisionFile(db, rev_file);
242     }
243     private void applyRevisionFile(SQLiteDatabase db, String rev_file) {
244         final Resources rm = mContext.getResources();
245         int res_id = rm.getIdentifier(rev_file, "raw", mContext.getPackageName());
246         if (res_id == 0)
247             throw new SQLException(String.format(Locale.US, "No resource for %s", rev_file));
248         db.beginTransaction();
249         try (InputStream res = rm.openRawResource(res_id)) {
250             Log.d("db", "Applying " + rev_file);
251             InputStreamReader isr = new InputStreamReader(res);
252             BufferedReader reader = new BufferedReader(isr);
253
254             String line;
255             int line_no = 1;
256             while ((line = reader.readLine()) != null) {
257                 if (line.startsWith("--")) {
258                     line_no++;
259                     continue;
260                 }
261                 if (line.isEmpty()) {
262                     line_no++;
263                     continue;
264                 }
265                 try {
266                     db.execSQL(line);
267                 }
268                 catch (Exception e) {
269                     throw new RuntimeException(
270                             String.format("Error applying %s, line %d", rev_file, line_no), e);
271                 }
272                 line_no++;
273             }
274
275             db.setTransactionSuccessful();
276         }
277         catch (IOException e) {
278             Log.e("db", String.format("Error opening raw resource for %s", rev_file));
279             e.printStackTrace();
280         }
281         finally {
282             db.endTransaction();
283         }
284     }
285 }