]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java
9089b9e5d9ea9f21508feee9513c947443d64259
[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 import net.ktnx.mobileledger.model.MobileLedgerProfile;
40
41 import org.jetbrains.annotations.NonNls;
42
43 import java.io.BufferedReader;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.InputStreamReader;
47 import java.util.Locale;
48
49 import static net.ktnx.mobileledger.utils.Logger.debug;
50
51 public final class MLDB {
52     public static final String ACCOUNTS_TABLE = "accounts";
53     public static final String DESCRIPTION_HISTORY_TABLE = "description_history";
54     public static final String OPT_LAST_SCRAPE = "last_scrape";
55     @NonNls
56     public static final String OPT_PROFILE_UUID = "profile_uuid";
57     private static final String NO_PROFILE = "-";
58     private static MobileLedgerDatabase dbHelper;
59     private static Application context;
60     private static void checkState() {
61         if (context == null)
62             throw new IllegalStateException("First call init with a valid context");
63     }
64     public static SQLiteDatabase getDatabase() {
65         checkState();
66
67         SQLiteDatabase db;
68
69         db = dbHelper.getWritableDatabase();
70
71         db.execSQL("pragma case_sensitive_like=ON;");
72         return db;
73     }
74     static public int getIntOption(String name, int default_value) {
75         String s = getOption(name, String.valueOf(default_value));
76         try {
77             return Integer.parseInt(s);
78         }
79         catch (Exception e) {
80             debug("db", "returning default int value of " + name, e);
81             return default_value;
82         }
83     }
84     static public long getLongOption(String name, long default_value) {
85         String s = getOption(name, String.valueOf(default_value));
86         try {
87             return Long.parseLong(s);
88         }
89         catch (Exception e) {
90             debug("db", "returning default long value of " + name, e);
91             return default_value;
92         }
93     }
94     static public String getOption(String name, String default_value) {
95         debug("db", "about to fetch option " + name);
96         SQLiteDatabase db = getDatabase();
97         try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
98                 new String[]{NO_PROFILE, name}))
99         {
100             if (cursor.moveToFirst()) {
101                 String result = cursor.getString(0);
102
103                 if (result == null) result = default_value;
104
105                 debug("db", "option " + name + "=" + result);
106                 return result;
107             }
108             else return default_value;
109         }
110         catch (Exception e) {
111             debug("db", "returning default value for " + name, e);
112             return default_value;
113         }
114     }
115     static public void setOption(String name, String value) {
116         debug("option", String.format("%s := %s", name, value));
117         SQLiteDatabase db = MLDB.getDatabase();
118         db.execSQL("insert or replace into options(profile, name, value) values(?, ?, ?);",
119                 new String[]{NO_PROFILE, name, value});
120     }
121     static public void setLongOption(String name, long value) {
122         setOption(name, String.valueOf(value));
123     }
124     @TargetApi(Build.VERSION_CODES.N)
125     public static void hookAutocompletionAdapter(final Context context,
126                                                  final AutoCompleteTextView view,
127                                                  final String table, final String field,
128                                                  final boolean profileSpecific) {
129         hookAutocompletionAdapter(context, view, table, field, profileSpecific, null, null,
130                 Data.profile.getValue());
131     }
132     @TargetApi(Build.VERSION_CODES.N)
133     public static void hookAutocompletionAdapter(final Context context,
134                                                  final AutoCompleteTextView view,
135                                                  final String table, final String field,
136                                                  final boolean profileSpecific, final View nextView,
137                                                  final DescriptionSelectedCallback callback,
138                                                  final MobileLedgerProfile profile) {
139         String[] from = {field};
140         int[] to = {android.R.id.text1};
141         SimpleCursorAdapter adapter =
142                 new SimpleCursorAdapter(context, android.R.layout.simple_dropdown_item_1line, null,
143                         from, to, 0);
144         adapter.setStringConversionColumn(1);
145
146         FilterQueryProvider provider = constraint -> {
147             if (constraint == null) return null;
148
149             String str = constraint.toString().toUpperCase();
150             debug("autocompletion", "Looking for " + str);
151             String[] col_names = {FontsContract.Columns._ID, field};
152             MatrixCursor c = new MatrixCursor(col_names);
153
154             String sql;
155             String[] params;
156             if (profileSpecific) {
157                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
158                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
159                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 else 9 end " +
160                                     "FROM %s " +
161                                     "WHERE profile=? AND %s_upper LIKE '%%'||?||'%%' " +
162                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
163                 params = new String[]{str, str, str, profile.getUuid(), str};
164             }
165             else {
166                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
167                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
168                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + "else 9 end " +
169                                     "FROM %s " + "WHERE %s_upper LIKE '%%'||?||'%%' " +
170                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
171                 params = new String[]{str, str, str, str};
172             }
173             debug("autocompletion", sql);
174             SQLiteDatabase db = MLDB.getDatabase();
175
176             try (Cursor matches = db.rawQuery(sql, params)) {
177                 int i = 0;
178                 while (matches.moveToNext()) {
179                     String match = matches.getString(0);
180                     int order = matches.getInt(1);
181                     debug("autocompletion", String.format("match: %s |%d", match, order));
182                     c.newRow().add(i++).add(match);
183                 }
184             }
185
186             return c;
187
188         };
189
190         adapter.setFilterQueryProvider(provider);
191
192         view.setAdapter(adapter);
193
194         if (nextView != null) {
195             view.setOnItemClickListener((parent, itemView, position, id) -> {
196                 nextView.requestFocus(View.FOCUS_FORWARD);
197                 if (callback != null) {
198                     callback.descriptionSelected(String.valueOf(view.getText()));
199                 }
200             });
201         }
202     }
203     public static synchronized void init(Application context) {
204         MLDB.context = context;
205         if (dbHelper != null)
206             throw new IllegalStateException("It appears init() was already called");
207         dbHelper = new MobileLedgerDatabase(context);
208     }
209     public static synchronized void done() {
210         if (dbHelper != null) {
211             debug("db", "Closing DB helper");
212             dbHelper.close();
213             dbHelper = null;
214         }
215     }
216 }
217
218 class MobileLedgerDatabase extends SQLiteOpenHelper {
219     private static final String DB_NAME = "MoLe.db";
220     private static final int LATEST_REVISION = 22;
221     private static final String CREATE_DB_SQL = "create_db";
222
223     private final Application mContext;
224
225     public MobileLedgerDatabase(Application context) {
226         super(context, DB_NAME, null, LATEST_REVISION);
227         debug("db", "creating helper instance");
228         mContext = context;
229         super.setWriteAheadLoggingEnabled(true);
230     }
231
232     @Override
233     public void onCreate(SQLiteDatabase db) {
234         debug("db", "onCreate called");
235         applyRevisionFile(db, CREATE_DB_SQL);
236     }
237
238     @Override
239     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
240         debug("db", "onUpgrade called");
241         for (int i = oldVersion + 1; i <= newVersion; i++) applyRevision(db, i);
242     }
243
244     private void applyRevision(SQLiteDatabase db, int rev_no) {
245         String rev_file = String.format(Locale.US, "sql_%d", rev_no);
246
247         applyRevisionFile(db, rev_file);
248     }
249     private void applyRevisionFile(SQLiteDatabase db, String rev_file) {
250         final Resources rm = mContext.getResources();
251         int res_id = rm.getIdentifier(rev_file, "raw", mContext.getPackageName());
252         if (res_id == 0)
253             throw new SQLException(String.format(Locale.US, "No resource for %s", rev_file));
254         db.beginTransaction();
255         try (InputStream res = rm.openRawResource(res_id)) {
256             debug("db", "Applying " + rev_file);
257             InputStreamReader isr = new InputStreamReader(res);
258             BufferedReader reader = new BufferedReader(isr);
259
260             String line;
261             int line_no = 1;
262             while ((line = reader.readLine()) != null) {
263                 if (line.startsWith("--")) {
264                     line_no++;
265                     continue;
266                 }
267                 if (line.isEmpty()) {
268                     line_no++;
269                     continue;
270                 }
271                 try {
272                     db.execSQL(line);
273                 }
274                 catch (Exception e) {
275                     throw new RuntimeException(
276                             String.format("Error applying %s, line %d", rev_file, line_no), e);
277                 }
278                 line_no++;
279             }
280
281             db.setTransactionSuccessful();
282         }
283         catch (IOException e) {
284             Log.e("db", String.format("Error opening raw resource for %s", rev_file));
285             e.printStackTrace();
286         }
287         finally {
288             db.endTransaction();
289         }
290     }
291 }