]> git.ktnx.net Git - mobile-ledger.git/commitdiff
major rework of the async stuff, view model, pull-to-refresh account list
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Thu, 13 Dec 2018 20:44:08 +0000 (20:44 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Thu, 13 Dec 2018 20:44:08 +0000 (20:44 +0000)
19 files changed:
app/build.gradle
app/src/main/java/net/ktnx/mobileledger/AccountRowLayout.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/AccountSummary.java
app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java
app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java
app/src/main/res/drawable/ic_cancel_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable/ic_star_white_24dp.xml [new file with mode: 0644]
app/src/main/res/layout/account_summary_row.xml [new file with mode: 0644]
app/src/main/res/layout/content_account_summary.xml
app/src/main/res/menu/account_summary.xml
app/src/main/res/raw/sql_5.sql [new file with mode: 0644]
app/src/main/res/raw/sql_6.sql [new file with mode: 0644]
app/src/main/res/values/ids.xml [new file with mode: 0644]
app/src/main/res/values/strings.xml

index 35d467480e9f37adb6662e68284fb7c6cd2d5be4..e7f612761c1698591ce2d15dc78451a9a2aaa5d1 100644 (file)
@@ -30,6 +30,7 @@ dependencies {
     implementation 'com.android.support:support-v4:28.0.0'
     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'
     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/java/net/ktnx/mobileledger/AccountRowLayout.java b/app/src/main/java/net/ktnx/mobileledger/AccountRowLayout.java
deleted file mode 100644 (file)
index 57ca623..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package net.ktnx.mobileledger;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.LinearLayout;
-
-class AccountRowLayout extends LinearLayout {
-    private String accountName;
-
-    public
-    AccountRowLayout(Context context, String accountName) {
-        super(context);
-        this.accountName = accountName;
-    }
-
-    public
-    AccountRowLayout(Context context, AttributeSet attrs, String accountName) {
-        super(context, attrs);
-        this.accountName = accountName;
-    }
-
-    public
-    AccountRowLayout(Context context, AttributeSet attrs, int defStyleAttr, String accountName) {
-        super(context, attrs, defStyleAttr);
-        this.accountName = accountName;
-    }
-
-    public
-    AccountRowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
-                     String accountName) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        this.accountName = accountName;
-    }
-
-    public
-    String getAccountName() {
-        return accountName;
-    }
-}
index 829b00b1717bb7f7e72bf4230e59c7b3b0528561..c1f834b183f427bbb49e4ee2c947681b0525b1a4 100644 (file)
@@ -1,40 +1,30 @@
 package net.ktnx.mobileledger;
 
-import android.annotation.SuppressLint;
+import android.arch.lifecycle.ViewModelProviders;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Typeface;
 import android.os.Build;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.support.design.widget.Snackbar;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
+import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.app.ActionBarDrawerToggle;
 import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.Toolbar;
 import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextMenu;
-import android.view.Gravity;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
 import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
 
+import java.lang.ref.WeakReference;
 import java.util.Date;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static android.view.View.GONE;
