/*
- * 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
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'
~
~ You should have received a copy of the GNU General Public License
~ along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
--->
+ -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.ktnx.mobileledger">
android:name="android.support.PARENT_ACTIVITY"
android:value="net.ktnx.mobileledger.ui.activity.MainActivity" />
</activity>
+ <activity
+ android:name=".ui.activity.ProfileListActivity"
+ android:label="@string/title_profile_list"
+ android:theme="@style/AppTheme.NoActionBar"></activity>
+ <activity
+ android:name=".ui.profiles.ProfileDetailActivity"
+ android:label="@string/title_profile_details"
+ android:parentActivityName=".ui.activity.ProfileListActivity"
+ android:theme="@style/AppTheme.NoActionBar">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="net.ktnx.mobileledger.ui.activity.ProfileListActivity" />
+ </activity>
</application>
</manifest>
\ No newline at end of file
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);
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);
}
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);
}
}
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);
}
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
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",
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);
}
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);
}
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;
private LedgerTransaction ltr;
protected String error;
- private SharedPreferences pref;
- public void setPref(SharedPreferences pref) {
- this.pref = pref;
- }
-
public SaveTransactionTask(TaskCallback callback) {
task_callback = callback;
}
protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
error = null;
try {
- backend_url = pref.getString("backend_url", "");
+ backend_url = Data.profile.get().getUrl();
ltr = ledgerTransactions[0];
int tried = 0;
public class UpdateAccountsTask extends AsyncTask<Boolean, Void, ArrayList<LedgerAccount>> {
protected ArrayList<LedgerAccount> doInBackground(Boolean[] onlyStarred) {
Data.backgroundTaskCount.incrementAndGet();
+ String profileUUID = Data.profile.get().getUuid();
try {
ArrayList<LedgerAccount> 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));
public class UpdateTransactionsTask extends AsyncTask<String, Void, List<LedgerTransaction>> {
protected List<LedgerTransaction> doInBackground(String[] filterAccName) {
Data.backgroundTaskCount.incrementAndGet();
+ String profile_uuid = Data.profile.get().getUuid();
try {
ArrayList<LedgerTransaction> newList = new ArrayList<>();
String sql;
String[] params;
- sql = "SELECT id FROM transactions ORDER BY date desc, id desc";
- params = null;
+ sql = "SELECT id FROM transactions WHERE profile=? ORDER BY date desc, id desc";
+ params = new String[]{profile_uuid};
if (hasFilter) {
sql = "SELECT distinct tr.id from transactions tr JOIN transaction_accounts ta " +
- "ON ta.transaction_id=tr.id WHERE ta.account_name LIKE ?||'%' AND ta" +
+ "ON ta.transaction_id=tr.id AND ta.profile=tr.profile WHERE tr.profile=? " +
+ "and ta" + ".account_name LIKE ?||'%' AND ta" +
".amount <> 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;
/*
- * 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
return Float.compare(o1.getAmount(), o2.getAmount());
}
};
+ private String profile;
private Integer id;
private String date;
private String description;
private ArrayList<LedgerTransactionAccount> 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;
this.dataHash = null;
dataLoaded = false;
}
- private String dataHash;
- private boolean dataLoaded;
- public ArrayList<LedgerTransactionAccount> getAccounts() {
- return accounts;
- }
public LedgerTransaction(String date, String description) {
this(null, date, description);
}
public LedgerTransaction(int id) {
this(id, null, null);
}
+ public ArrayList<LedgerTransactionAccount> getAccounts() {
+ return accounts;
+ }
public void addAccount(LedgerTransactionAccount item) {
accounts.add(item);
dataHash = null;
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());
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;
+ }
}
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<MobileLedgerProfile> loadAllFromDB() {
List<MobileLedgerProfile> result = new ArrayList<>();
SQLiteDatabase db = MLDB.getReadableDatabase();
"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<MobileLedgerProfile> createInitialProfileList() {
+ List<MobileLedgerProfile> 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;
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();
}
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));
+ }
}
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() {
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;
}
});
- 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 =
}
});
}
+ private void setupProfile() {
+ List<MobileLedgerProfile> 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);
}
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) {
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) {
/*
- * 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
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;
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;
});
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);
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);
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());
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);
}
--- /dev/null
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<MobileLedgerProfile> 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<MobileLedgerProfile> list = MobileLedgerProfile.loadAllFromDB();
+ recyclerView.setAdapter(new ProfilesRecyclerViewAdapter(this, list, mTwoPane));
+ }
+
+ public static class ProfilesRecyclerViewAdapter
+ extends RecyclerView.Adapter<ProfilesRecyclerViewAdapter.ProfileListViewHolder> {
+
+ private final ProfileListActivity mParentActivity;
+ private final List<MobileLedgerProfile> 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<MobileLedgerProfile> 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);
+ }
+ }
+ }
+}
/*
- * 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
*/
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.
--- /dev/null
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+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);
+ }
+}
--- /dev/null
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+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;
+ }
+}
/*
- * 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
// a bit longer
if (tr == null) return;
- Log.d("transactions", String.format("Filling position %d", position));
+ Log.d("transactions", String.format("Filling position %d with %d accounts", position,
+ tr.getAccounts().size()));
TransactionLoader loader = new TransactionLoader();
loader.execute(new TransactionLoaderParams(tr, holder, position, boldAccountName));
int rowIndex = 0;
for (LedgerTransactionAccount acc : tr.getAccounts()) {
+ Log.d("tmp", String.format("publishing tr %d acc %s %1.2f", tr.getId(),
+ acc.getAccountName(), acc.getAmount()));
publishProgress(new TransactionLoaderStep(p[0].holder, acc, rowIndex++,
p[0].boldAccountName));
}
accName.setText(acc.getAccountName());
accAmount.setText(acc.toString());
+ Log.d("tmp", String.format("showing acc row %d: %s %1.2f", rowIndex,
+ acc.getAccountName(), acc.getAmount()));
+
String boldAccountName = step.getBoldAccountName();
if ((boldAccountName != null) && boldAccountName.equals(acc.getAccountName())) {
accName.setTypeface(null, Typeface.BOLD);
accNameFilter = mActivity.findViewById(R.id.transaction_filter_account_name);
TransactionListFragment me = this;
- MLDB.hook_autocompletion_adapter(mActivity, accNameFilter, "accounts", "name");
+ MLDB.hook_autocompletion_adapter(mActivity, accNameFilter, "accounts", "name", true);
accNameFilter.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
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
import android.widget.FilterQueryProvider;
import android.widget.SimpleCursorAdapter;
+import net.ktnx.mobileledger.model.Data;
+
import org.jetbrains.annotations.NonNls;
import java.io.BufferedReader;
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() {
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);
}
}
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 =
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);
MLDB.context = context;
}
public static void done() {
- if (helperForReading != null)
- helperForReading.close();
+ if (helperForReading != null) helperForReading.close();
if ((helperForWriting != helperForReading) && (helperForWriting != null))
helperForWriting.close();
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;
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();
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();
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector android:height="24dp" android:tint="#313131"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector android:height="24dp" android:tint="#313131"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M4,14h4v-4L4,10v4zM4,19h4v-4L4,15v4zM4,9h4L8,5L4,5v4zM9,14h12v-4L9,10v4zM9,19h12v-4L9,15v4zM9,5v4h12L21,5L9,5z"/>
+</vector>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:baselineAligned="false"
+ android:divider="?android:attr/dividerHorizontal"
+ android:orientation="horizontal"
+ android:showDividers="middle"
+ tools:context=".ui.activity.ProfileListActivity">
+
+ <!--
+ This layout is a two-pane layout for the Profiles
+ master/detail flow.
+
+ -->
+
+ <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/profile_list"
+ android:name="net.ktnx.mobileledger.ui.activity.ProfileListFragment"
+ android:layout_width="@dimen/item_width"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ app:layoutManager="LinearLayoutManager"
+ tools:context="net.ktnx.mobileledger.ui.activity.ProfileListActivity"
+ tools:listitem="@layout/profile_list_content" />
+
+ <FrameLayout
+ android:id="@+id/profile_detail_container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3" />
+
+</LinearLayout>
\ No newline at end of file
android:showDividers="beginning"
app:layout_constraintBottom_toBottomOf="parent">
+ <TextView
+ android:id="@+id/nav_profiles"
+ style="@style/nav_button"
+ android:layout_width="match_parent"
+ android:layout_weight="1"
+ android:drawableStart="@drawable/ic_view_list_black_24dp"
+ android:onClick="nav_profiles_clicked"
+ android:text="@string/profiles" />
+
<TextView
android:id="@+id/textView2"
style="@style/nav_button"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".ui.profiles.ProfileDetailActivity"
+ tools:ignore="MergeRootFrame">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_bar_height"
+ android:fitsSystemWindows="true"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+
+ <android.support.design.widget.CollapsingToolbarLayout
+ android:id="@+id/toolbar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ app:contentScrim="?attr/colorPrimary"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed"
+ app:toolbarId="@+id/toolbar">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/detail_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:layout_collapseMode="pin"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
+
+ </android.support.design.widget.CollapsingToolbarLayout>
+
+ </android.support.design.widget.AppBarLayout>
+
+ <android.support.v4.widget.NestedScrollView
+ android:id="@+id/profile_detail_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_margin="@dimen/fab_margin"
+ app:layout_anchor="@+id/profile_detail_container"
+ app:layout_anchorGravity="top|end"
+ app:srcCompat="@drawable/ic_save_white_24dp" />
+
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".ui.activity.ProfileListActivity">
+
+ <android.support.design.widget.AppBarLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ </android.support.design.widget.AppBarLayout>
+
+ <FrameLayout
+ android:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <include layout="@layout/profile_list" />
+ </FrameLayout>
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:srcCompat="@drawable/svg_thick_plus_white" />
+
+
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/profile_detail"
+ style="?android:attr/textAppearanceLarge"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp"
+ android:textIsSelectable="true"
+ tools:context=".ui.profiles.ProfileDetailFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Profile name" />
+
+ <EditText
+ android:id="@+id/profile_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textPersonName"
+ android:text="Name" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="URL" />
+
+ <EditText
+ android:id="@+id/url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textUri"
+ android:text="https://server/url" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Switch
+ android:id="@+id/enable_http_auth"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="@string/pref_title_use_http_auth" />
+
+ <LinearLayout
+ android:id="@+id/auth_params"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="8dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/pref_title_backend_auth_user" />
+
+ <EditText
+ android:id="@+id/auth_user_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textPersonName"
+ android:text="Name" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/pref_title_backend_auth_password" />
+
+ <EditText
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textPassword" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/profile_list"
+ android:name="net.ktnx.mobileledger.ui.activity.ProfileListFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ app:layoutManager="LinearLayoutManager"
+ tools:context=".ui.activity.ProfileListActivity"
+ tools:listitem="@layout/profile_list_content" />
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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 <https://www.gnu.org/licenses/>.
+ -->
+
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/profile_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:foregroundGravity="center_vertical"
+ android:paddingVertical="@dimen/nav_header_vertical_spacing">
+
+ <TextView
+ android:id="@+id/profile_list_edit_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="9"
+ android:drawableStart="@drawable/ic_mode_edit_black_24dp"
+ android:padding="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <RadioButton
+ android:id="@+id/profile_list_radio"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="9"
+ android:text="Profile name"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/profile_list_edit_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText" />
+
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ 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
<string name="err_bad_auth">Грешно потребителско име или парола</string>
<string name="new_transaction_amount_hint">0,00</string>
<string name="transactions_last_update_label">Данни към:</string>
+ <string name="title_profile_list">Профили</string>
+ <string name="profiles">Профили</string>
+ <string name="title_profile_details">Данни за профила</string>
+ <string name="transaction_last_update_never">никога</string>
+ <string name="err_cancelled">Операцията е прекъсната</string>
+ <string name="title_activity_transaction_list">Трансакции</string>
+ <string name="err_http_error">Грешка в HTTP</string>
+ <string name="new_profile_title">Нов профил</string>
</resources>
\ No newline at end of file
<!--
- ~ 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
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="fab_margin">16dp</dimen>
+ <dimen name="app_bar_height">200dp</dimen>
+ <dimen name="item_width">200dp</dimen>
+ <dimen name="text_margin">16dp</dimen>
</resources>
\ No newline at end of file
<!--
- ~ 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
<string name="pref_show_only_starred_on_summary">Only starred accounts are shown</string>
<string name="menu_acc_summary_hide_selected_title">Hide selected accounts</string>
<string name="menu_acc_summary_cancel_selection_title">Cancel selection</string>
- <string name="menu_acc_summary_confirm_selection_title">Confirm selectin</string>
+ <string name="menu_acc_summary_confirm_selection_title">Confirm selectiоn</string>
<string name="title_activity_transaction_list">Transactions</string>
<string name="transactions_last_update_label">Last update:</string>
<string name="transaction_last_update_never">never</string>
<string name="err_cancelled">Operation cancelled</string>
+ <string name="title_profile_list">Profiles</string>
+ <string name="title_profile_details">Profile Details</string>
+ <string name="profiles">Profiles</string>
+ <string name="new_profile_title" type="id">New profile</string>
</resources>
+++ /dev/null
-<!--
- ~ Copyright © 2018 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 <https://www.gnu.org/licenses/>.
- -->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
- <!-- NOTE: EditTextPreference accepts EditText attributes. -->
- <!-- NOTE: EditTextPreference's summary should be set to its value by the activity code. -->
- <EditTextPreference
- android:id="@+id/pref_backend_url"
- android:capitalize="words"
- android:defaultValue="@string/pref_default_backend_url"
- android:inputType="textUri"
- android:key="backend_url"
- android:maxLines="1"
- android:selectAllOnFocus="true"
- android:singleLine="true"
- android:title="@string/pref_title_backend_url" />
-
- <SwitchPreference
- android:id="@+id/pref_backend_use_http_auth"
- android:defaultValue="false"
- android:disableDependentsState="false"
- android:key="backend_use_http_auth"
- android:summaryOff="@string/pref_description_use_http_auth_off"
- android:summaryOn="@string/pref_description_use_http_auth_on"
- android:title="@string/pref_title_use_http_auth" />
-
- <EditTextPreference
- android:id="@+id/pref_backend_auth_user"
- android:dependency="backend_use_http_auth"
- android:inputType="text"
- android:key="backend_auth_user"
- android:maxLines="1"
- android:selectAllOnFocus="true"
- android:singleLine="true"
- android:title="@string/pref_title_backend_auth_user" />
-
- <EditTextPreference
- android:id="@+id/pref_backend_auth_password"
- android:dependency="backend_use_http_auth"
- android:inputType="textPassword"
- android:key="backend_auth_password"
- android:maxLines="1"
- android:selectAllOnFocus="true"
- android:singleLine="true"
- android:title="@string/pref_title_backend_auth_password" />
-
-</PreferenceScreen>