From 3b365016042215dd73cb4600840aa8199b8322b9 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Mon, 7 Jan 2019 21:03:17 +0000 Subject: [PATCH] somewhat complete profile implementation most of the breakages are fixed, some minor remain --- app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 15 +- .../async/RetrieveTransactionsTask.java | 50 ++--- .../async/SaveTransactionTask.java | 9 +- .../async/UpdateAccountsTask.java | 11 +- .../async/UpdateTransactionsTask.java | 16 +- .../mobileledger/model/LedgerTransaction.java | 49 ++-- .../model/MobileLedgerProfile.java | 156 ++++++++++++- .../AccountSummaryFragment.java | 6 + .../ui/activity/MainActivity.java | 80 +++++-- .../ui/activity/NewTransactionActivity.java | 13 +- .../ui/activity/ProfileListActivity.java | 209 ++++++++++++++++++ .../ui/activity/SettingsActivity.java | 35 +-- .../ui/profiles/ProfileDetailActivity.java | 100 +++++++++ .../ui/profiles/ProfileDetailFragment.java | 158 +++++++++++++ .../TransactionListAdapter.java | 10 +- .../TransactionListFragment.java | 12 +- .../net/ktnx/mobileledger/utils/MLDB.java | 76 +++++-- .../ktnx/mobileledger/utils/NetworkUtil.java | 2 +- .../res/drawable/ic_mode_edit_black_24dp.xml | 21 ++ .../res/drawable/ic_view_list_black_24dp.xml | 21 ++ .../main/res/layout-w900dp/profile_list.xml | 56 +++++ app/src/main/res/layout/activity_main.xml | 9 + .../res/layout/activity_profile_detail.xml | 71 ++++++ .../main/res/layout/activity_profile_list.xml | 59 +++++ app/src/main/res/layout/profile_detail.xml | 130 +++++++++++ app/src/main/res/layout/profile_list.xml | 30 +++ .../main/res/layout/profile_list_content.xml | 53 +++++ app/src/main/res/raw/sql_13.sql | 22 ++ app/src/main/res/raw/sql_14.sql | 8 + app/src/main/res/raw/sql_15.sql | 10 + app/src/main/res/values-bg/strings.xml | 10 +- app/src/main/res/values/dimens.xml | 5 +- app/src/main/res/values/strings.xml | 8 +- app/src/main/res/xml/pref_backend.xml | 62 ------ 35 files changed, 1346 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileListActivity.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java create mode 100644 app/src/main/res/drawable/ic_mode_edit_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_view_list_black_24dp.xml create mode 100644 app/src/main/res/layout-w900dp/profile_list.xml create mode 100644 app/src/main/res/layout/activity_profile_detail.xml create mode 100644 app/src/main/res/layout/activity_profile_list.xml create mode 100644 app/src/main/res/layout/profile_detail.xml create mode 100644 app/src/main/res/layout/profile_list.xml create mode 100644 app/src/main/res/layout/profile_list_content.xml create mode 100644 app/src/main/res/raw/sql_13.sql create mode 100644 app/src/main/res/raw/sql_14.sql create mode 100644 app/src/main/res/raw/sql_15.sql delete mode 100644 app/src/main/res/xml/pref_backend.xml diff --git a/app/build.gradle b/app/build.gradle index db130222..14d4d175 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Damyan Ivanov. + * Copyright © 2019 Damyan Ivanov. * This file is part of Mobile-Ledger. * Mobile-Ledger is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -51,6 +51,7 @@ dependencies { implementation 'com.android.support:design:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'android.arch.lifecycle:extensions:1.1.1' + implementation 'com.android.support:recyclerview-v7:28.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7171718a..de59e4fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ ~ ~ You should have received a copy of the GNU General Public License ~ along with Mobile-Ledger. If not, see . ---> + --> @@ -55,6 +55,19 @@ android:name="android.support.PARENT_ACTIVITY" android:value="net.ktnx.mobileledger.ui.activity.MainActivity" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java index 12e53cb6..508190c7 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -160,7 +160,7 @@ public class RetrieveTransactionsTask acct_name = acct_name.replace("\"", ""); L(String.format("found account: %s", acct_name)); - addAccount(db, acct_name); + profile.storeAccount(acct_name); lastAccount = new LedgerAccount(acct_name); accountList.add(lastAccount); @@ -181,11 +181,8 @@ public class RetrieveTransactionsTask if (currency == null) currency = ""; value = value.replace(',', '.'); L("curr=" + currency + ", value=" + value); - db.execSQL( - "insert or replace into account_values(account, currency, value, keep) values(?, ?, ?, 1);", - new Object[]{lastAccount.getName(), currency, - Float.valueOf(value) - }); + profile.storeAccountValue(lastAccount.getName(), currency, + Float.valueOf(value)); lastAccount.addAmount(Float.parseFloat(value), currency); } @@ -244,16 +241,21 @@ public class RetrieveTransactionsTask if (line.isEmpty()) { // transaction data collected if (transaction.existsInDb(db)) { - db.execSQL("UPDATE transactions SET keep = 1 WHERE id" + - "=?", new Integer[]{transaction.getId()}); + db.execSQL("UPDATE transactions SET keep = 1 WHERE " + + "profile = ? and id=?", + new Object[]{profile.getUuid(), + transaction.getId() + }); matchedTransactionsCount++; if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) { db.execSQL("UPDATE transactions SET keep=1 WHERE " + - "id < ?", - new Integer[]{transaction.getId()}); + "profile = ? and id < ?", + new Object[]{profile.getUuid(), + transaction.getId() + }); success = true; progress.setTotal(progress.getProgress()); publishProgress(progress); @@ -261,12 +263,7 @@ public class RetrieveTransactionsTask } } else { - db.execSQL("DELETE from transactions WHERE id=?", - new Integer[]{transaction.getId()}); - db.execSQL("DELETE from transaction_accounts WHERE " + - "transaction_id=?", - new Integer[]{transaction.getId()}); - transaction.insertInto(db); + profile.storeTransaction(transaction); matchedTransactionsCount = 0; progress.setTotal(maxTransactionId); } @@ -275,6 +272,7 @@ public class RetrieveTransactionsTask L(String.format( "transaction %s saved → expecting transaction", transaction.getId())); + transaction.finishLoading(); transactionList.add(transaction); // sounds like a good idea, but transaction-1 may not be the first one chronologically @@ -291,11 +289,13 @@ public class RetrieveTransactionsTask String acc_name = m.group(1); String amount = m.group(2); String currency = m.group(3); + if (currency == null) currency = ""; amount = amount.replace(',', '.'); transaction.addAccount( new LedgerTransactionAccount(acc_name, Float.valueOf(amount), currency)); - L(String.format("%s = %s", acc_name, amount)); + L(String.format("%d: %s = %s", transaction.getId(), + acc_name, amount)); } else throw new IllegalStateException( String.format("Can't parse transaction %d details", @@ -311,12 +311,13 @@ public class RetrieveTransactionsTask throwIfCancelled(); - db.execSQL("DELETE FROM transactions WHERE keep = 0"); + db.execSQL("DELETE FROM transactions WHERE profile=? AND keep = 0", + new String[]{profile.getUuid()}); db.setTransactionSuccessful(); Log.d("db", "Updating transaction value stamp"); Date now = new Date(); - MLDB.set_option_value(MLDB.OPT_TRANSACTION_LIST_STAMP, now.getTime()); + profile.set_option_value(MLDB.OPT_LAST_SCRAPE, now.getTime()); Data.lastUpdateDate.set(now); Data.transactions.set(transactionList); } @@ -350,17 +351,6 @@ public class RetrieveTransactionsTask private MainActivity getContext() { return contextRef.get(); } - private void addAccount(SQLiteDatabase db, String name) { - do { - LedgerAccount acc = new LedgerAccount(name); - db.execSQL("update accounts set level = ?, keep = 1 where name = ?", - new Object[]{acc.getLevel(), name}); - db.execSQL("insert into accounts(name, name_upper, parent_name, level) select ?,?," + - "?,? " + "where (select changes() = 0)", - new Object[]{name, name.toUpperCase(), acc.getParentName(), acc.getLevel()}); - name = acc.getParentName(); - } while (name != null); - } private void throwIfCancelled() { if (isCancelled()) throw new OperationCanceledException(null); } diff --git a/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java b/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java index 5ccbaaca..913fddc5 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java @@ -17,10 +17,10 @@ package net.ktnx.mobileledger.async; -import android.content.SharedPreferences; import android.os.AsyncTask; import android.util.Log; +import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; import net.ktnx.mobileledger.utils.NetworkUtil; @@ -48,11 +48,6 @@ public class SaveTransactionTask extends AsyncTask> { protected ArrayList doInBackground(Boolean[] onlyStarred) { Data.backgroundTaskCount.incrementAndGet(); + String profileUUID = Data.profile.get().getUuid(); try { ArrayList newList = new ArrayList<>(); - String sql = "SELECT name, hidden FROM accounts"; - if (onlyStarred[0]) sql += " WHERE hidden = 0"; + String sql = "SELECT name, hidden FROM accounts WHERE profile = ?"; + if (onlyStarred[0]) sql += " AND hidden = 0"; sql += " ORDER BY name"; SQLiteDatabase db = MLDB.getReadableDatabase(); - try (Cursor cursor = db.rawQuery(sql, null)) { + try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) { while (cursor.moveToNext()) { LedgerAccount acc = new LedgerAccount(cursor.getString(0)); acc.setHidden(cursor.getInt(1) == 1); try (Cursor c2 = db.rawQuery( - "SELECT value, currency FROM account_values " + "WHERE account = ?", - new String[]{acc.getName()})) + "SELECT value, currency FROM account_values WHERE profile = ? " + + "AND account = ?", new String[]{profileUUID, acc.getName()})) { while (c2.moveToNext()) { acc.addAmount(c2.getFloat(0), c2.getString(1)); diff --git a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java index b3e2e0c5..22511560 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java @@ -32,6 +32,7 @@ import java.util.List; public class UpdateTransactionsTask extends AsyncTask> { protected List doInBackground(String[] filterAccName) { Data.backgroundTaskCount.incrementAndGet(); + String profile_uuid = Data.profile.get().getUuid(); try { ArrayList newList = new ArrayList<>(); @@ -41,26 +42,29 @@ public class UpdateTransactionsTask extends AsyncTask 0 ORDER BY tr.date desc, tr.id desc"; params = filterAccName; } - Log.d("tmp", sql); + Log.d("UTT", sql); SQLiteDatabase db = MLDB.getReadableDatabase(); try (Cursor cursor = db.rawQuery(sql, params)) { while (cursor.moveToNext()) { if (isCancelled()) return null; - newList.add(new LedgerTransaction(cursor.getInt(0))); + int transaction_id = cursor.getInt(0); + newList.add(new LedgerTransaction(transaction_id)); + Log.d("UTT", String.format("got transaction %d", transaction_id)); } Data.transactions.set(newList); - Log.d("transactions", "transaction value updated"); + Log.d("UTT", "transaction list value updated"); } return newList; diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java index 9db9f299..3e5a45fd 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Damyan Ivanov. + * Copyright © 2019 Damyan Ivanov. * This file is part of Mobile-Ledger. * Mobile-Ledger is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -41,11 +41,15 @@ public class LedgerTransaction { return Float.compare(o1.getAmount(), o2.getAmount()); } }; + private String profile; private Integer id; private String date; private String description; private ArrayList accounts; + private String dataHash; + private boolean dataLoaded; public LedgerTransaction(Integer id, String date, String description) { + this.profile = Data.profile.get().getUuid(); this.id = id; this.date = date; this.description = description; @@ -53,17 +57,15 @@ public class LedgerTransaction { this.dataHash = null; dataLoaded = false; } - private String dataHash; - private boolean dataLoaded; - public ArrayList getAccounts() { - return accounts; - } public LedgerTransaction(String date, String description) { this(null, date, description); } public LedgerTransaction(int id) { this(id, null, null); } + public ArrayList getAccounts() { + return accounts; + } public void addAccount(LedgerTransactionAccount item) { accounts.add(item); dataHash = null; @@ -85,22 +87,12 @@ public class LedgerTransaction { public int getId() { return id; } - public void insertInto(SQLiteDatabase db) { - fillDataHash(); - db.execSQL("INSERT INTO transactions(id, date, description, data_hash) values(?,?,?,?)", - new Object[]{id, date, description, dataHash}); - - for (LedgerTransactionAccount item : accounts) { - db.execSQL("INSERT INTO transaction_accounts(transaction_id, account_name, amount, " + - "currency) values(?, ?, ?, ?)", - new Object[]{id, item.getAccountName(), item.getAmount(), item.getCurrency()}); - } - } - private void fillDataHash() { + protected void fillDataHash() { if (dataHash != null) return; try { Digest sha = new Digest(DIGEST_TYPE); StringBuilder data = new StringBuilder(); + data.append(profile); data.append(getId()); data.append('\0'); data.append(getDescription()); @@ -134,26 +126,37 @@ public class LedgerTransaction { public void loadData(SQLiteDatabase db) { if (dataLoaded) return; - try (Cursor cTr = db.rawQuery("SELECT date, description from transactions WHERE id=?", - new String[]{String.valueOf(id)})) + try (Cursor cTr = db + .rawQuery("SELECT date, description from transactions WHERE profile=? AND id=?", + new String[]{profile, String.valueOf(id)})) { if (cTr.moveToFirst()) { date = cTr.getString(0); description = cTr.getString(1); try (Cursor cAcc = db.rawQuery("SELECT account_name, amount, currency FROM " + - "transaction_accounts WHERE transaction_id = ?", - new String[]{String.valueOf(id)})) + "transaction_accounts WHERE " + + "profile=? AND transaction_id = ?", + new String[]{profile, String.valueOf(id)})) { while (cAcc.moveToNext()) { +// Log.d("transactions", +// String.format("Loaded %d: %s %1.2f %s", id, cAcc.getString(0), +// cAcc.getFloat(1), cAcc.getString(2))); addAccount(new LedgerTransactionAccount(cAcc.getString(0), cAcc.getFloat(1), cAcc.getString(2))); } - dataLoaded = true; + finishLoading(); } } } } + public String getDataHash() { + return dataHash; + } + public void finishLoading() { + dataLoaded = true; + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java index af78c59d..72097a3f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java @@ -19,28 +19,39 @@ package net.ktnx.mobileledger.model; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.util.Log; import net.ktnx.mobileledger.utils.MLDB; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public final class MobileLedgerProfile { private String uuid; private String name; private String url; - private boolean useAuthentication; + private boolean authEnabled; private String authUserName; private String authPassword; - public MobileLedgerProfile(String uuid, String name, String url, boolean useAuthentication, + public MobileLedgerProfile(String uuid, String name, String url, boolean authEnabled, String authUserName, String authPassword) { this.uuid = uuid; this.name = name; this.url = url; - this.useAuthentication = useAuthentication; + this.authEnabled = authEnabled; this.authUserName = authUserName; this.authPassword = authPassword; } + public MobileLedgerProfile(CharSequence name, CharSequence url, boolean authEnabled, + CharSequence authUserName, CharSequence authPassword) { + this.uuid = String.valueOf(UUID.randomUUID()); + this.name = String.valueOf(name); + this.url = String.valueOf(url); + this.authEnabled = authEnabled; + this.authUserName = String.valueOf(authUserName); + this.authPassword = String.valueOf(authPassword); + } public static List loadAllFromDB() { List result = new ArrayList<>(); SQLiteDatabase db = MLDB.getReadableDatabase(); @@ -48,13 +59,21 @@ public final class MobileLedgerProfile { "auth_password FROM profiles", null)) { while (cursor.moveToNext()) { - result.add(new MobileLedgerProfile(cursor.getString(0), cursor.getString(1), - cursor.getString(2), cursor.getInt(3) == 1, cursor.getString(4), + result.add(new MobileLedgerProfile(cursor.getString(0), cursor.getString(1), cursor.getString(2), cursor.getInt(3) == 1, cursor.getString(4), cursor.getString(5))); } } return result; } + public static List createInitialProfileList() { + List result = new ArrayList<>(); + MobileLedgerProfile first = + new MobileLedgerProfile(UUID.randomUUID().toString(), "default", "", false, "", ""); + first.storeInDB(); + result.add(first); + + return result; + } public static MobileLedgerProfile loadUUIDFromDB(String profileUUID) { SQLiteDatabase db = MLDB.getReadableDatabase(); String name; @@ -90,27 +109,53 @@ public final class MobileLedgerProfile { public String getName() { return name; } + public void setName(CharSequence text) { + setName(String.valueOf(text)); + } + public void setName(String name) { + this.name = name; + } public String getUrl() { return url; } - public boolean isUseAuthentication() { - return useAuthentication; + public void setUrl(CharSequence text) { + setUrl(String.valueOf(text)); + } + public void setUrl(String url) { + this.url = url; + } + public boolean isAuthEnabled() { + return authEnabled; + } + public void setAuthEnabled(boolean authEnabled) { + this.authEnabled = authEnabled; } public String getAuthUserName() { return authUserName; } + public void setAuthUserName(CharSequence text) { + setAuthUserName(String.valueOf(text)); + } + public void setAuthUserName(String authUserName) { + this.authUserName = authUserName; + } public String getAuthPassword() { return authPassword; } + public void setAuthPassword(CharSequence text) { + setAuthPassword(String.valueOf(text)); + } + public void setAuthPassword(String authPassword) { + this.authPassword = authPassword; + } public void storeInDB() { SQLiteDatabase db = MLDB.getWritableDatabase(); db.beginTransaction(); try { db.execSQL("REPLACE INTO profiles(uuid, name, url, use_authentication, auth_user, " + "auth_password) VALUES(?, ?, ?, ?, ?, ?)", - new Object[]{uuid, name, url, useAuthentication, - useAuthentication ? authUserName : null, - useAuthentication ? authPassword : null + new Object[]{uuid, name, url, authEnabled, authEnabled ? authUserName : null, + authEnabled ? authPassword : null }); db.setTransactionSuccessful(); } @@ -118,4 +163,95 @@ public final class MobileLedgerProfile { db.endTransaction(); } } + public void storeAccount(String name) { + SQLiteDatabase db = MLDB.getWritableDatabase(); + + do { + LedgerAccount acc = new LedgerAccount(name); + db.execSQL("replace into accounts(profile, name, name_upper, level, keep) values(?, " + + "?, ?, ?, 1)", + new Object[]{this.uuid, name, name.toUpperCase(), acc.getLevel()}); + name = acc.getParentName(); + } while (name != null); + } + public void storeAccountValue(String name, String currency, Float amount) { + SQLiteDatabase db = MLDB.getWritableDatabase(); + db.execSQL("replace into account_values(profile, account, " + + "currency, value, keep) values(?, ?, ?, ?, 1);", + new Object[]{uuid, name, currency, amount}); + } + public void storeTransaction(LedgerTransaction tr) { + SQLiteDatabase db = MLDB.getWritableDatabase(); + tr.fillDataHash(); + db.execSQL("DELETE from transactions WHERE profile=? and id=?", + new Object[]{uuid, tr.getId()}); + db.execSQL("DELETE from transaction_accounts WHERE profile = ? and transaction_id=?", + new Object[]{uuid, tr.getId()}); + + db.execSQL("INSERT INTO transactions(profile, id, date, description, data_hash, keep) " + + "values(?,?,?,?,?,1)", + new Object[]{uuid, tr.getId(), tr.getDate(), tr.getDescription(), tr.getDataHash() + }); + + for (LedgerTransactionAccount item : tr.getAccounts()) { + db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " + + "account_name, amount, currency) values(?, ?, ?, ?, ?)", + new Object[]{uuid, tr.getId(), item.getAccountName(), item.getAmount(), + item.getCurrency() + }); + } + Log.d("profile", String.format("Transaction %d stored", tr.getId())); + } + public String get_option_value(String name, String default_value) { + SQLiteDatabase db = MLDB.getReadableDatabase(); + try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?", + new String[]{uuid, name})) + { + if (cursor.moveToFirst()) { + String result = cursor.getString(0); + + if (result == null) { + Log.d("profile", "returning default value for " + name); + result = default_value; + } + else Log.d("profile", String.format("option %s=%s", name, result)); + + return result; + } + else return default_value; + } + catch (Exception e) { + Log.d("db", "returning default value for " + name, e); + return default_value; + } + } + public long get_option_value(String name, long default_value) { + long longResult; + String result = get_option_value(name, ""); + if ((result == null) || result.isEmpty()) { + Log.d("profile", String.format("Returning default value for option %s", name)); + longResult = default_value; + } + else { + try { + longResult = Long.parseLong(result); + Log.d("profile", String.format("option %s=%s", name, result)); + } + catch (Exception e) { + Log.d("profile", String.format("Returning default value for option %s", name), e); + longResult = default_value; + } + } + + return longResult; + } + public void set_option_value(String name, String value) { + Log.d("profile", String.format("setting option %s=%s", name, value)); + SQLiteDatabase db = MLDB.getWritableDatabase(); + db.execSQL("insert or replace into options(profile, name, value) values(?, ?, ?);", + new String[]{uuid, name, value}); + } + public void set_option_value(String name, long value) { + set_option_value(name, String.valueOf(value)); + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java index 830bf636..1f43c61e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java @@ -174,6 +174,12 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()); } }); + Data.profile.addObserver(new Observer() { + @Override + public void update(Observable o, Object arg) { + mActivity.runOnUiThread(() -> model.scheduleAccountListReload(mActivity)); + } + }); update_account_table(); } private void update_account_table() { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java index 5615a2c7..7386de0c 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -54,9 +54,9 @@ import java.lang.ref.WeakReference; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; +import java.util.List; import java.util.Observable; import java.util.Observer; -import java.util.UUID; public class MainActivity extends AppCompatActivity { public MobileLedgerListFragment currentFragment = null; @@ -108,28 +108,7 @@ public class MainActivity extends AppCompatActivity { } }); - String profileUUID = MLDB.get_option_value(MLDB.OPT_PROFILE_UUID, null); - if (profileUUID == null) { - SharedPreferences backend = getSharedPreferences("backend", MODE_PRIVATE); - Log.d("profiles", "Migrating from preferences to profiles"); - // migration to multiple profiles - profileUUID = UUID.randomUUID().toString(); - MobileLedgerProfile profile = new MobileLedgerProfile(profileUUID, "default", - backend.getString("backend_url", ""), - backend.getBoolean("backend_use_http_auth", false), - backend.getString("backend_auth_user", null), - backend.getString("backend_auth_password", null)); - profile.storeInDB(); - SharedPreferences.Editor editor = backend.edit(); - editor.clear(); - editor.apply(); - Data.profile.set(profile); - MLDB.set_option_value(MLDB.OPT_PROFILE_UUID, profileUUID); - } - else { - MobileLedgerProfile profile = MobileLedgerProfile.loadUUIDFromDB(profileUUID); - Data.profile.set(profile); - } + setupProfile(); drawer = findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = @@ -204,6 +183,54 @@ public class MainActivity extends AppCompatActivity { } }); } + private void setupProfile() { + List profiles = MobileLedgerProfile.loadAllFromDB(); + MobileLedgerProfile profile = null; + + String profileUUID = MLDB.get_option_value(MLDB.OPT_PROFILE_UUID, null); + if (profileUUID == null) { + if (profiles.isEmpty()) { + profiles = MobileLedgerProfile.createInitialProfileList(); + profile = profiles.get(0); + + SharedPreferences backend = getSharedPreferences("backend", MODE_PRIVATE); + Log.d("profiles", "Migrating from preferences to profiles"); + // migration to multiple profiles + if (profile.getUrl().isEmpty()) { + // no legacy config + Intent intent = new Intent(this, ProfileListActivity.class); + startActivity(intent); + } + profile.setUrl(backend.getString("backend_url", "")); + profile.setAuthEnabled(backend.getBoolean("backend_use_http_auth", false)); + profile.setAuthUserName(backend.getString("backend_auth_user", null)); + profile.setAuthPassword(backend.getString("backend_auth_password", null)); + profile.storeInDB(); + SharedPreferences.Editor editor = backend.edit(); + editor.clear(); + editor.apply(); + } + } + else { + profile = MobileLedgerProfile.loadUUIDFromDB(profileUUID); + } + + if (profile == null) profile = profiles.get(0); + + if (profile == null) throw new AssertionError("profile must have a value"); + + Data.profile.set(profile); + MLDB.set_option_value(MLDB.OPT_PROFILE_UUID, profile.getUuid()); + + if (profile.getUrl().isEmpty()) { + Intent intent = new Intent(this, ProfileListActivity.class); + Bundle args = new Bundle(); + args.putInt(ProfileListActivity.ARG_ACTION, ProfileListActivity.ACTION_EDIT_PROFILE); + args.putInt(ProfileListActivity.ARG_PROFILE_INDEX, 0); + intent.putExtras(args); + startActivity(intent, args); + } + } public void fab_new_transaction_clicked(View view) { Intent intent = new Intent(this, NewTransactionActivity.class); startActivity(intent); @@ -332,7 +359,7 @@ public class MainActivity extends AppCompatActivity { } public void updateLastUpdateTextFromDB() { { - long last_update = MLDB.get_option_value(MLDB.OPT_TRANSACTION_LIST_STAMP, 0L); + long last_update = Data.profile.get().get_option_value(MLDB.OPT_LAST_SCRAPE, 0L); Log.d("transactions", String.format("Last update = %d", last_update)); if (last_update == 0) { @@ -382,6 +409,11 @@ public class MainActivity extends AppCompatActivity { progressBar.setIndeterminate(false); } } + public void nav_profiles_clicked(View view) { + drawer.closeDrawers(); + Intent intent = new Intent(this, ProfileListActivity.class); + startActivity(intent); + } public class SectionsPagerAdapter extends FragmentPagerAdapter { public SectionsPagerAdapter(FragmentManager fm) { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java index 43c10b27..bc904a09 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Damyan Ivanov. + * Copyright © 2019 Damyan Ivanov. * This file is part of Mobile-Ledger. * Mobile-Ledger is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -19,7 +19,6 @@ package net.ktnx.mobileledger.ui.activity; import android.annotation.SuppressLint; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.design.widget.BaseTransientBottomBar; import android.support.design.widget.Snackbar; import android.support.v4.app.DialogFragment; @@ -43,13 +42,13 @@ import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; -import net.ktnx.mobileledger.ui.OnSwipeTouchListener; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.async.SaveTransactionTask; import net.ktnx.mobileledger.async.TaskCallback; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; import net.ktnx.mobileledger.ui.DatePickerFragment; +import net.ktnx.mobileledger.ui.OnSwipeTouchListener; import net.ktnx.mobileledger.utils.MLDB; import java.util.Date; @@ -91,7 +90,7 @@ public class NewTransactionActivity extends AppCompatActivity implements TaskCal }); text_descr = findViewById(R.id.new_transaction_description); MLDB.hook_autocompletion_adapter(this, text_descr, MLDB.DESCRIPTION_HISTORY_TABLE, - "description"); + "description", false); hook_text_change_listener(text_descr); progress = findViewById(R.id.save_transaction_progress); @@ -103,7 +102,8 @@ public class NewTransactionActivity extends AppCompatActivity implements TaskCal AutoCompleteTextView acc_name_view = (AutoCompleteTextView) row.getChildAt(0); TextView amount_view = (TextView) row.getChildAt(1); hook_swipe_listener(row); - MLDB.hook_autocompletion_adapter(this, acc_name_view, MLDB.ACCOUNTS_TABLE, "name"); + MLDB.hook_autocompletion_adapter(this, acc_name_view, MLDB.ACCOUNTS_TABLE, "name", + true); hook_text_change_listener(acc_name_view); hook_text_change_listener(amount_view); // Log.d("swipe", "hooked to row "+i); @@ -139,7 +139,6 @@ public class NewTransactionActivity extends AppCompatActivity implements TaskCal saver = new SaveTransactionTask(this); - saver.setPref(PreferenceManager.getDefaultSharedPreferences(this)); String date = text_date.getText().toString(); if (date.isEmpty()) date = String.valueOf(new Date().getDate()); LedgerTransaction tr = new LedgerTransaction(date, text_descr.getText().toString()); @@ -300,7 +299,7 @@ public class NewTransactionActivity extends AppCompatActivity implements TaskCal if (focus) acc.requestFocus(); hook_swipe_listener(row); - MLDB.hook_autocompletion_adapter(this, acc, MLDB.ACCOUNTS_TABLE, "name"); + MLDB.hook_autocompletion_adapter(this, acc, MLDB.ACCOUNTS_TABLE, "name", true); hook_text_change_listener(acc); hook_text_change_listener(amt); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileListActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileListActivity.java new file mode 100644 index 00000000..3cd61712 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileListActivity.java @@ -0,0 +1,209 @@ +/* + * Copyright © 2019 Damyan Ivanov. + * This file is part of Mobile-Ledger. + * Mobile-Ledger is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * Mobile-Ledger is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with Mobile-Ledger. If not, see . + */ + +package net.ktnx.mobileledger.ui.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.RadioButton; +import android.widget.TextView; + +import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity; +import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment; +import net.ktnx.mobileledger.utils.MLDB; + +import java.util.List; +import java.util.Observable; +import java.util.Observer; + +/** + * An activity representing a list of Profiles. This activity + * has different presentations for handset and tablet-size devices. On + * handsets, the activity presents a list of items, which when touched, + * lead to a {@link ProfileDetailActivity} representing + * item details. On tablets, the activity presents the list of items and + * item details side-by-side using two vertical panes. + */ +public class ProfileListActivity extends AppCompatActivity { + + public static final String ARG_ACTION = "action"; + public static final String ARG_PROFILE_INDEX = "profile_uuid"; + public static final int ACTION_EDIT_PROFILE = 1; + public static final int ACTION_INVALID = -1; + /** + * Whether or not the activity is in two-pane mode, i.e. running on a tablet + * device. + */ + private boolean mTwoPane; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_profile_list); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + toolbar.setTitle(getTitle()); + + RecyclerView recyclerView = findViewById(R.id.profile_list); + if (recyclerView == null) throw new AssertionError(); + setupRecyclerView(recyclerView); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ProfilesRecyclerViewAdapter adapter = + (ProfilesRecyclerViewAdapter) recyclerView.getAdapter(); + if (adapter != null) adapter.editProfile(recyclerView, null); + } + }); + + if (findViewById(R.id.profile_detail_container) != null) { + // The detail container view will be present only in the + // large-screen layouts (res/values-w900dp). + // If this view is present, then the + // activity should be in two-pane mode. + mTwoPane = true; + } + + int action = getIntent().getIntExtra(ARG_ACTION, ACTION_INVALID); + if (action == ACTION_EDIT_PROFILE) { + Log.d("profiles", "got edit profile action"); + int index = getIntent().getIntExtra(ARG_PROFILE_INDEX, -1); + if (index >= 0) { + List list = MobileLedgerProfile.loadAllFromDB(); + if (index < list.size()) { + ProfilesRecyclerViewAdapter adapter = + (ProfilesRecyclerViewAdapter) recyclerView.getAdapter(); + if (adapter != null) adapter.editProfile(recyclerView, list.get(index)); + } + } + } + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + List list = MobileLedgerProfile.loadAllFromDB(); + recyclerView.setAdapter(new ProfilesRecyclerViewAdapter(this, list, mTwoPane)); + } + + public static class ProfilesRecyclerViewAdapter + extends RecyclerView.Adapter { + + private final ProfileListActivity mParentActivity; + private final List mValues; + private final boolean mTwoPane; + private final View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + MobileLedgerProfile item = (MobileLedgerProfile) ((View) view.getParent()).getTag(); + editProfile(view, item); + } + }; + ProfilesRecyclerViewAdapter(ProfileListActivity parent, List items, + boolean twoPane) { + mValues = items; + mParentActivity = parent; + mTwoPane = twoPane; + } + private void editProfile(View view, MobileLedgerProfile item) { + if (mTwoPane) { + Bundle arguments = new Bundle(); + arguments.putString(ProfileDetailFragment.ARG_ITEM_ID, item.getUuid()); + ProfileDetailFragment fragment = new ProfileDetailFragment(); + fragment.setArguments(arguments); + mParentActivity.getSupportFragmentManager().beginTransaction() + .replace(R.id.profile_detail_container, fragment).commit(); + } + else { + Context context = view.getContext(); + Intent intent = new Intent(context, ProfileDetailActivity.class); + intent.putExtra(ProfileDetailFragment.ARG_ITEM_ID, + (item == null) ? null : item.getUuid()); + + context.startActivity(intent); + } + } + @NonNull + @Override + public ProfileListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.profile_list_content, parent, false); + ProfileListViewHolder holder = new ProfileListViewHolder(view); + Data.profile.addObserver(new Observer() { + @Override + public void update(Observable o, Object arg) { + MobileLedgerProfile newProfile = Data.profile.get(); + MobileLedgerProfile profile = (MobileLedgerProfile) holder.itemView.getTag(); + holder.mRadioView.setChecked( + newProfile != null && newProfile.getUuid().equals(profile.getUuid())); + } + }); + return holder; + } + @Override + public void onBindViewHolder(@NonNull final ProfileListViewHolder holder, int position) { + final MobileLedgerProfile profile = mValues.get(position); + final MobileLedgerProfile currentProfile = Data.profile.get(); + Log.d("profiles", String.format("pos %d: %s, current: %s", position, profile.getUuid(), + currentProfile.getUuid())); + holder.mRadioView.setText(profile.getName()); + holder.mRadioView.setChecked(profile.getUuid().equals(currentProfile.getUuid())); + holder.mRadioView + .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (!isChecked) return; + MLDB.set_option_value(MLDB.OPT_PROFILE_UUID, profile.getUuid()); + Data.profile.set(profile); + } + }); + + holder.itemView.setTag(profile); + holder.mEditButton.setOnClickListener(mOnClickListener); + + } + @Override + public int getItemCount() { + return mValues.size(); + } + class ProfileListViewHolder extends RecyclerView.ViewHolder { + final RadioButton mRadioView; + final TextView mEditButton; + + ProfileListViewHolder(View view) { + super(view); + mRadioView = view.findViewById(R.id.profile_list_radio); + mEditButton = view.findViewById(R.id.profile_list_edit_button); + } + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java index 8f6ff2fb..3645d828 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Damyan Ivanov. + * Copyright © 2019 Damyan Ivanov. * This file is part of Mobile-Ledger. * Mobile-Ledger is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -186,44 +186,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity { */ protected boolean isValidFragment(String fragmentName) { return PreferenceFragment.class.getName().equals(fragmentName) - || BackendPreferenceFragment.class.getName().equals(fragmentName) || DataSyncPreferenceFragment.class.getName().equals(fragmentName) || NotificationPreferenceFragment.class.getName().equals(fragmentName) || InterfacePreferenceFragment.class.getName().equals(fragmentName); } - /** - * This fragment shows general preferences only. It is used when the - * activity is showing a two-pane settings UI. - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class BackendPreferenceFragment extends PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_backend); - setHasOptionsMenu(true); - - // Bind the summaries of EditText/List/Dialog/Ringtone preferences - // to their values. When their values change, their summaries are - // updated to reflect the new value, per the Android Design - // guidelines. - bindPreferenceSummaryToValue(findPreference("backend_url")); - bindPreferenceSummaryToValue(findPreference("backend_auth_user")); - - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - startActivity(new Intent(getActivity(), SettingsActivity.class)); - return true; - } - return super.onOptionsItemSelected(item); - } - } - /** * This fragment shows general preferences only. It is used when the * activity is showing a two-pane settings UI. diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java new file mode 100644 index 00000000..09099732 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java @@ -0,0 +1,100 @@ +/* + * Copyright © 2019 Damyan Ivanov. + * This file is part of Mobile-Ledger. + * Mobile-Ledger is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * Mobile-Ledger is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with Mobile-Ledger. If not, see . + */ + +package net.ktnx.mobileledger.ui.profiles; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.ActionBar; +import android.view.MenuItem; + +import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.ui.activity.ProfileListActivity; + +/** + * An activity representing a single Profile detail screen. This + * activity is only used on narrow width devices. On tablet-size devices, + * item details are presented side-by-side with a list of items + * in a {@link ProfileListActivity}. + */ +public class ProfileDetailActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_profile_detail); + Toolbar toolbar = (Toolbar) findViewById(R.id.detail_toolbar); + setSupportActionBar(toolbar); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Snackbar.make(view, "Replace with your own detail action", Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + }); + + // Show the Up button in the action bar. + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + // savedInstanceState is non-null when there is fragment state + // saved from previous configurations of this activity + // (e.g. when rotating the screen from portrait to landscape). + // In this case, the fragment will automatically be re-added + // to its container so we don't need to manually add it. + // For more information, see the Fragments API guide at: + // + // http://developer.android.com/guide/components/fragments.html + // + if (savedInstanceState == null) { + // Create the detail fragment and add it to the activity + // using a fragment transaction. + Bundle arguments = new Bundle(); + arguments.putString(ProfileDetailFragment.ARG_ITEM_ID, + getIntent().getStringExtra(ProfileDetailFragment.ARG_ITEM_ID)); + ProfileDetailFragment fragment = new ProfileDetailFragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction() + .add(R.id.profile_detail_container, fragment).commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + // This ID represents the Home or Up button. In the case of this + // activity, the Up button is shown. For + // more details, see the Navigation pattern on Android Design: + // + // http://developer.android.com/design/patterns/navigation.html#up-vs-back + // + navigateUpTo(new Intent(this, ProfileListActivity.class)); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java new file mode 100644 index 00000000..ff1b5e5b --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java @@ -0,0 +1,158 @@ +/* + * Copyright © 2019 Damyan Ivanov. + * This file is part of Mobile-Ledger. + * Mobile-Ledger is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * Mobile-Ledger is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with Mobile-Ledger. If not, see . + */ + +package net.ktnx.mobileledger.ui.profiles; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.CollapsingToolbarLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.Switch; +import android.widget.TextView; + +import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.ui.activity.ProfileListActivity; + +/** + * A fragment representing a single Profile detail screen. + * This fragment is either contained in a {@link ProfileListActivity} + * in two-pane mode (on tablets) or a {@link ProfileDetailActivity} + * on handsets. + */ +public class ProfileDetailFragment extends Fragment { + /** + * The fragment argument representing the item ID that this fragment + * represents. + */ + public static final String ARG_ITEM_ID = "item_id"; + + /** + * The dummy content this fragment is presenting. + */ + private MobileLedgerProfile mItem; + private TextView url; + private LinearLayout authParams; + private Switch useAuthentication; + private TextView userName; + private TextView password; + private FloatingActionButton fab; + private TextView profileName; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public ProfileDetailFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) { + // Load the dummy content specified by the fragment + // arguments. In a real-world scenario, use a Loader + // to load content from a content provider. + String uuid = getArguments().getString(ARG_ITEM_ID); + if (uuid != null) + mItem = MobileLedgerProfile.loadUUIDFromDB(getArguments().getString(ARG_ITEM_ID)); + + Activity activity = this.getActivity(); + if (activity == null) throw new AssertionError(); + CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout); + if (appBarLayout != null) { + if (mItem != null) appBarLayout.setTitle(mItem.getName()); + else appBarLayout.setTitle(getResources().getString(R.string.new_profile_title)); + } + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + fab = ((Activity) context).findViewById(R.id.fab); + fab.setOnClickListener(v -> { + if (mItem != null) { + mItem.setName(profileName.getText()); + mItem.setUrl(url.getText()); + mItem.setAuthEnabled(useAuthentication.isChecked()); + mItem.setAuthUserName(userName.getText()); + mItem.setAuthPassword(password.getText()); + mItem.storeInDB(); + + + if (mItem.getUuid().equals(Data.profile.get().getUuid())) { + Data.profile.set(mItem); + } + } + else { + mItem = new MobileLedgerProfile(profileName.getText(), url.getText(), + useAuthentication.isChecked(), userName.getText(), password.getText()); + mItem.storeInDB(); + } + + Activity activity = getActivity(); + if (activity != null) activity.finish(); + }); + } + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.profile_detail, container, false); + + profileName = rootView.findViewById(R.id.profile_name); + url = rootView.findViewById(R.id.url); + authParams = rootView.findViewById(R.id.auth_params); + useAuthentication = rootView.findViewById(R.id.enable_http_auth); + userName = rootView.findViewById(R.id.auth_user_name); + password = rootView.findViewById(R.id.password); + + useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> { + Log.d("profiles", isChecked ? "auth enabled " : "auth disabled"); + authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + + if (mItem != null) { + profileName.setText(mItem.getName()); + url.setText(mItem.getUrl()); + useAuthentication.setChecked(mItem.isAuthEnabled()); + authParams.setVisibility(mItem.isAuthEnabled() ? View.VISIBLE : View.GONE); + userName.setText(mItem.isAuthEnabled() ? mItem.getAuthUserName() : ""); + password.setText(mItem.isAuthEnabled() ? mItem.getAuthPassword() : ""); + } + else { + profileName.setText(""); + url.setText(""); + useAuthentication.setChecked(false); + authParams.setVisibility(View.GONE); + userName.setText(""); + password.setText(""); + } + + return rootView; + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java index bcdc1982..b824f2c7 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Damyan Ivanov. + * Copyright © 2019 Damyan Ivanov. * This file is part of Mobile-Ledger. * Mobile-Ledger is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -50,7 +50,8 @@ public class TransactionListAdapter extends RecyclerView.Adapter parent, View view, int position, long id) { @@ -177,6 +177,16 @@ public class TransactionListFragment extends MobileLedgerListFragment { Log.d("flow", String.format("Account filter set to '%s'", mShowOnlyAccountName)); } + Data.profile.addObserver(new Observer() { + @Override + public void update(Observable o, Object arg) { + mActivity.runOnUiThread(() -> { + Log.d("transactions", "requesting list reload"); + TransactionListViewModel.scheduleTransactionListReload(mActivity); + }); + } + }); + TransactionListViewModel.scheduleTransactionListReload(mActivity); TransactionListViewModel.updating.addObserver(new Observer() { @Override diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java b/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java index d6fa7774..b863cf6e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java @@ -33,6 +33,8 @@ import android.widget.AutoCompleteTextView; import android.widget.FilterQueryProvider; import android.widget.SimpleCursorAdapter; +import net.ktnx.mobileledger.model.Data; + import org.jetbrains.annotations.NonNls; import java.io.BufferedReader; @@ -47,10 +49,10 @@ import static net.ktnx.mobileledger.utils.MLDB.DatabaseMode.WRITE; public final class MLDB { public static final String ACCOUNTS_TABLE = "accounts"; public static final String DESCRIPTION_HISTORY_TABLE = "description_history"; - public static final String OPT_TRANSACTION_LIST_STAMP = "transaction_list_last_update"; - public static final String OPT_LAST_REFRESH = "last_refresh"; + public static final String OPT_LAST_SCRAPE = "last_scrape"; @NonNls public static final String OPT_PROFILE_UUID = "profile_uuid"; + private static final String NO_PROFILE = "-"; private static MobileLedgerDatabase helperForReading, helperForWriting; private static Application context; private static void checkState() { @@ -98,8 +100,8 @@ public final class MLDB { static public String get_option_value(String name, String default_value) { Log.d("db", "about to fetch option " + name); SQLiteDatabase db = getReadableDatabase(); - try (Cursor cursor = db - .rawQuery("select value from options where name=?", new String[]{name})) + try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?", + new String[]{NO_PROFILE, name})) { if (cursor.moveToFirst()) { String result = cursor.getString(0); @@ -117,18 +119,19 @@ public final class MLDB { } } static public void set_option_value(String name, String value) { - Log.d("db", "setting option " + name + "=" + value); - SQLiteDatabase db = getWritableDatabase(); - db.execSQL("insert or replace into options(name, value) values(?, ?);", - new String[]{name, value}); + Log.d("option", String.format("%s := %s", name, value)); + SQLiteDatabase db = MLDB.getWritableDatabase(); + db.execSQL("insert or replace into options(profile, name, value) values(?, ?, ?);", + new String[]{NO_PROFILE, name, value}); } static public void set_option_value(String name, long value) { - set_option_value(name, String.valueOf(value)); + set_option_value(name, value); } @TargetApi(Build.VERSION_CODES.N) public static void hook_autocompletion_adapter(final Context context, final AutoCompleteTextView view, - final String table, final String field) { + final String table, final String field, + final boolean profileSpecific) { String[] from = {field}; int[] to = {android.R.id.text1}; SimpleCursorAdapter adapter = @@ -146,15 +149,30 @@ public final class MLDB { String[] col_names = {FontsContract.Columns._ID, field}; MatrixCursor c = new MatrixCursor(col_names); + String sql; + String[] params; + if (profileSpecific) { + sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " + + "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " + + "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + + "else 9 end " + "FROM %s " + + "WHERE profile=? AND %s_upper LIKE '%%'||?||'%%' " + + "ORDER BY 2, 1;", field, field, field, field, table, field); + params = new String[]{str, str, str, Data.profile.get().getUuid(), str}; + } + else { + sql = String.format("SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " + + "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " + + "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + + "else 9 end " + "FROM %s " + + "WHERE %s_upper LIKE '%%'||?||'%%' " + "ORDER BY 2, 1;", + field, field, field, field, table, field); + params = new String[]{str, str, str, str}; + } + Log.d("autocompletion", sql); SQLiteDatabase db = MLDB.getReadableDatabase(); - try (Cursor matches = db.rawQuery(String.format( - "SELECT %s as a, case when %s_upper LIKE ?||'%%' then 1 " + - "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " + - "WHEN %s_upper LIKE '%% '||?||'%%' then 3 " + "else 9 end " + "FROM %s " + - "WHERE %s_upper LIKE '%%'||?||'%%' " + "ORDER BY 2, 1;", field, field, - field, field, table, field), new String[]{str, str, str, str})) - { + try (Cursor matches = db.rawQuery(sql, params)) { int i = 0; while (matches.moveToNext()) { String match = matches.getString(0); @@ -177,8 +195,7 @@ public final class MLDB { MLDB.context = context; } public static void done() { - if (helperForReading != null) - helperForReading.close(); + if (helperForReading != null) helperForReading.close(); if ((helperForWriting != helperForReading) && (helperForWriting != null)) helperForWriting.close(); @@ -188,7 +205,7 @@ public final class MLDB { class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable { public static final String DB_NAME = "mobile-ledger.db"; - public static final int LATEST_REVISION = 11; + public static final int LATEST_REVISION = 15; private final Application mContext; @@ -225,8 +242,25 @@ class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable { BufferedReader reader = new BufferedReader(isr); String line; + int line_no = 1; while ((line = reader.readLine()) != null) { - db.execSQL(line); + if (line.startsWith("--")) { + line_no++; + continue; + } + if (line.isEmpty()) { + line_no++; + continue; + } + try { + db.execSQL(line); + } + catch (Exception e) { + throw new RuntimeException( + String.format("Error applying revision %d, line %d", rev_no, line_no), + e); + } + line_no++; } db.setTransactionSuccessful(); diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java index 86417510..efb1ac97 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java @@ -32,7 +32,7 @@ public final class NetworkUtil { public static HttpURLConnection prepare_connection(String path) throws IOException { MobileLedgerProfile profile = Data.profile.get(); final String backend_url = profile.getUrl(); - final boolean use_auth = profile.isUseAuthentication(); + final boolean use_auth = profile.isAuthEnabled(); Log.d("network", "Connecting to " + backend_url + "/" + path); HttpURLConnection http = (HttpURLConnection) new URL(backend_url + "/" + path).openConnection(); diff --git a/app/src/main/res/drawable/ic_mode_edit_black_24dp.xml b/app/src/main/res/drawable/ic_mode_edit_black_24dp.xml new file mode 100644 index 00000000..eee95600 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_edit_black_24dp.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_view_list_black_24dp.xml b/app/src/main/res/drawable/ic_view_list_black_24dp.xml new file mode 100644 index 00000000..83000271 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_list_black_24dp.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout-w900dp/profile_list.xml b/app/src/main/res/layout-w900dp/profile_list.xml new file mode 100644 index 00000000..5d21e302 --- /dev/null +++ b/app/src/main/res/layout-w900dp/profile_list.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index dacbcea8..833b5fb2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -240,6 +240,15 @@ android:showDividers="beginning" app:layout_constraintBottom_toBottomOf="parent"> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile_list.xml b/app/src/main/res/layout/activity_profile_list.xml new file mode 100644 index 00000000..65a90c6b --- /dev/null +++ b/app/src/main/res/layout/activity_profile_list.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/profile_detail.xml b/app/src/main/res/layout/profile_detail.xml new file mode 100644 index 00000000..39eeb50d --- /dev/null +++ b/app/src/main/res/layout/profile_detail.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/profile_list.xml b/app/src/main/res/layout/profile_list.xml new file mode 100644 index 00000000..cb915aaa --- /dev/null +++ b/app/src/main/res/layout/profile_list.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/profile_list_content.xml b/app/src/main/res/layout/profile_list_content.xml new file mode 100644 index 00000000..68fa9a96 --- /dev/null +++ b/app/src/main/res/layout/profile_list_content.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/sql_13.sql b/app/src/main/res/raw/sql_13.sql new file mode 100644 index 00000000..e8038e9e --- /dev/null +++ b/app/src/main/res/raw/sql_13.sql @@ -0,0 +1,22 @@ +delete from options where name='transaction_list_last_update'; +delete from options where name='last_refresh'; +alter table options add profile varchar; +drop index idx_options_name; +create unique index un_options on options(profile,name); +-- +drop table account_values; +create table account_values(profile varchar not null, account varchar not null, currency varchar not null default '', keep boolean, value decimal not null ); +create unique index un_account_values on account_values(profile,account,currency); +-- +drop table accounts; +create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, hidden boolean not null default 0, keep boolean not null default 0, level integer not null, parent_name varchar); +create unique index un_accounts on accounts(profile, name); +-- +drop table transaction_accounts; +drop table transactions; +-- +create table transactions(id integer not null, data_hash varchar not null, date varchar not null, description varchar not null, keep boolean not null default 0); +create unique index un_transactions_id on transactions(id); +create unique index un_transactions_data_hash on transactions(data_hash); +-- +create table transaction_accounts(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(transaction_id) references transactions(id)); \ No newline at end of file diff --git a/app/src/main/res/raw/sql_14.sql b/app/src/main/res/raw/sql_14.sql new file mode 100644 index 00000000..ddc634e6 --- /dev/null +++ b/app/src/main/res/raw/sql_14.sql @@ -0,0 +1,8 @@ +drop table transaction_accounts; +drop table transactions; +-- +create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, date varchar not null, description varchar not null, keep boolean not null default 0); +create unique index un_transactions_id on transactions(profile,id); +create unique index un_transactions_data_hash on transactions(profile,data_hash); +-- +create table transaction_accounts(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id)); \ No newline at end of file diff --git a/app/src/main/res/raw/sql_15.sql b/app/src/main/res/raw/sql_15.sql new file mode 100644 index 00000000..56eb75e2 --- /dev/null +++ b/app/src/main/res/raw/sql_15.sql @@ -0,0 +1,10 @@ +delete from options where profile is null and name='last_scrape'; +create table new_options(profile varchar not null, name varchar not null, value varchar); + +insert into new_options(profile, name, value) select distinct '-', o.name, (select o2.value from options o2 where o2.name=o.name and o2.profile is null) from options o where o.profile is null; +insert into new_options(profile, name, value) select distinct o.profile, o.name, (select o2.value from options o2 where o2.name=o.name and o2.profile=o.profile) from options o where o.profile is not null; +drop table options; +create table options(profile varchar not null, name varchar not null, value varchar); +create unique index un_options on options(profile,name); +insert into options(profile,name,value) select profile,name,value from new_options; +drop table new_options; \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 7d3e1df9..c81a8f08 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - -- 2.39.2