+import java.util.List;
 
 public class AccountSummary extends AppCompatActivity {
     DrawerLayout drawer;
@@ -43,13 +33,14 @@ public class AccountSummary extends AppCompatActivity {
     private static boolean account_list_needs_update = true;
     MenuItem mShowHiddenAccounts;
     SharedPreferences.OnSharedPreferenceChangeListener sBindPreferenceSummaryToValueListener;
-    private AccountRowLayout clickedAccountRow;
     private MobileLedgerDatabase dbh;
+    private AccountSummaryViewModel model;
+    private AccountSummaryAdapter modelAdapter;
+    private Menu optMenu;
 
     public static void preferences_changed() {
         account_list_needs_update = true;
     }
-    MenuItem mRefresh;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -75,8 +66,43 @@ public class AccountSummary extends AppCompatActivity {
             e.printStackTrace();
         }
 
+        model = ViewModelProviders.of(this).get(AccountSummaryViewModel.class);
+        List<LedgerAccount> accounts = model.getAccounts();
+        modelAdapter = new AccountSummaryAdapter(accounts);
+
+        RecyclerView root = findViewById(R.id.account_root);
+        root.setAdapter(modelAdapter);
+
+        LinearLayoutManager llm = new LinearLayoutManager(this);
+        llm.setOrientation(LinearLayoutManager.VERTICAL);
+        root.setLayoutManager(llm);
+
+        root.addOnItemTouchListener(new RecyclerItemListener(this, root, new RecyclerItemListener.RecyclerTouchListener() {
+            @Override
+            public void onClickItem(View v, int position) {
+                Log.d("list", String.format("item %d clicked", position));
+                if (modelAdapter.isSelectionActive()) {
+                    modelAdapter.selectItem(position);
+                }
+            }
+
+            @Override
+            public void onLongClickItem(View v, int position) {
+                Log.d("list", String.format("item %d long-clicked", position));
+                modelAdapter.startSelection();
+                if (optMenu != null) {
+                    optMenu.findItem(R.id.menu_acc_summary_cancel_selection).setVisible(true);
+                    optMenu.findItem(R.id.menu_acc_summary_hide_selected).setVisible(true);
+                }
+            }
+        }));
+
+        ((SwipeRefreshLayout) findViewById(R.id.account_swiper)).setOnRefreshListener(() -> {
+            Log.d("ui", "refreshing accounts via swipe");
+            update_accounts(true);
+        });
         prepare_db();
-        update_account_table();
+//        update_account_table();
         update_accounts(false);
     }
 
@@ -111,7 +137,7 @@ public class AccountSummary extends AppCompatActivity {
     }
 
     public void nav_exit_clicked(View view) {
-        Log.w("mobileledger", "exiting");
+        Log.w("app", "exiting");
         finish();
     }
 
@@ -134,21 +160,13 @@ public class AccountSummary extends AppCompatActivity {
     public boolean onCreateOptionsMenu(Menu menu) {
         // Inflate the menu; this adds items to the action bar if it is present.
         getMenuInflater().inflate(R.menu.account_summary, menu);
-        mRefresh = menu.findItem(R.id.menu_acc_summary_refresh);
-        if (mRefresh == null) throw new AssertionError();
+        optMenu = menu;
 
         mShowHiddenAccounts = menu.findItem(R.id.menu_acc_summary_show_hidden);
         if (mShowHiddenAccounts == null) throw new AssertionError();
 
-        sBindPreferenceSummaryToValueListener =
-                new SharedPreferences.OnSharedPreferenceChangeListener() {
-                    @Override
-                    public
-                    void onSharedPreferenceChanged(SharedPreferences preference, String value) {
-                        mShowHiddenAccounts
-                                .setChecked(preference.getBoolean("show_hidden_accounts", false));
-                    }
-                };
+        sBindPreferenceSummaryToValueListener = (preference, value) -> mShowHiddenAccounts
+                .setChecked(preference.getBoolean("show_hidden_accounts", false));
         SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
         pref.registerOnSharedPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
 
@@ -202,178 +220,34 @@ public class AccountSummary extends AppCompatActivity {
     }
 
     private void update_accounts() {
-        if (mRefresh != null) mRefresh.setVisible(false);
-        Resources rm = getResources();
-
-        ProgressBar pb = findViewById(R.id.progressBar);
-        pb.setVisibility(View.VISIBLE);
-        TextView pt = findViewById(R.id.textProgress);
-        pt.setVisibility(View.VISIBLE);
-        pb.setIndeterminate(true);
-
-        RetrieveAccountsTask task = new RetrieveAccountsTask() {
-            @Override
-            protected void onProgressUpdate(Integer... values) {
-                if ( values[0] == 0 )
-                    pt.setText(R.string.progress_connecting);
-                else
-                    pt.setText(String.format(getResources().getString(R.string.progress_N_accounts_loaded), values[0]));
-            }
-
-            @Override
-            protected void onPostExecute(Void result) {
-                pb.setVisibility(GONE);
-                pt.setVisibility(GONE);
-                if (mRefresh != null) mRefresh.setVisible(true);
-                if (this.error != 0) {
-                    String err_text = rm.getString(this.error);
-                    Log.d("visual", String.format("showing snackbar: %s", err_text));
-                    Snackbar.make(drawer, err_text, Snackbar.LENGTH_LONG ).show();
-                }
-                else {
-                    dbh.set_option_value("last_refresh", new Date().getTime() );
-                    update_account_table();
-                }
-            }
-        };
+        RetrieveAccountsTask task = new RetrieveAccountsTask(new WeakReference<>(this));
 
         task.setPref(PreferenceManager.getDefaultSharedPreferences(this));
         task.execute();
 
     }
-
-    public int dp2px(float dp) {
-        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()));
-    }
-
-    Pattern higher_account = Pattern.compile("^[^:]+:");
-
-    private String strip_higher_accounts(String acc_name, int[] count) {
-        count[0] = 0;
-        while (true) {
-            Matcher m = higher_account.matcher(acc_name);
-            if (m.find()) {
-                count[0]++;
-                acc_name = m.replaceFirst("");
-            }
-            else break;
+    void onAccountRefreshDone(int error) {
+        SwipeRefreshLayout srl = findViewById(R.id.account_swiper);
+        srl.setRefreshing(false);
+        if (error != 0) {
+            String err_text = getResources().getString(error);
+            Log.d("visual", String.format("showing snackbar: %s", err_text));
+            Snackbar.make(drawer, err_text, Snackbar.LENGTH_LONG ).show();
         }
-
-        return acc_name;
-    }
-
-    public void hideAccountClicked(MenuItem item) {
-        try(SQLiteDatabase db = dbh.getWritableDatabase()) {
-            db.execSQL("update accounts set hidden=1 where name=?", new Object[]{clickedAccountRow.getAccountName()});
+        else {
+            dbh.set_option_value("last_refresh", new Date().getTime() );
+            update_account_table();
         }
-        update_account_table();
     }
-
-    @SuppressLint("DefaultLocale")
     private void update_account_table() {
-        LinearLayout root = findViewById(R.id.account_root);
-        root.removeAllViewsInLayout();
-
-        View.OnCreateContextMenuListener ccml = new View.OnCreateContextMenuListener() {
-            @Override
-            public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
-                clickedAccountRow = (AccountRowLayout) v;
-                getMenuInflater().inflate(R.menu.account_summary_account_menu, menu);
-            }
-        };
-
-        int rowHeight =
-                (int) (getTheme().obtainStyledAttributes(new int[]{android.R.attr.actionBarSize})
-                        .getDimensionPixelSize(0, dp2px(56)) * 0.75);
-
-        boolean showingHiddenAccounts = PreferenceManager.getDefaultSharedPreferences(this)
-                .getBoolean("show_hidden_accounts", false);
-        Log.d("pref", "show_hidden_accounts is " + (showingHiddenAccounts ? "true" : "false"));
-
-        try(SQLiteDatabase db = dbh.getReadableDatabase()) {
-            try (Cursor cursor = db
-                    .rawQuery("SELECT name, hidden FROM accounts ORDER BY name;", null))
-            {
-                boolean even = false;
-                String skippingAccountName = null;
-                while (cursor.moveToNext()) {
-                    String acc_name = cursor.getString(0);
-                    if (skippingAccountName != null) {
-                        if (acc_name.startsWith(skippingAccountName + ":")) continue;
-
-                        skippingAccountName = null;
-                    }
-
-                    boolean is_hidden = cursor.getInt(1) == 1;
-
-                    if (!showingHiddenAccounts && is_hidden) {
-                        skippingAccountName = acc_name;
-                        continue;
-                    }
-
-                    LinearLayout r = new AccountRowLayout(this, acc_name);
-                    r.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                            ViewGroup.LayoutParams.WRAP_CONTENT));
-                    r.setGravity(Gravity.CENTER_VERTICAL);
-                    r.setPadding(getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin), dp2px(3),
-                            getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin),
-                            dp2px(4));
-                    r.setMinimumHeight(rowHeight);
-
-                    if (even) {
-                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                            r.setBackgroundColor(
-                                    getResources().getColor(R.color.table_row_even_bg, getTheme()));
-                        }
-                        else {
-                            r.setBackgroundColor(getResources().getColor(R.color.table_row_even_bg));
-                        }
-                    }
-                    even = !even;
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                        r.setContextClickable(true);
-                    }
-                    r.setOnCreateContextMenuListener(ccml);
-
-
-                    TextView acc_tv = new TextView(this, null, R.style.account_summary_account_name);
-                    acc_tv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
-                            ViewGroup.LayoutParams.MATCH_PARENT, 5f));
-                    acc_tv.setGravity(Gravity.CENTER_VERTICAL);
-                    int[] indent_level = new int[]{0};
-                    String short_acc_name = strip_higher_accounts(acc_name, indent_level);
-                    acc_tv.setPadding(indent_level[0] * getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin) / 2, 0, 0,
-                            0);
-                    acc_tv.setText(short_acc_name);
-                    if (is_hidden) acc_tv.setTypeface(null, Typeface.ITALIC);
-                    r.addView(acc_tv);
-
-                    TextView amt_tv = new TextView(this, null, R.style.account_summary_amounts);
-                    amt_tv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
-                            ViewGroup.LayoutParams.MATCH_PARENT, 1f));
-                    amt_tv.setTextAlignment(EditText.TEXT_ALIGNMENT_VIEW_END);
-                    amt_tv.setGravity(Gravity.CENTER_VERTICAL);
-//                amt_tv.setGravity(Gravity.CENTER);
-                    amt_tv.setMinWidth(dp2px(60f));
-                    StringBuilder amt_text = new StringBuilder();
-                    try (Cursor cAmounts = db.rawQuery(
-                            "SELECT currency, value FROM account_values WHERE account = ?", new String[]{acc_name}))
-                    {
-                        while (cAmounts.moveToNext()) {
-                            String curr = cAmounts.getString(0);
-                            Float amt = cAmounts.getFloat(1);
-                            if (amt_text.length() != 0) amt_text.append('\n');
-                            amt_text.append(String.format("%s %,1.2f", curr, amt));
-                        }
-                    }
-                    amt_tv.setText(amt_text.toString());
-                    if (is_hidden) amt_tv.setTypeface(null, Typeface.ITALIC);
-
-                    r.addView(amt_tv);
-
-                    root.addView(r);
-                }
-            }
+        model.reloadAccounts();
+        modelAdapter.notifyDataSetChanged();
+    }
+    public void onCancelAccSelection(MenuItem item) {
+        modelAdapter.stopSelection();
+        if (optMenu != null) {
+            optMenu.findItem(R.id.menu_acc_summary_cancel_selection).setVisible(false);
+            optMenu.findItem(R.id.menu_acc_summary_hide_selected).setVisible(false);
         }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java b/app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java
new file mode 100644 (file)
index 0000000..da11631
--- /dev/null
@@ -0,0 +1,249 @@
+package net.ktnx.mobileledger;
+
+import android.app.Application;
+import android.arch.lifecycle.AndroidViewModel;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class AccountSummaryViewModel extends AndroidViewModel {
+    private MobileLedgerDatabase dbh;
+    private List<LedgerAccount> accounts;
+
+    public AccountSummaryViewModel(@NonNull Application application) {
+        super(application);
+        dbh = new MobileLedgerDatabase(application);
+    }
+
+    List<LedgerAccount> getAccounts() {
+        if (accounts == null) {
+            accounts = new ArrayList<>();
+            reloadAccounts();
+        }
+
+        return accounts;
+    }
+
+    void reloadAccounts() {
+        accounts.clear();
+        boolean showingHiddenAccounts =
+                PreferenceManager.getDefaultSharedPreferences(getApplication())
+                        .getBoolean("show_hidden_accounts", false);
+        String sql = "SELECT name, hidden FROM accounts";
+        if (!showingHiddenAccounts) sql += " WHERE hidden = 0";
+        sql += " ORDER BY name";
+
+        try (SQLiteDatabase db = dbh.getReadableDatabase()) {
+            try (Cursor cursor = db
+                    .rawQuery(sql,null))
+            {
+                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()}))
+                    {
+                        while (c2.moveToNext()) {
+                            acc.addAmount(c2.getFloat(0), c2.getString(1));
+                        }
+                    }
+                    accounts.add(acc);
+                }
+            }
+        }
+    }
+}
+
+class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
+    private List<LedgerAccount> accounts;
+    private boolean selectionActive;
+
+    AccountSummaryAdapter(List<LedgerAccount> accounts) {
+        this.accounts = accounts;
+        this.selectionActive = false;
+    }
+
+    public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
+        LedgerAccount acc = accounts.get(position);
+        Context ctx = holder.row.getContext();
+        Resources rm = ctx.getResources();
+
+        holder.tvAccountName.setText(acc.getShortName());
+        holder.tvAccountName.setPadding(
+                acc.getLevel() * rm.getDimensionPixelSize(R.dimen.activity_horizontal_margin)/2,
+                0, 0,
+                0);
+        holder.tvAccountAmounts.setText(acc.getAmountsString());
+
+        if (acc.isHidden()) {
+            holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
+            holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
+        }
+        else {
+            holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
+            holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
+        }
+
+        if (position % 2 == 0) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+                    .setBackgroundColor(rm.getColor(R.color.table_row_even_bg, ctx.getTheme()));
+            else holder.row.setBackgroundColor(rm.getColor(R.color.table_row_even_bg));
+        }
+        else {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+                    .setBackgroundColor(rm.getColor(R.color.drawer_background, ctx.getTheme()));
+            else holder.row.setBackgroundColor(rm.getColor(R.color.drawer_background));
+        }
+
+        holder.selectionCb.setVisibility( selectionActive ? View.VISIBLE : View.GONE);
+        holder.selectionCb.setChecked(acc.isSelected());
+
+        holder.row.setTag(R.id.POS, position);
+    }
+
+    @NonNull
+    @Override
+    public LedgerRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View row = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.account_summary_row, parent, false);
+        return new LedgerRowHolder(row);
+    }
+
+    @Override
+    public int getItemCount() {
+        return accounts.size();
+    }
+
+    public void startSelection() {
+        for( LedgerAccount acc : accounts ) acc.setSelected(false);
+        this.selectionActive = true;
+        notifyDataSetChanged();
+    }
+
+    public void stopSelection() {
+        this.selectionActive = false;
+        notifyDataSetChanged();
+    }
+
+    public boolean isSelectionActive() {
+        return selectionActive;
+    }
+
+    public void selectItem(int position) {
+        accounts.get(position).toggleSelected();
+        notifyItemChanged(position);
+    }
+
+//    @NonNull
+//    @Override
+//    public View getView(int position, @Nullable View row, @NonNull ViewGroup parent) {
+//        LedgerAccount acc = getItem(position);
+//        LedgerRowHolder holder;
+//        if (row == null) {
+//            holder = new LedgerRowHolder();
+//            LayoutInflater vi = getSystemService(this.getContext(), LayoutInflater.class);
+//            MenuInflater mi = getSystemService(this.getContext(), MenuInflater.class);
+//
+//            if (vi == null)
+//                throw new IllegalStateException("Unable to instantiate the inflater " + "service");
+//            row = vi.inflate(R.layout.account_summary_row, parent, false);
+//            holder.tvAccountName = row.findViewById(R.id.account_row_acc_name);
+//            holder.tvAccountAmounts = row.findViewById(R.id.account_row_acc_amounts);
+//            row.setTag(R.id.VH, holder);
+//
+//            row.setPadding(context.getResources()
+//                            .getDimensionPixelSize(R.dimen.activity_horizontal_margin)/2, dp2px
+//                            (context, 3),
+//                    context.getResources()
+//                            .getDimensionPixelSize(R.dimen.activity_horizontal_margin)/2,
+//                    dp2px(context, 4));
+//            View.OnCreateContextMenuListener ccml = new View.OnCreateContextMenuListener() {
+//                @Override
+//                public void onCreateContextMenu(ContextMenu menu, View v,
+//                                                ContextMenu.ContextMenuInfo menuInfo) {
+//                    final ListView parent = (ListView) v.getParent();
+//                    int pos = parent.getPositionForView(v);
+//                    parent.setItemChecked(pos, true);
+//                    Log.d("list", String.format("checking pos %d", pos));
+//                }
+//            };
+//            row.setOnCreateContextMenuListener(ccml);
+//
+//        }
+//        else holder = (LedgerRowHolder) row.getTag(R.id.VH);
+//
+//        holder.tvAccountName.setText(acc.getShortName());
+//        holder.tvAccountName.setPadding(acc.getLevel() * context.getResources()
+//                .getDimensionPixelSize(R.dimen.activity_horizontal_margin), 0, 0, 0);
+//        holder.tvAccountAmounts.setText(acc.getAmountsString());
+//
+//        if (acc.isHidden()) {
+//            holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
+//            holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
+//        }
+//        else {
+//            holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
+//            holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
+//        }
+//
+//        int real_pos = ((ListView)parent).getPositionForView(row);
+//        Log.d("model", String.format("%s: real_pos=%d, position=%d", acc.getName(), real_pos,
+//                position));
+//        if (real_pos == -1) real_pos = position+1;
+//        else real_pos = real_pos + 1;
+//
+//        if ( real_pos % 2 == 0) {
+//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+//                row.setBackgroundColor(context.getResources()
+//                        .getColor(R.color.table_row_even_bg, context.getTheme()));
+//            }
+//            else {
+//                row.setBackgroundColor(
+//                        context.getResources().getColor(R.color.table_row_even_bg));
+//            }
+//        }
+//        else {
+//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+//                row.setBackgroundColor(context.getResources()
+//                        .getColor(R.color.drawer_background, context.getTheme()));
+//            }
+//            else {
+//                row.setBackgroundColor(context.getResources().getColor(R.color.drawer_background));
+//            }
+//        }
+//
+//        row.setTag(R.id.POS, position);
+//
+//        return row;
+//    }
+
+    class LedgerRowHolder extends RecyclerView.ViewHolder {
+        CheckBox selectionCb;
+        TextView tvAccountName, tvAccountAmounts;
+        LinearLayout row;
+
+        public LedgerRowHolder(@NonNull View itemView) {
+            super(itemView);
+            this.row = (LinearLayout) itemView;
+            this.tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
+            this.tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
+            this.selectionCb = itemView.findViewById(R.id.account_row_check);
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java b/app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java
new file mode 100644 (file)
index 0000000..f8e6d94
--- /dev/null
@@ -0,0 +1,12 @@
+package net.ktnx.mobileledger;
+
+import android.content.Context;
+import android.util.TypedValue;
+
+public class DimensionUtils {
+    public static int dp2px(Context context, float dp) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+               context.getResources().getDisplayMetrics()));
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java
new file mode 100644 (file)
index 0000000..27ab1d8
--- /dev/null
@@ -0,0 +1,113 @@
+package net.ktnx.mobileledger;
+
+import android.support.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class LedgerAccount {
+    private String name;
+    private String shortName;
+    private int level;
+    private String parentName;
+    private boolean hidden;
+    private List<LedgerAmount> amounts;
+    private boolean selected;
+    static Pattern higher_account = Pattern.compile("^[^:]+:");
+
+    LedgerAccount(String name) {
+        this.setName(name);
+        hidden = false;
+        selected = false;
+    }
+
+    public boolean isHidden() {
+        return hidden;
+    }
+
+    public void setHidden(boolean hidden) {
+        this.hidden = hidden;
+    }
+
+    LedgerAccount(String name, float amount) {
+        this.setName(name);
+        this.hidden = false;
+        this.amounts = new ArrayList<LedgerAmount>();
+        this.addAmount(amount);
+    }
+
+    public void setName(String name) {
+        this.name = name;
+        stripName();
+    }
+
+    private void stripName() {
+        level = 0;
+        shortName = name;
+        StringBuilder parentBuilder = new StringBuilder();
+        while (true) {
+            Matcher m = higher_account.matcher(shortName);
+            if (m.find()) {
+                level++;
+                parentBuilder.append(m.group(0));
+                shortName = m.replaceFirst("");
+            }
+            else break;
+        }
+        if (parentBuilder.length() > 0)
+            parentName = parentBuilder.substring(0, parentBuilder.length() - 1);
+        else parentName = null;
+    }
+
+    String getName() {
+        return name;
+    }
+
+    void addAmount(float amount, String currency) {
+        if (amounts == null ) amounts = new ArrayList<>();
+        amounts.add(new LedgerAmount(amount, currency));
+    }
+    void addAmount(float amount) {
+        this.addAmount(amount, null);
+    }
+
+    String getAmountsString() {
+        if ((amounts == null) || amounts.isEmpty()) return "";
+
+        StringBuilder builder = new StringBuilder();
+        for( LedgerAmount amount : amounts ) {
+            String amt = amount.toString();
+            if (builder.length() > 0) builder.append('\n');
+            builder.append(amt);
+        }
+
+        return builder.toString();
+    }
+
+    public int getLevel() {
+        return level;
+    }
+
+    @NonNull
+    public String getShortName() {
+        return shortName;
+    }
+
+    public String getParentName() {
+        return parentName;
+    }
+
+    public boolean isSelected() {
+        return selected;
+    }
+
+    public void setSelected(boolean selected) {
+        this.selected = selected;
+    }
+
+    public void toggleSelected() {
+        selected = !selected;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java b/app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java
new file mode 100644 (file)
index 0000000..6afea7d
--- /dev/null
@@ -0,0 +1,28 @@
+package net.ktnx.mobileledger;
+
+import android.annotation.SuppressLint;
+import android.support.annotation.NonNull;
+
+class LedgerAmount {
+    private String currency;
+    private float amount;
+
+    public
+    LedgerAmount(float amount, @NonNull String currency) {
+        this.currency = currency;
+        this.amount = amount;
+    }
+
+    public
+    LedgerAmount(float amount) {
+        this.amount = amount;
+        this.currency = null;
+    }
+
+    @SuppressLint("DefaultLocale")
+    @NonNull
+    public String toString() {
+        if (currency == null) return String.format("%,1.2f", amount);
+        else return String.format("%s %,1.2f", currency, amount);
+    }
+}
index 301b946fb8d3e8e0d272f498c7ecf2771be3a381..72a68eea2c08f9b6e83383afef4b83563a0274eb 100644 (file)
@@ -18,7 +18,7 @@ class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable {
     static final String DB_NAME = "mobile-ledger.db";
     static final String ACCOUNTS_TABLE = "accounts";
     static final String DESCRIPTION_HISTORY_TABLE = "description_history";
-    static final int LATEST_REVISION = 4;
+    static final int LATEST_REVISION = 6;
 
     final Context mContext;
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java b/app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java
new file mode 100644 (file)
index 0000000..df65bb5
--- /dev/null
@@ -0,0 +1,56 @@
+package net.ktnx.mobileledger;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+class RecyclerItemListener implements OnItemTouchListener {
+    private RecyclerTouchListener listener;
+    private GestureDetector gd;
+
+    interface RecyclerTouchListener {
+        void onClickItem(View v, int position);
+        void onLongClickItem(View v, int position);
+    }
+
+    public RecyclerItemListener(Context ctx, RecyclerView rv, RecyclerTouchListener listener) {
+        this.listener = listener;
+        this.gd = new GestureDetector(
+                ctx, new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public void onLongPress(MotionEvent e) {
+                View v = rv.findChildViewUnder(e.getX(), e.getY());
+                listener.onLongClickItem(v, rv.getChildAdapterPosition(v));
+            }
+
+            @Override
+            public boolean onSingleTapUp(MotionEvent e) {
+                View v = rv.findChildViewUnder(e.getX(), e.getY());
+                listener.onClickItem(v, rv.getChildAdapterPosition(v));
+                return true;
+            }
+        }
+        );
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+                                         @NonNull MotionEvent motionEvent) {
+        View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
+        return (v != null) && gd.onTouchEvent(motionEvent);
+    }
+
+    @Override
+    public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
+
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean b) {
+
+    }
+}
index 21963dd6b37abbbe0b85cc68af4682ab6cbdaf37..2c06849c87c74a1ff393a981cca0a391d1539cbd 100644 (file)
@@ -1,6 +1,5 @@
 package net.ktnx.mobileledger;
 
-import android.content.Context;
 import android.content.SharedPreferences;
 import android.database.sqlite.SQLiteDatabase;
 import android.util.Log;
@@ -10,19 +9,20 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URLDecoder;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-abstract public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
+class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
     int error;
 
     private SharedPreferences pref;
-    private final Context mContext;
+    WeakReference<AccountSummary> mContext;
 
-    RetrieveAccountsTask(Context context) {
+    RetrieveAccountsTask(WeakReference<AccountSummary> context) {
         mContext = context;
         error = 0;
     }
@@ -37,7 +37,7 @@ abstract public class RetrieveAccountsTask extends android.os.AsyncTask<Void, In
             http.setAllowUserInteraction(false);
             http.setRequestProperty("Accept-Charset", "UTF-8");
             publishProgress(0);
-            try(MobileLedgerDatabase dbh = new MobileLedgerDatabase(mContext)) {
+            try(MobileLedgerDatabase dbh = new MobileLedgerDatabase(mContext.get())) {
                 try(SQLiteDatabase db = dbh.getWritableDatabase()) {
                     try (InputStream resp = http.getInputStream()) {
                         Log.d("update_accounts", String.valueOf(http.getResponseCode()));
@@ -74,10 +74,7 @@ abstract public class RetrieveAccountsTask extends android.os.AsyncTask<Void, In
                                         acct_name = acct_name.replace("\"", "");
                                         Log.d("account-parser", acct_name);
 
-                                        db.execSQL(
-                                                "insert or replace into accounts(name, name_upper, "
-                                                        + "keep) values(?, ?, 1)",
-                                                new Object[]{acct_name, acct_name.toUpperCase()});
+                                        addAccount(db, acct_name);
                                         publishProgress(++count);
 
                                         last_account_name = acct_name;
@@ -162,7 +159,21 @@ abstract public class RetrieveAccountsTask extends android.os.AsyncTask<Void, In
         return null;
     }
 
-    abstract protected void onProgressUpdate(Integer... values);
+    private void addAccount(SQLiteDatabase db, String name) {
+        do {
+            LedgerAccount acc = new LedgerAccount(name);
+            db.execSQL(
+                    "insert or replace into accounts(name, name_upper, level, parent_name, keep) "
+                            + "values(?, ?, ?, ?, 1)",
+                    new Object[]{name, name.toUpperCase(), acc.getLevel(), acc.getParentName()});
+            name = acc.getParentName();
+        } while (name != null);
+    }
+    @Override
+    protected void onPostExecute(Void result) {
+        AccountSummary ctx = mContext.get();
+        if (ctx == null) return;
+        ctx.onAccountRefreshDone(this.error);
+    }
 
-    abstract protected void onPostExecute(Void result);
 }
diff --git a/app/src/main/res/drawable/ic_cancel_white_24dp.xml b/app/src/main/res/drawable/ic_cancel_white_24dp.xml
new file mode 100644 (file)
index 0000000..6b89f1f
--- /dev/null
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#EEEEEE"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_star_white_24dp.xml b/app/src/main/res/drawable/ic_star_white_24dp.xml
new file mode 100644 (file)
index 0000000..0c87e52
--- /dev/null
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#EEEEEE"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
+</vector>
diff --git a/app/src/main/res/layout/account_summary_row.xml b/app/src/main/res/layout/account_summary_row.xml
new file mode 100644 (file)
index 0000000..9617db6
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/account_summary_row"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:minHeight="36dp"
+    android:orientation="horizontal"
+    android:paddingStart="8dp"
+    android:paddingEnd="8dp"
+    tools:showIn="@layout/content_account_summary">
+
+    <CheckBox
+        android:id="@+id/account_row_check"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent" />
+
+    <TextView
+        android:id="@+id/account_row_acc_name"
+        style="@style/account_summary_account_name"
+        android:text="Account name, a really long one. A very very very long one. It may even spawn on more than two lines -- three, four or more." />
+
+    <TextView
+        android:id="@+id/account_row_acc_amounts"
+        style="@style/account_summary_amounts"
+        android:text="123,45\n678,90" />
+</LinearLayout>
\ No newline at end of file
index c30125ee74c0dd9819ed49755348cbfc000e2357..dc59bb5d3afa128bedf2757c8dcd8f11843ebe7d 100644 (file)
@@ -9,79 +9,19 @@
     tools:context=".AccountSummary"
     tools:showIn="@layout/app_bar_account_summary">
 
-    <TextView
-        android:id="@+id/textProgress"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="TextView"
-        android:visibility="gone"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        tools:ignore="HardcodedText" />
-
-    <ProgressBar
-        android:id="@+id/progressBar"
-        style="?android:attr/progressBarStyle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="8dp"
-        android:layout_marginEnd="8dp"
-        android:layout_marginBottom="16dp"
-        android:visibility="gone"
-        app:layout_constraintBottom_toTopOf="@+id/textProgress"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
-
-    <ScrollView
-        android:id="@+id/account_root_scroller"
+    <android.support.v4.widget.SwipeRefreshLayout
+        android:id="@+id/account_swiper"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        app:layout_constraintBottom_toTopOf="@+id/textProgress"
-        app:layout_constraintTop_toTopOf="parent">
+        android:layout_height="match_parent">
 
-        <LinearLayout
+        <android.support.v7.widget.RecyclerView
             android:id="@+id/account_root"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:choiceMode="multipleChoice"
+            android:drawSelectorOnTop="true"
             android:orientation="vertical">
 
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:gravity="center_vertical"
-                android:orientation="horizontal">
-
-                <TextView
-                    android:id="@+id/textView"
-                    style="@style/account_summary_account_name"
-                    android:text="Account name, a really long one. A very very very long one. It may even spawn on more than two lines -- three, four or more." />
-
-                <TextView
-                    android:id="@+id/textView2"
-                    style="@style/account_summary_amounts"
-                    android:text="123,45\n678,90" />
-            </LinearLayout>
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:gravity="center_vertical"
-                android:orientation="horizontal">
-
-                <TextView
-                    android:id="@+id/textView3"
-                    style="@style/account_summary_account_name"
-                    android:text="TextView" />
-
-                <TextView
-                    android:id="@+id/textView4"
-                    style="@style/account_summary_amounts"
-                    android:text="123,45\n678,90" />
-
-            </LinearLayout>
-
-        </LinearLayout>
-    </ScrollView>
-
+        </android.support.v7.widget.RecyclerView>
+    </android.support.v4.widget.SwipeRefreshLayout>
 </android.support.constraint.ConstraintLayout>
\ No newline at end of file
index 17f8525846a6414d6380ccce92bd81cc4c709c19..0a4b43617d2e56f37f6ca36800f9eb833e33c627 100644 (file)
@@ -2,12 +2,6 @@
 <menu xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item
-        android:id="@+id/menu_acc_summary_refresh"
-        android:icon="@drawable/ic_refresh_white_24dp"
-        android:title="@string/menu_acc_summary_refresh_title"
-        android:onClick="onRefreshAccountSummaryClicked"
-        app:showAsAction="ifRoom" />
     <item
         android:id="@+id/menu_acc_summary_show_hidden"
         android:checkable="true"
         app:actionLayout="@layout/switch_item"
         android:onClick="onShowHiddenAccountsClicked"
         app:showAsAction="never" />
+    <item android:id="@+id/menu_acc_summary_hide_selected"
+        android:icon="@drawable/ic_star_white_24dp"
+        android:title="@string/menu_acc_summary_hide_selected_title"
+        app:showAsAction="always"
+        android:visible="false"
+        />
+    <item android:id="@+id/menu_acc_summary_cancel_selection"
+        android:title="@string/menu_acc_summary_cancel_selection_title"
+        app:showAsAction="always"
+        android:visible="false"
+        android:onClick="onCancelAccSelection"
+        android:icon="@drawable/ic_cancel_white_24dp" />
 </menu>
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_5.sql b/app/src/main/res/raw/sql_5.sql
new file mode 100644 (file)
index 0000000..309f82e
--- /dev/null
@@ -0,0 +1,2 @@
+alter table accounts add level integer;
+alter table accounts add parent varchar;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_6.sql b/app/src/main/res/raw/sql_6.sql
new file mode 100644 (file)
index 0000000..c8e8634
--- /dev/null
@@ -0,0 +1,7 @@
+drop index idx_accounts_name;
+create table accounts_tmp(name varchar not null, name_upper varchar not null primary key, hidden boolean not null default 0, level integer not null default 0, parent_name varchar);
+insert or replace into accounts_tmp(name, name_upper, hidden, level, parent_name) select name, name_upper, hidden, level, parent from accounts;
+drop table accounts;
+create table accounts(name varchar not null, name_upper varchar not null primary key, hidden boolean not null default 0, level integer not null default 0, parent_name varchar, keep boolean default 1);
+insert into accounts(name, name_upper, hidden, level, parent_name) select name, name_upper, hidden, level, parent_name from accounts_tmp;
+drop table accounts_tmp;
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644 (file)
index 0000000..31d8b4f
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="VH" type="id" />
+    <item name="POS" type="id" />
+    <item name="SELECTING" type="id" />
+</resources>
\ No newline at end of file
index 903582cf5d386396e176ff4b0cbb2c598bec2b61..c05401623beeafab92f3b773e891cc1b7acf01dd 100644 (file)
@@ -79,4 +79,6 @@
     <string name="interface_pref_header_title">Interface</string>
     <string name="pref_show_hidden_accounts_off_summary">Hidden accounts are not visible in the account list</string>
     <string name="pref_show_hidden_accounts_on_summary">Hidden accounts are shown in the account list</string>
+    <string name="menu_acc_summary_hide_selected_title">Hide selected accounts</string>
+    <string name="menu_acc_summary_cancel_selection_title">Cancel selection</string>
 </resources>