From 1131c69ea15435760a7c126ae6cac961cd543665 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 13:02:43 +0000 Subject: [PATCH 01/16] whitespace --- .../java/net/ktnx/mobileledger/ui/activity/MainActivity.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 90e02c9a..c2ff5c27 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 @@ -55,10 +55,10 @@ import java.util.Observable; import java.util.Observer; public class MainActivity extends AppCompatActivity { + public MobileLedgerListFragment currentFragment = null; DrawerLayout drawer; private AccountSummaryFragment accountSummaryFragment; private TransactionListFragment transactionListFragment; - public MobileLedgerListFragment currentFragment = null; private FragmentManager fragmentManager; private TextView tvLastUpdate; private RetrieveTransactionsTask retrieveTransactionsTask; @@ -93,8 +93,7 @@ public class MainActivity extends AppCompatActivity { tvLastUpdate = findViewById(R.id.transactions_last_update); - bTransactionListCancelDownload = - findViewById(R.id.transaction_list_cancel_download); + bTransactionListCancelDownload = findViewById(R.id.transaction_list_cancel_download); progressBar = findViewById(R.id.transaction_list_progress_bar); if (progressBar == null) throw new RuntimeException("Can't get hold on the transaction value progress bar"); -- 2.39.2 From 198f2762d2236a57cec413322c0bde6985ead88a Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 13:06:16 +0000 Subject: [PATCH 02/16] some lambdas --- .../AccountSummaryFragment.java | 18 ++++----------- .../ui/activity/MainActivity.java | 23 ++++++++----------- .../TransactionListFragment.java | 18 ++++----------- 3 files changed, 20 insertions(+), 39 deletions(-) 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 19c88f2f..82709138 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 @@ -79,13 +79,10 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { public void update(Observable o, Object arg) { if (mActivity == null) return; if (swiper == null) return; - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - int cnt = Data.backgroundTaskCount.get(); - Log.d("acc", String.format("background task count changed to %d", cnt)); - swiper.setRefreshing(cnt > 0); - } + mActivity.runOnUiThread(() -> { + int cnt = Data.backgroundTaskCount.get(); + Log.d("acc", String.format("background task count changed to %d", cnt)); + swiper.setRefreshing(cnt > 0); }); } }); @@ -174,12 +171,7 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { Data.accounts.addObserver(new Observer() { @Override public void update(Observable o, Object arg) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - modelAdapter.notifyDataSetChanged(); - } - }); + mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()); } }); 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 c2ff5c27..6e17983b 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 @@ -109,21 +109,18 @@ public class MainActivity extends AppCompatActivity { @Override public void update(Observable o, Object arg) { Log.d("main", "lastUpdateDate changed"); - runOnUiThread(new Runnable() { - @Override - public void run() { - Date date = Data.lastUpdateDate.get(); - if (date == null) { - tvLastUpdate.setText(R.string.transaction_last_update_never); + runOnUiThread(() -> { + Date date = Data.lastUpdateDate.get(); + if (date == null) { + tvLastUpdate.setText(R.string.transaction_last_update_never); + } + else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + tvLastUpdate.setText(date.toInstant().atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - tvLastUpdate.setText(date.toInstant().atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - } - else { - tvLastUpdate.setText(date.toLocaleString()); - } + tvLastUpdate.setText(date.toLocaleString()); } } }); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index e3496a08..a15650ae 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -94,13 +94,10 @@ public class TransactionListFragment extends MobileLedgerListFragment { Data.backgroundTaskCount.addObserver(backgroundTaskCountObserver = new Observer() { @Override public void update(Observable o, Object arg) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - int cnt = Data.backgroundTaskCount.get(); - Log.d("trl", String.format("background task count changed to %d", cnt)); - swiper.setRefreshing(cnt > 0); - } + mActivity.runOnUiThread(() -> { + int cnt = Data.backgroundTaskCount.get(); + Log.d("trl", String.format("background task count changed to %d", cnt)); + swiper.setRefreshing(cnt > 0); }); } }); @@ -197,12 +194,7 @@ public class TransactionListFragment extends MobileLedgerListFragment { Data.transactions.addObserver(new Observer() { @Override public void update(Observable o, Object arg) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - modelAdapter.notifyDataSetChanged(); - } - }); + mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()); } }); -- 2.39.2 From faa6adf8171d31e281351464eea44e5606ef3d6c Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 13:17:01 +0000 Subject: [PATCH 03/16] fab is guaranteed to have a value --- .../ui/transaction_list/TransactionListFragment.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index a15650ae..3c4bfa9e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -141,10 +141,8 @@ public class TransactionListFragment extends MobileLedgerListFragment { root.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (fab != null) { - if (dy < 0) fab.show(); - if (dy > 0) fab.hide(); - } + if (dy < 0) fab.show(); + if (dy > 0) fab.hide(); } }); -- 2.39.2 From d3a64f9a064a144b0ea8e4568517595615b2d943 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 13:34:51 +0000 Subject: [PATCH 04/16] static class method call --- .../ui/transaction_list/TransactionListFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index 3c4bfa9e..79c34b89 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -167,7 +167,7 @@ public class TransactionListFragment extends MobileLedgerListFragment { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Log.d("tmp", "direct onItemClick"); - ((TransactionListViewModel) model).scheduleTransactionListReload(mActivity); + TransactionListViewModel.scheduleTransactionListReload(mActivity); MatrixCursor mc = (MatrixCursor) parent.getItemAtPosition(position); modelAdapter.setBoldAccountName(mc.getString(1)); modelAdapter.notifyDataSetChanged(); -- 2.39.2 From bb21e22f7b251a22cc830358e9f2c1d2911965c4 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 15:48:10 +0000 Subject: [PATCH 05/16] parse and show ledger name --- .../async/RetrieveTransactionsTask.java | 10 ++++++++++ .../main/java/net/ktnx/mobileledger/model/Data.java | 1 + .../ktnx/mobileledger/ui/activity/MainActivity.java | 13 +++++++++++++ 3 files changed, 24 insertions(+) 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 5a65f57a..ad064121 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -61,6 +61,7 @@ public class RetrieveTransactionsTask extends protected WeakReference contextRef; protected int error; // %3A is '=' + private Pattern ledger_title_re = Pattern.compile("

([^<]+)

"); Pattern account_name_re = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\""); Pattern account_value_re = Pattern.compile( "\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?"); @@ -125,6 +126,8 @@ public class RetrieveTransactionsTask extends String.format("HTTP error %d", http.getResponseCode())); db.beginTransaction(); try { + String ledgerTitle = null; + db.execSQL("UPDATE transactions set keep=0"); db.execSQL("update account_values set keep=0;"); db.execSQL("update accounts set keep=0;"); @@ -165,6 +168,13 @@ public class RetrieveTransactionsTask extends state = ParserState.EXPECTING_ACCOUNT_AMOUNT; L("→ expecting account amount"); } + else if (ledgerTitle == null) { + m = ledger_title_re.matcher(line); + if (m.find()) { + ledgerTitle = m.group(1); + Data.ledgerTitle.set(ledgerTitle); + } + } break; case EXPECTING_ACCOUNT_AMOUNT: diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Data.java b/app/src/main/java/net/ktnx/mobileledger/model/Data.java index ba0c5a64..beb8dd54 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/Data.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/Data.java @@ -28,4 +28,5 @@ public final class Data { public static ObservableValue> descriptions = new ObservableValue<>(); public static ObservableAtomicInteger backgroundTaskCount = new ObservableAtomicInteger(0); public static ObservableValue lastUpdateDate = new ObservableValue<>(); + public static ObservableValue ledgerTitle = new ObservableValue<>(); } 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 6e17983b..0ed83719 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 @@ -139,6 +139,19 @@ public class MainActivity extends AppCompatActivity { scheduleTransactionListRetrieval(); } + + Data.ledgerTitle.addObserver(new Observer() { + @Override + public void update(Observable o, Object arg) { + runOnUiThread(() -> { + String title = Data.ledgerTitle.get(); + if (title == null) + toolbar.setSubtitle(""); + else + toolbar.setSubtitle(title); + }); + } + }); } public void fab_new_transaction_clicked(View view) { Intent intent = new Intent(this, NewTransactionActivity.class); -- 2.39.2 From 2e1ff4566d4a229f920bd713eef234f9495a6d99 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 19:06:59 +0000 Subject: [PATCH 06/16] clickable suggests focusable --- app/src/main/res/layout/activity_main.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index abcf9fb8..2b26944d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -116,7 +116,7 @@ android:layout_height="wrap_content" android:background="@drawable/ic_clear_black_24dp" android:clickable="true" - android:focusable="auto" + android:focusable="true" android:onClick="onStopTransactionRefreshClick" /> -- 2.39.2 From 0628b3f6f00212726a03842dff2b169adb64c64c Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 5 Jan 2019 19:24:50 +0000 Subject: [PATCH 07/16] nice side-swipe switch between account summary and transaction list --- app/src/main/AndroidManifest.xml | 9 +- .../AccountSummaryFragment.java | 10 +- .../ui/activity/MainActivity.java | 139 ++++++++++++------ .../TransactionListFragment.java | 6 +- app/src/main/res/layout/activity_main.xml | 8 +- 5 files changed, 109 insertions(+), 63 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f324207..7171718a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - +--> + 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 82709138..830bf636 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 @@ -95,24 +95,24 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + Log.d("flow", "AccountSummaryFragment.onCreateView()"); return inflater.inflate(R.layout.account_summary_fragment, container, false); } @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + Log.d("flow", "AccountSummaryFragment.onActivityCreated()"); super.onActivityCreated(savedInstanceState); - mActivity.markDrawerItemCurrent(R.id.nav_account_summary); - model = ViewModelProviders.of(this).get(AccountSummaryViewModel.class); modelAdapter = new AccountSummaryAdapter(); - RecyclerView root = mActivity.findViewById(R.id.account_root); - root.setAdapter(modelAdapter); - + root = mActivity.findViewById(R.id.account_root); LinearLayoutManager llm = new LinearLayoutManager(mActivity); llm.setOrientation(LinearLayoutManager.VERTICAL); root.setLayoutManager(llm); + root.setAdapter(modelAdapter); fab = mActivity.findViewById(R.id.btn_add_transaction); 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 0ed83719..e245a250 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 @@ -23,9 +23,11 @@ import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.ColorInt; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; @@ -65,7 +67,27 @@ public class MainActivity extends AppCompatActivity { private View bTransactionListCancelDownload; private ProgressBar progressBar; private LinearLayout progressLayout; + private SectionsPagerAdapter mSectionsPagerAdapter; + private ViewPager mViewPager; + @Override + protected void onStart() { + super.onStart(); + + Data.lastUpdateDate.set(null); + updateLastUpdateTextFromDB(); + Date lastUpdate = Data.lastUpdateDate.get(); + + long now = new Date().getTime(); + if ((lastUpdate == null) || (now > (lastUpdate.getTime() + (24 * 3600 * 1000)))) { + if (lastUpdate == null) Log.d("db::", "WEB data never fetched. scheduling a fetch"); + else Log.d("db", + String.format("WEB data last fetched at %1.3f and now is %1.3f. re-fetching", + lastUpdate.getTime() / 1000f, now / 1000f)); + + scheduleTransactionListRetrieval(); + } + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -102,8 +124,27 @@ public class MainActivity extends AppCompatActivity { "Can't get hold on the transaction value progress bar layout"); fragmentManager = getSupportFragmentManager(); + mSectionsPagerAdapter = new SectionsPagerAdapter(fragmentManager); - onAccountSummaryClicked(null); + mViewPager = findViewById(R.id.root_frame); + mViewPager.setAdapter(mSectionsPagerAdapter); + mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener(){ + @Override + public void onPageSelected(int position) { + switch (position) { + case 0: + markDrawerItemCurrent(R.id.nav_account_summary); + break; + case 1: + markDrawerItemCurrent(R.id.nav_latest_transactions); + break; + default: + Log.e("MainActivity", String.format("Unexpected page index %d", position)); + } + + super.onPageSelected(position); + } + }); Data.lastUpdateDate.addObserver(new Observer() { @Override @@ -127,28 +168,13 @@ public class MainActivity extends AppCompatActivity { } }); - updateLastUpdateTextFromDB(); - Date lastUpdate = Data.lastUpdateDate.get(); - - long now = new Date().getTime(); - if ((lastUpdate == null) || (now > (lastUpdate.getTime() + (24 * 3600 * 1000)))) { - if (lastUpdate == null) Log.d("db", "WEB data never fetched. scheduling a fetch"); - else Log.d("db", - String.format("WEB data last fetched at %1.3f and now is %1.3f. re-fetching", - lastUpdate.getTime() / 1000f, now / 1000f)); - - scheduleTransactionListRetrieval(); - } - Data.ledgerTitle.addObserver(new Observer() { @Override public void update(Observable o, Object arg) { runOnUiThread(() -> { String title = Data.ledgerTitle.get(); - if (title == null) - toolbar.setSubtitle(""); - else - toolbar.setSubtitle(title); + if (title == null) toolbar.setSubtitle(""); + else toolbar.setSubtitle(title); }); } }); @@ -225,45 +251,43 @@ public class MainActivity extends AppCompatActivity { public void onAccountSummaryClicked(View view) { drawer.closeDrawers(); - resetFragmentBackStack(); - showAccountSummaryFragment(); } private void showAccountSummaryFragment() { - FragmentTransaction ft = fragmentManager.beginTransaction(); - accountSummaryFragment = new AccountSummaryFragment(); - ft.replace(R.id.root_frame, accountSummaryFragment); - ft.commit(); - currentFragment = accountSummaryFragment; + mViewPager.setCurrentItem(0, true); +// FragmentTransaction ft = fragmentManager.beginTransaction(); +// accountSummaryFragment = new AccountSummaryFragment(); +// ft.replace(R.id.root_frame, accountSummaryFragment); +// ft.commit(); +// currentFragment = accountSummaryFragment; } public void onLatestTransactionsClicked(View view) { drawer.closeDrawers(); - resetFragmentBackStack(); - showTransactionsFragment(null); } private void resetFragmentBackStack() { // fragmentManager.popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE); } private void showTransactionsFragment(LedgerAccount account) { - FragmentTransaction ft = fragmentManager.beginTransaction(); - if (transactionListFragment == null) { - Log.d("flow", "MainActivity creating TransactionListFragment"); - transactionListFragment = new TransactionListFragment(); - } - Bundle bundle = new Bundle(); - if (account != null) { - bundle.putString(TransactionListFragment.BUNDLE_KEY_FILTER_ACCOUNT_NAME, - account.getName()); - } - transactionListFragment.setArguments(bundle); - ft.replace(R.id.root_frame, transactionListFragment); - if (account != null) - ft.addToBackStack(getResources().getString(R.string.title_activity_transaction_list)); - ft.commit(); - - currentFragment = transactionListFragment; + mViewPager.setCurrentItem(1, true); +// FragmentTransaction ft = fragmentManager.beginTransaction(); +// if (transactionListFragment == null) { +// Log.d("flow", "MainActivity creating TransactionListFragment"); +// transactionListFragment = new TransactionListFragment(); +// } +// Bundle bundle = new Bundle(); +// if (account != null) { +// bundle.putString(TransactionListFragment.BUNDLE_KEY_FILTER_ACCOUNT_NAME, +// account.getName()); +// } +// transactionListFragment.setArguments(bundle); +// ft.replace(R.id.root_frame, transactionListFragment); +// if (account != null) +// ft.addToBackStack(getResources().getString(R.string.title_activity_transaction_list)); +// ft.commit(); +// +// currentFragment = transactionListFragment; } public void showAccountTransactions(LedgerAccount account) { showTransactionsFragment(account); @@ -336,5 +360,30 @@ public class MainActivity extends AppCompatActivity { progressBar.setIndeterminate(false); } } + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + public SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + Log.d("main", String.format("Switching to gragment %d", position)); + switch (position) { + case 0: + return new AccountSummaryFragment(); + case 1: + return new TransactionListFragment(); + default: + throw new IllegalStateException( + String.format("Unexpected fragment index: " + "%d", position)); + } + } + + @Override + public int getCount() { + return 2; + } + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index 79c34b89..b9a3b838 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -120,8 +120,6 @@ public class TransactionListFragment extends MobileLedgerListFragment { Log.d("flow", "TransactionListFragment.onActivityCreated called"); super.onActivityCreated(savedInstanceState); - mActivity.markDrawerItemCurrent(R.id.nav_latest_transactions); - swiper = mActivity.findViewById(R.id.transaction_swipe); if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout"); root = mActivity.findViewById(R.id.transaction_root); @@ -131,12 +129,10 @@ public class TransactionListFragment extends MobileLedgerListFragment { modelAdapter = new TransactionListAdapter(); modelAdapter.setBoldAccountName(mShowOnlyAccountName); + root.setAdapter(modelAdapter); FloatingActionButton fab = mActivity.findViewById(R.id.btn_add_transaction); - RecyclerView root = mActivity.findViewById(R.id.transaction_root); - root.setAdapter(modelAdapter); - fab.show(); root.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2b26944d..dacbcea8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -122,14 +122,16 @@ - + app:layout_constraintTop_toBottomOf="@+id/main_header" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + > - + Date: Sun, 6 Jan 2019 07:55:44 +0000 Subject: [PATCH 08/16] SQL: table for storing connection profiles --- app/src/main/res/raw/sql_11.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/raw/sql_11.sql diff --git a/app/src/main/res/raw/sql_11.sql b/app/src/main/res/raw/sql_11.sql new file mode 100644 index 00000000..356aa16c --- /dev/null +++ b/app/src/main/res/raw/sql_11.sql @@ -0,0 +1,2 @@ +create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar); +create unique index un_profile_name on profiles(name); \ No newline at end of file -- 2.39.2 From 5c73162fd331c1401bb105e56d600c0be84fc715 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 07:56:48 +0000 Subject: [PATCH 09/16] model: class for working with connection profiles --- .../model/MobileLedgerProfile.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java new file mode 100644 index 00000000..af78c59d --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java @@ -0,0 +1,121 @@ +/* + * 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.model; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import net.ktnx.mobileledger.utils.MLDB; + +import java.util.ArrayList; +import java.util.List; + +public final class MobileLedgerProfile { + private String uuid; + private String name; + private String url; + private boolean useAuthentication; + private String authUserName; + private String authPassword; + public MobileLedgerProfile(String uuid, String name, String url, boolean useAuthentication, + String authUserName, String authPassword) { + this.uuid = uuid; + this.name = name; + this.url = url; + this.useAuthentication = useAuthentication; + this.authUserName = authUserName; + this.authPassword = authPassword; + } + public static List loadAllFromDB() { + List result = new ArrayList<>(); + SQLiteDatabase db = MLDB.getReadableDatabase(); + try (Cursor cursor = db.rawQuery("SELECT uuid, name, url, use_authentication, auth_user, " + + "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), + cursor.getString(5))); + } + } + return result; + } + public static MobileLedgerProfile loadUUIDFromDB(String profileUUID) { + SQLiteDatabase db = MLDB.getReadableDatabase(); + String name; + String url; + String authUser; + String authPassword; + Boolean useAuthentication; + try (Cursor cursor = db.rawQuery("SELECT name, url, use_authentication, auth_user, " + + "auth_password FROM profiles WHERE uuid=?", + new String[]{profileUUID})) + { + if (cursor.moveToNext()) { + name = cursor.getString(0); + url = cursor.getString(1); + useAuthentication = cursor.getInt(2) == 1; + authUser = useAuthentication ? cursor.getString(3) : null; + authPassword = useAuthentication ? cursor.getString(4) : null; + } + else { + name = "Unknown profile"; + url = "Https://server/url"; + useAuthentication = false; + authUser = authPassword = null; + } + } + + return new MobileLedgerProfile(profileUUID, name, url, useAuthentication, authUser, + authPassword); + } + public String getUuid() { + return uuid; + } + public String getName() { + return name; + } + public String getUrl() { + return url; + } + public boolean isUseAuthentication() { + return useAuthentication; + } + public String getAuthUserName() { + return authUserName; + } + public String getAuthPassword() { + return 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 + }); + db.setTransactionSuccessful(); + } + finally { + db.endTransaction(); + } + } +} -- 2.39.2 From ee7f9769f1f6a6e02fed8c4d7b68c8f4ac5c4ea2 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 07:57:24 +0000 Subject: [PATCH 10/16] bump latest revision to 11 --- app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4446427f..47a5e965 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java @@ -184,7 +184,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 = 10; + public static final int LATEST_REVISION = 11; private final Context mContext; -- 2.39.2 From e3872d583f324e225580a6fd05568d36e4ba0db0 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 08:14:13 +0000 Subject: [PATCH 11/16] use profiles for connection parameters included migration from shared preferences --- .../async/RetrieveTransactionsTask.java | 35 +++--------- .../async/SaveTransactionTask.java | 4 +- .../net/ktnx/mobileledger/model/Data.java | 2 +- .../ui/activity/MainActivity.java | 54 +++++++++++++------ .../net/ktnx/mobileledger/utils/MLDB.java | 4 ++ .../ktnx/mobileledger/utils/NetworkUtil.java | 26 +++++---- 6 files changed, 68 insertions(+), 57 deletions(-) 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 ad064121..12e53cb6 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -18,7 +18,6 @@ package net.ktnx.mobileledger.async; import android.annotation.SuppressLint; -import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; import android.os.OperationCanceledException; @@ -29,6 +28,7 @@ import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; +import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.activity.MainActivity; import net.ktnx.mobileledger.utils.MLDB; import net.ktnx.mobileledger.utils.NetworkUtil; @@ -48,11 +48,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class RetrieveTransactionsTask extends - AsyncTask { +public class RetrieveTransactionsTask + extends AsyncTask { public static final int MATCHING_TRANSACTIONS_LIMIT = 50; private static final Pattern transactionStartPattern = Pattern.compile("([\\d.-]+)"); + "id=\"transaction-(\\d+)\">([\\d.-]+)"); private static final Pattern transactionDescriptionPattern = Pattern.compile(" contextRef; protected int error; - // %3A is '=' - private Pattern ledger_title_re = Pattern.compile("

([^<]+)

"); Pattern account_name_re = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\""); Pattern account_value_re = Pattern.compile( "\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?"); Pattern tr_end_re = Pattern.compile(""); Pattern descriptions_line_re = Pattern.compile("\\bdescriptionsSuggester\\s*=\\s*new\\b"); Pattern description_items_re = Pattern.compile("\"value\":\"([^\"]+)\""); + // %3A is '=' private boolean success; public RetrieveTransactionsTask(WeakReference contextRef) { this.contextRef = contextRef; @@ -105,7 +104,8 @@ public class RetrieveTransactionsTask extends } @SuppressLint("DefaultLocale") @Override - protected Void doInBackground(Params... params) { + protected Void doInBackground(Void... params) { + MobileLedgerProfile profile = Data.profile.get(); Progress progress = new Progress(); int maxTransactionId = Progress.INDETERMINATE; success = false; @@ -114,8 +114,7 @@ public class RetrieveTransactionsTask extends LedgerAccount lastAccount = null; Data.backgroundTaskCount.incrementAndGet(); try { - HttpURLConnection http = - NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal"); + HttpURLConnection http = NetworkUtil.prepare_connection("journal"); http.setAllowUserInteraction(false); publishProgress(progress); MainActivity ctx = getContext(); @@ -168,13 +167,6 @@ public class RetrieveTransactionsTask extends state = ParserState.EXPECTING_ACCOUNT_AMOUNT; L("→ expecting account amount"); } - else if (ledgerTitle == null) { - m = ledger_title_re.matcher(line); - if (m.find()) { - ledgerTitle = m.group(1); - Data.ledgerTitle.set(ledgerTitle); - } - } break; case EXPECTING_ACCOUNT_AMOUNT: @@ -378,17 +370,6 @@ public class RetrieveTransactionsTask extends EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS } - public static class Params { - private SharedPreferences backendPref; - - public Params(SharedPreferences backendPref) { - this.backendPref = backendPref; - } - SharedPreferences getBackendPref() { - return backendPref; - } - } - public class Progress { public static final int INDETERMINATE = -1; private int progress; 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 9cd669e2..5ccbaaca 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.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 @@ -57,7 +57,7 @@ public class SaveTransactionTask extends AsyncTask> descriptions = new ObservableValue<>(); public static ObservableAtomicInteger backgroundTaskCount = new ObservableAtomicInteger(0); public static ObservableValue lastUpdateDate = new ObservableValue<>(); - public static ObservableValue ledgerTitle = new ObservableValue<>(); + public static ObservableValue profile = new ObservableValue<>(); } 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 e245a250..2ec65859 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 @@ -18,10 +18,10 @@ package net.ktnx.mobileledger.ui.activity; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.annotation.ColorInt; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -44,6 +44,7 @@ import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.async.RetrieveTransactionsTask; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; +import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.MobileLedgerListFragment; import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment; import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment; @@ -55,6 +56,7 @@ import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Observable; import java.util.Observer; +import java.util.UUID; public class MainActivity extends AppCompatActivity { public MobileLedgerListFragment currentFragment = null; @@ -95,6 +97,40 @@ public class MainActivity extends AppCompatActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); + Data.profile.addObserver(new Observer() { + @Override + public void update(Observable o, Object arg) { + MobileLedgerProfile profile = Data.profile.get(); + runOnUiThread(() -> { + if (profile == null) toolbar.setSubtitle(""); + else toolbar.setSubtitle(profile.getName()); + }); + } + }); + + 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); + } + drawer = findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, @@ -167,17 +203,6 @@ public class MainActivity extends AppCompatActivity { }); } }); - - Data.ledgerTitle.addObserver(new Observer() { - @Override - public void update(Observable o, Object arg) { - runOnUiThread(() -> { - String title = Data.ledgerTitle.get(); - if (title == null) toolbar.setSubtitle(""); - else toolbar.setSubtitle(title); - }); - } - }); } public void fab_new_transaction_clicked(View view) { Intent intent = new Intent(this, NewTransactionActivity.class); @@ -321,10 +346,7 @@ public class MainActivity extends AppCompatActivity { public void scheduleTransactionListRetrieval() { retrieveTransactionsTask = new RetrieveTransactionsTask(new WeakReference<>(this)); - RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params( - PreferenceManager.getDefaultSharedPreferences(this)); - - retrieveTransactionsTask.execute(params); + retrieveTransactionsTask.execute(); bTransactionListCancelDownload.setEnabled(true); } public void onStopTransactionRefreshClick(View view) { 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 47a5e965..e0dc9f1a 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 org.jetbrains.annotations.NonNls; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -47,6 +49,8 @@ public final class MLDB { 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"; + @NonNls + public static final String OPT_PROFILE_UUID = "profile_uuid"; private static MobileLedgerDatabase helperForReading, helperForWriting; private static Application context; private static void checkState() { 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 44a7eb9d..86417510 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.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 @@ -17,26 +17,30 @@ package net.ktnx.mobileledger.utils; -import android.content.SharedPreferences; import android.util.Base64; import android.util.Log; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.model.MobileLedgerProfile; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public final class NetworkUtil { private static final int thirtySeconds = 30000; - public static HttpURLConnection prepare_connection(SharedPreferences pref, String path) throws - IOException { - final String backend_url = pref.getString("backend_url", ""); - final boolean use_auth = pref.getBoolean("backend_use_http_auth", false); - Log.d("network", "Connecting to "+backend_url + "/" + path); - HttpURLConnection http = (HttpURLConnection) new URL(backend_url + "/" + path).openConnection(); + 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(); + Log.d("network", "Connecting to " + backend_url + "/" + path); + HttpURLConnection http = + (HttpURLConnection) new URL(backend_url + "/" + path).openConnection(); if (use_auth) { - final String auth_user = pref.getString("backend_auth_user", ""); - final String auth_password = pref.getString("backend_auth_password", ""); - final byte[] bytes = (String.format("%s:%s", auth_user, auth_password)).getBytes("UTF-8"); + final String auth_user = profile.getAuthUserName(); + final String auth_password = profile.getAuthPassword(); + final byte[] bytes = + (String.format("%s:%s", auth_user, auth_password)).getBytes("UTF-8"); final String value = Base64.encodeToString(bytes, Base64.DEFAULT); http.setRequestProperty("Authorization", "Basic " + value); } -- 2.39.2 From 58da72a8ebdbfb80e643084e6730afafa2e06f34 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 08:14:23 +0000 Subject: [PATCH 12/16] whitespace --- .../java/net/ktnx/mobileledger/ui/activity/MainActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2ec65859..5615a2c7 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 @@ -164,7 +164,7 @@ public class MainActivity extends AppCompatActivity { mViewPager = findViewById(R.id.root_frame); mViewPager.setAdapter(mSectionsPagerAdapter); - mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener(){ + mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { switch (position) { -- 2.39.2 From bad1742b6e8817f08488a3638de56b785a0729c6 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 08:24:34 +0000 Subject: [PATCH 13/16] MLDB: the locally-stored context in the Application instance it was before, but now it is declared so and the code analysis doesn't complain anymore --- app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e0dc9f1a..d6fa7774 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java @@ -190,9 +190,9 @@ class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable { public static final String DB_NAME = "mobile-ledger.db"; public static final int LATEST_REVISION = 11; - private final Context mContext; + private final Application mContext; - public MobileLedgerDatabase(Context context) { + public MobileLedgerDatabase(Application context) { super(context, DB_NAME, null, LATEST_REVISION); Log.d("db", "creating helper instance"); mContext = context; -- 2.39.2 From 57600692e78322e04de0db7be62f595b1230b0ec Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 6 Jan 2019 13:44:50 +0000 Subject: [PATCH 14/16] drop unique index on profile name it messes with the unique key and doesn't prevent duplicate names because we use replace into anyway duplicate names are permitted --- app/src/main/res/raw/sql_12.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/src/main/res/raw/sql_12.sql diff --git a/app/src/main/res/raw/sql_12.sql b/app/src/main/res/raw/sql_12.sql new file mode 100644 index 00000000..f64bc5ad --- /dev/null +++ b/app/src/main/res/raw/sql_12.sql @@ -0,0 +1 @@ +drop index un_profile_name; \ No newline at end of file -- 2.39.2 From 3b365016042215dd73cb4600840aa8199b8322b9 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Mon, 7 Jan 2019 21:03:17 +0000 Subject: [PATCH 15/16] 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 From 7b2966220b258cc2f1becae3ff91ef62a36cc01b Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Tue, 8 Jan 2019 19:38:24 +0000 Subject: [PATCH 16/16] skip comments while parsing transactions --- .../async/RetrieveTransactionsTask.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 508190c7..7b898593 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -51,12 +51,13 @@ import java.util.regex.Pattern; public class RetrieveTransactionsTask extends AsyncTask { public static final int MATCHING_TRANSACTIONS_LIMIT = 50; + public static final Pattern commentPattern = Pattern.compile("^\\s*;"); private static final Pattern transactionStartPattern = Pattern.compile("([\\d.-]+)"); private static final Pattern transactionDescriptionPattern = Pattern.compile(" contextRef; protected int error; @@ -144,6 +145,12 @@ public class RetrieveTransactionsTask while ((line = buf.readLine()) != null) { throwIfCancelled(); Matcher m; + m = commentPattern.matcher(line); + if (m.find()) { + // TODO: comments are ignored for now + Log.v("transaction-parser", "Ignoring comment"); + continue; + } //L(String.format("State is %d", updating)); switch (state) { case EXPECTING_ACCOUNT: @@ -297,9 +304,9 @@ public class RetrieveTransactionsTask L(String.format("%d: %s = %s", transaction.getId(), acc_name, amount)); } - else throw new IllegalStateException( - String.format("Can't parse transaction %d details", - transactionId)); + else throw new IllegalStateException(String.format( + "Can't parse transaction %d " + "details: %s", + transactionId, line)); } break; default: -- 2.39.2