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.
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.
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/>.
18 package net.ktnx.mobileledger.utils;
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;
37 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
38 import net.ktnx.mobileledger.model.Data;
39 import net.ktnx.mobileledger.model.MobileLedgerProfile;
41 import org.jetbrains.annotations.NonNls;
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;
49 import static net.ktnx.mobileledger.utils.Logger.debug;
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";
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() {
62 throw new IllegalStateException("First call init with a valid context");
64 public static SQLiteDatabase getDatabase() {
69 db = dbHelper.getWritableDatabase();
71 db.execSQL("pragma case_sensitive_like=ON;");
74 static public int getIntOption(String name, int default_value) {
75 String s = getOption(name, String.valueOf(default_value));
77 return Integer.parseInt(s);
80 debug("db", "returning default int value of " + name, e);
84 static public long getLongOption(String name, long default_value) {
85 String s = getOption(name, String.valueOf(default_value));
87 return Long.parseLong(s);
90 debug("db", "returning default long value of " + name, e);
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}))
100 if (cursor.moveToFirst()) {
101 String result = cursor.getString(0);
103 if (result == null) result = default_value;
105 debug("db", "option " + name + "=" + result);
108 else return default_value;
110 catch (Exception e) {
111 debug("db", "returning default value for " + name, e);
112 return default_value;
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});
121 static public void setLongOption(String name, long value) {
122 setOption(name, String.valueOf(value));
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());
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,
144 adapter.setStringConversionColumn(1);
146 FilterQueryProvider provider = constraint -> {
147 if (constraint == null) return null;
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);
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 " +
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};
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};
173 debug("autocompletion", sql);
174 SQLiteDatabase db = MLDB.getDatabase();
176 try (Cursor matches = db.rawQuery(sql, params)) {
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);
190 adapter.setFilterQueryProvider(provider);
192 view.setAdapter(adapter);
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()));
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);
209 public static synchronized void done() {
210 if (dbHelper != null) {
211 debug("db", "Closing DB helper");
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";
223 private final Application mContext;
225 public MobileLedgerDatabase(Application context) {
226 super(context, DB_NAME, null, LATEST_REVISION);
227 debug("db", "creating helper instance");
229 super.setWriteAheadLoggingEnabled(true);
233 public void onCreate(SQLiteDatabase db) {
234 debug("db", "onCreate called");
235 applyRevisionFile(db, CREATE_DB_SQL);
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);
244 private void applyRevision(SQLiteDatabase db, int rev_no) {
245 String rev_file = String.format(Locale.US, "sql_%d", rev_no);
247 applyRevisionFile(db, rev_file);
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());
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);
262 while ((line = reader.readLine()) != null) {
263 if (line.startsWith("--")) {
267 if (line.isEmpty()) {
274 catch (Exception e) {
275 throw new RuntimeException(
276 String.format("Error applying %s, line %d", rev_file, line_no), e);
281 db.setTransactionSuccessful();
283 catch (IOException e) {
284 Log.e("db", String.format("Error opening raw resource for %s", rev_file));