fix hooking profile-specific auto-completion adapter
[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.content.Context;
22 import android.database.Cursor;
23 import android.database.MatrixCursor;
24 import android.database.sqlite.SQLiteDatabase;
25 import android.os.AsyncTask;
26 import android.os.Build;
27 import android.provider.FontsContract;
28 import android.view.View;
29 import android.widget.AutoCompleteTextView;
30 import android.widget.FilterQueryProvider;
31 import android.widget.SimpleCursorAdapter;
32
33 import net.ktnx.mobileledger.App;
34 import net.ktnx.mobileledger.async.DbOpQueue;
35 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.MobileLedgerProfile;
38
39 import org.jetbrains.annotations.NonNls;
40
41 import java.util.Locale;
42
43 import static net.ktnx.mobileledger.utils.Logger.debug;
44
45 public final class MLDB {
46     public static final String ACCOUNTS_TABLE = "accounts";
47     public static final String DESCRIPTION_HISTORY_TABLE = "description_history";
48     public static final String OPT_LAST_SCRAPE = "last_scrape";
49     @NonNls
50     public static final String OPT_PROFILE_UUID = "profile_uuid";
51     private static final String NO_PROFILE = "-";
52     @SuppressWarnings("unused")
53     static public int getIntOption(String name, int default_value) {
54         String s = getOption(name, String.valueOf(default_value));
55         try {
56             return Integer.parseInt(s);
57         }
58         catch (Exception e) {
59             debug("db", "returning default int value of " + name, e);
60             return default_value;
61         }
62     }
63     @SuppressWarnings("unused")
64     static public long getLongOption(String name, long default_value) {
65         String s = getOption(name, String.valueOf(default_value));
66         try {
67             return Long.parseLong(s);
68         }
69         catch (Exception e) {
70             debug("db", "returning default long value of " + name, e);
71             return default_value;
72         }
73     }
74     static public void getOption(String name, String defaultValue, GetOptCallback cb) {
75         AsyncTask<Void, Void, String> t = new AsyncTask<Void, Void, String>() {
76             @Override
77             protected String doInBackground(Void... params) {
78                 SQLiteDatabase db = App.getDatabase();
79                 try (Cursor cursor = db
80                         .rawQuery("select value from options where profile = ? and name=?",
81                                 new String[]{NO_PROFILE, name}))
82                 {
83                     if (cursor.moveToFirst()) {
84                         String result = cursor.getString(0);
85
86                         if (result == null) result = defaultValue;
87
88                         debug("async-db", "option " + name + "=" + result);
89                         return result;
90                     }
91                     else return defaultValue;
92                 }
93                 catch (Exception e) {
94                     debug("db", "returning default value for " + name, e);
95                     return defaultValue;
96                 }
97             }
98             @Override
99             protected void onPostExecute(String result) {
100                 cb.onResult(result);
101             }
102         };
103
104         t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
105     }
106     static public String getOption(String name, String default_value) {
107         debug("db", "about to fetch option " + name);
108         SQLiteDatabase db = App.getDatabase();
109         try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
110                 new String[]{NO_PROFILE, name}))
111         {
112             if (cursor.moveToFirst()) {
113                 String result = cursor.getString(0);
114
115                 if (result == null) result = default_value;
116
117                 debug("db", "option " + name + "=" + result);
118                 return result;
119             }
120             else return default_value;
121         }
122         catch (Exception e) {
123             debug("db", "returning default value for " + name, e);
124             return default_value;
125         }
126     }
127     static public void setOption(String name, String value) {
128         debug("option", String.format("%s := %s", name, value));
129         DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
130                 new String[]{NO_PROFILE, name, value});
131     }
132     @SuppressWarnings("unused")
133     static public void setLongOption(String name, long value) {
134         setOption(name, String.valueOf(value));
135     }
136     @TargetApi(Build.VERSION_CODES.N)
137     public static void hookAutocompletionAdapter(final Context context,
138                                                  final AutoCompleteTextView view,
139                                                  final String table, final String field) {
140         hookAutocompletionAdapter(context, view, table, field, true, null, null, null);
141     }
142     @TargetApi(Build.VERSION_CODES.N)
143     public static void hookAutocompletionAdapter(final Context context,
144                                                  final AutoCompleteTextView view,
145                                                  final String table, final String field,
146                                                  final boolean profileSpecific, final View nextView,
147                                                  final DescriptionSelectedCallback callback,
148                                                  final MobileLedgerProfile profile) {
149         String[] from = {field};
150         int[] to = {android.R.id.text1};
151         SimpleCursorAdapter adapter =
152                 new SimpleCursorAdapter(context, android.R.layout.simple_dropdown_item_1line, null,
153                         from, to, 0);
154         adapter.setStringConversionColumn(1);
155
156         FilterQueryProvider provider = constraint -> {
157             if (constraint == null) return null;
158
159             String str = constraint.toString().toUpperCase();
160             debug("autocompletion", "Looking for " + str);
161             String[] col_names = {FontsContract.Columns._ID, field};
162             MatrixCursor c = new MatrixCursor(col_names);
163
164             String sql;
165             String[] params;
166             if (profileSpecific) {
167                 MobileLedgerProfile p = (profile == null) ? Data.profile.getValue() : profile;
168                 if (p == null) throw new AssertionError();
169                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
170                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
171                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 else 9 end " +
172                                     "FROM %s " +
173                                     "WHERE profile=? AND %s_upper LIKE '%%'||?||'%%' " +
174                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
175                 params = new String[]{str, str, str, p.getUuid(), str};
176             }
177             else {
178                 sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " +
179                                     "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
180                                     "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + "else 9 end " +
181                                     "FROM %s " + "WHERE %s_upper LIKE '%%'||?||'%%' " +
182                                     "ORDER BY 2, 1;", field, field, field, field, table, field);
183                 params = new String[]{str, str, str, str};
184             }
185             debug("autocompletion", sql);
186             SQLiteDatabase db = App.getDatabase();
187
188             try (Cursor matches = db.rawQuery(sql, params)) {
189                 int i = 0;
190                 while (matches.moveToNext()) {
191                     String match = matches.getString(0);
192                     int order = matches.getInt(1);
193                     debug("autocompletion",
194                             String.format(Locale.ENGLISH, "match: %s |%d", match, order));
195                     c.newRow().add(i++).add(match);
196                 }
197             }
198
199             return c;
200
201         };
202
203         adapter.setFilterQueryProvider(provider);
204
205         view.setAdapter(adapter);
206
207         if (nextView != null) {
208             view.setOnItemClickListener((parent, itemView, position, id) -> {
209                 nextView.requestFocus(View.FOCUS_FORWARD);
210                 if (callback != null) {
211                     callback.descriptionSelected(String.valueOf(view.getText()));
212                 }
213             });
214         }
215     }
216 }
217