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