From 943c92ca95431530e953738a9670f196fddd19be Mon Sep 17 00:00:00 2001 From: Michael Totschnig Date: Thu, 26 Oct 2023 15:26:53 +0200 Subject: [PATCH] Follow-up to #1232: Fix crash on device rotation Migrated from ViewPager to ViewPager2 --- .../myexpenses/activity/CsvImportActivity.kt | 79 ++---- .../myexpenses/activity/TabbedActivity.java | 92 ------- .../myexpenses/activity/TabbedActivity.kt | 54 ++++ .../fragment/CsvImportDataFragment.kt | 253 ++++++++++++------ .../viewmodel/CsvImportViewModel.kt | 41 ++- .../main/res/layout/activity_with_tabs.xml | 2 +- .../src/main/res/layout/import_csv_data.xml | 62 +++-- 7 files changed, 311 insertions(+), 272 deletions(-) delete mode 100644 myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.java create mode 100644 myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.kt diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/activity/CsvImportActivity.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/activity/CsvImportActivity.kt index 1533ad7e23..1bd05e4982 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/activity/CsvImportActivity.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/activity/CsvImportActivity.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.Menu import android.widget.AdapterView import androidx.activity.viewModels -import androidx.lifecycle.ViewModelProvider import com.evernote.android.state.State import org.apache.commons.csv.CSVRecord import org.totschnig.myexpenses.R @@ -27,8 +26,6 @@ import javax.inject.Inject class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { - @State - var dataReady = false @State var mUsageRecorded = false @@ -39,10 +36,6 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { @Inject lateinit var repository: Repository - private fun setDataReady() { - dataReady = true - mSectionsPagerAdapter.notifyDataSetChanged() - } private val csvImportViewModel: CsvImportViewModel by viewModels() @@ -56,7 +49,7 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { } override fun onPrepareOptionsMenu(menu: Menu): Boolean { - val allowed = parseFragment.isReady && idle + val allowed = parseFragment?.isReady == true && idle menu.findItem(R.id.PARSE_COMMAND)?.isEnabled = allowed menu.findItem(R.id.IMPORT_COMMAND)?.isEnabled = allowed super.onPrepareOptionsMenu(menu) @@ -86,33 +79,25 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { if (shouldGoBack()) super.onBackPressed() } - override fun setupTabs() { - //we only add the first tab, the second one once data has been parsed - addTab(0) - if (dataReady) { - addTab(1) - } + override fun createFragment(position: Int) = when(position) { + 0 -> CsvImportParseFragment.newInstance() + 1 -> CsvImportDataFragment.newInstance() + else -> throw IllegalArgumentException() } - private fun addTab(index: Int) { - when (index) { - 0 -> mSectionsPagerAdapter.addFragment( - CsvImportParseFragment.newInstance(), - getString(R.string.menu_parse) - ) - 1 -> mSectionsPagerAdapter.addFragment( - CsvImportDataFragment.newInstance(), - getString(R.string.csv_import_preview) - ) - } + override fun getItemCount() = 2 + + override fun getTitle(position: Int) = when(position) { + 0 -> getString(R.string.menu_parse) + 1 -> getString(R.string.csv_import_preview) + else -> throw IllegalArgumentException() } + override fun onPositive(args: Bundle, checked: Boolean) { super.onPositive(args, checked) if (args.getInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE) == R.id.SET_HEADER_COMMAND) { - (supportFragmentManager.findFragmentByTag( - mSectionsPagerAdapter.getFragmentName(1) - ) as? CsvImportDataFragment)?.setHeader(args.getInt(CsvImportDataFragment.KEY_HEADER_LINE_POSITION)) + dataFragment!!.setHeader(args.getInt(CsvImportDataFragment.KEY_HEADER_LINE_POSITION)) } } @@ -132,21 +117,8 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { showProgress() csvImportViewModel.parseFile(uri, delimiter, encoding).observe(this) { result -> hideProgress() - result.onSuccess { data -> - if (data.isNotEmpty()) { - if (!dataReady) { - addTab(1) - setDataReady() - } - (supportFragmentManager.findFragmentByTag( - mSectionsPagerAdapter.getFragmentName(1) - ) as? CsvImportDataFragment)?.let { - it.setData(data) - binding.viewPager.currentItem = 1 - } - } else { - showSnackBar(R.string.parse_error_no_data_found) - } + result.onSuccess { + binding.viewPager.currentItem = 1 }.onFailure { showSnackBar( when (it) { @@ -168,9 +140,9 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { dataSet, columnToFieldMap, dateFormat, - parseFragment.autoFillCategories, + parseFragment!!.autoFillCategories, AccountConfiguration(accountId, currency, accountType), - parseFragment.uri!! + parseFragment!!.uri!! ).observe(this) { result -> hideProgress() result.onSuccess { resultList -> @@ -206,26 +178,31 @@ class CsvImportActivity : TabbedActivity(), ConfirmationDialogListener { } } - private val parseFragment: CsvImportParseFragment + private val dataFragment: CsvImportDataFragment? + get() = supportFragmentManager.findFragmentByTag( + mSectionsPagerAdapter.getFragmentName(1) + ) as CsvImportDataFragment? + + private val parseFragment: CsvImportParseFragment? get() = supportFragmentManager.findFragmentByTag( mSectionsPagerAdapter.getFragmentName(0) - ) as CsvImportParseFragment + ) as? CsvImportParseFragment val accountId: Long get() { - return parseFragment.getSelectedAccountId() + return parseFragment!!.getSelectedAccountId() } val currency: String - get() = parseFragment.getSelectedCurrency() + get() = parseFragment!!.getSelectedCurrency() val dateFormat: QifDateFormat get() { - return parseFragment.dateFormat + return parseFragment!!.dateFormat } val accountType: AccountType get() { - return parseFragment.accountType + return parseFragment!!.accountType } } \ No newline at end of file diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.java b/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.java deleted file mode 100644 index 226d6dae86..0000000000 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.totschnig.myexpenses.activity; - -import android.os.Bundle; - -import org.totschnig.myexpenses.databinding.ActivityWithTabsBinding; -import org.totschnig.myexpenses.ui.FragmentPagerAdapter; - -import java.util.ArrayList; -import java.util.List; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; -import androidx.viewpager.widget.PagerAdapter; - -public abstract class TabbedActivity extends ProtectedFragmentActivity { - protected ActivityWithTabsBinding binding; - /** - * The {@link PagerAdapter} that will provide - * fragments for each of the sections. We use a - * {@link FragmentPagerAdapter} derivative, which will keep every - * loaded fragment in memory. If this becomes too memory intensive, it - * may be best to switch to a - * {@link FragmentStatePagerAdapter}. - */ - SectionsPagerAdapter mSectionsPagerAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityWithTabsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setupToolbar(); - - mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); - - setupTabs(); - - binding.viewPager.setAdapter(mSectionsPagerAdapter); - - binding.tabs.setupWithViewPager(binding.viewPager); - } - - protected abstract void setupTabs(); - - /** - * A {@link FragmentPagerAdapter} that returns a fragment corresponding to - * one of the sections/tabs/pages. - */ - public class SectionsPagerAdapter extends FragmentPagerAdapter { - - private final List mFragments = new ArrayList<>(); - private final List mFragmentTitles = new ArrayList<>(); - - public SectionsPagerAdapter(FragmentManager fm) { - super(fm); - } - - - public void addFragment(Fragment fragment, String title) { - mFragments.add(fragment); - mFragmentTitles.add(title); - } - - @Override - public Fragment getItem(int position) { - return mFragments.get(position); - } - - @Override - public int getCount() { - return mFragments.size(); - } - - @Override - public CharSequence getPageTitle(int position) { - return mFragmentTitles.get(position); - } - - public String getFragmentName(int currentPosition) { - //http://stackoverflow.com/questions/7379165/update-data-in-listfragment-as-part-of-viewpager - //would call this function if it were visible - //return makeFragmentName(R.id.viewpager_main,currentPosition); - return "android:switcher:" + binding.viewPager.getId() + ":" + getItemId(currentPosition); - } - } - - @Override - protected int getSnackBarContainerId() { - return binding.viewPager.getId(); - } -} diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.kt new file mode 100644 index 0000000000..f35ab8b459 --- /dev/null +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/activity/TabbedActivity.kt @@ -0,0 +1,54 @@ +package org.totschnig.myexpenses.activity + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayoutMediator +import org.totschnig.myexpenses.databinding.ActivityWithTabsBinding +import org.totschnig.myexpenses.ui.FragmentPagerAdapter + +abstract class TabbedActivity : ProtectedFragmentActivity() { + lateinit var binding: ActivityWithTabsBinding + + lateinit var mSectionsPagerAdapter: SectionsPagerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityWithTabsBinding.inflate( + layoutInflater + ) + setContentView(binding.root) + setupToolbar() + mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager) + binding.viewPager.adapter = mSectionsPagerAdapter + TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position -> + tab.text = getTitle(position) + }.attach() + } + + abstract fun getTitle(position: Int): CharSequence + + abstract fun createFragment(position: Int): Fragment + + abstract fun getItemCount(): Int + + /** + * A [FragmentPagerAdapter] that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ + inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentStateAdapter(fm, lifecycle) { + + fun getFragmentName(currentPosition: Int): String { + //https://stackoverflow.com/a/61178226/1199911 + return "f" + getItemId(currentPosition) + } + + override fun getItemCount() = this@TabbedActivity.getItemCount() + + override fun createFragment(position: Int) = this@TabbedActivity.createFragment(position) + } + + override val snackBarContainerId: Int + get() = binding.viewPager.id +} diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/fragment/CsvImportDataFragment.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/fragment/CsvImportDataFragment.kt index 6698b1fbdc..481556bf4f 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/fragment/CsvImportDataFragment.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/fragment/CsvImportDataFragment.kt @@ -20,7 +20,12 @@ import android.widget.TextView import androidx.appcompat.widget.AppCompatSpinner import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch import org.apache.commons.csv.CSVRecord import org.json.JSONException import org.json.JSONObject @@ -36,37 +41,38 @@ import org.totschnig.myexpenses.preference.PrefKey import org.totschnig.myexpenses.util.SparseBooleanArrayParcelable import org.totschnig.myexpenses.util.Utils import org.totschnig.myexpenses.util.crashreporting.CrashHandler +import org.totschnig.myexpenses.viewmodel.CsvImportViewModel import timber.log.Timber -import java.util.* import javax.inject.Inject class CsvImportDataFragment : Fragment() { + + private val viewModel: CsvImportViewModel by activityViewModels() + private var _binding: ImportCsvDataBinding? = null private val binding get() = _binding!! - private lateinit var dataSet: ArrayList - private lateinit var selectedRows: SparseBooleanArrayParcelable - private lateinit var mFieldAdapter: ArrayAdapter> + private var selectedRows: SparseBooleanArrayParcelable = SparseBooleanArrayParcelable() private lateinit var cellParams: LinearLayout.LayoutParams private var headerLine = -1 private var nrOfColumns: Int = 0 private val allFields: List> = listOf( - R.string.discard to null, - R.string.account to "ACCOUNT", - R.string.amount to "AMOUNT", - R.string.expense to "EXPENSE", - R.string.income to "INCOME", - R.string.date to "DATE", - R.string.booking_date to "BOOKING_DATE", - R.string.value_date to "VALUE_DATE", - R.string.payer_or_payee to "PAYEE", - R.string.comment to "COMMENT", - R.string.category to "CATEGORY", - R.string.subcategory to "SUB_CATEGORY", - R.string.method to "METHOD", - R.string.status to "STATUS", - R.string.reference_number to "NUMBER", - R.string.split_transaction to "SPLIT", - R.string.tags to "TAGS" + R.string.discard to null, + R.string.account to "ACCOUNT", + R.string.amount to "AMOUNT", + R.string.expense to "EXPENSE", + R.string.income to "INCOME", + R.string.date to "DATE", + R.string.booking_date to "BOOKING_DATE", + R.string.value_date to "VALUE_DATE", + R.string.payer_or_payee to "PAYEE", + R.string.comment to "COMMENT", + R.string.category to "CATEGORY", + R.string.subcategory to "SUB_CATEGORY", + R.string.method to "METHOD", + R.string.status to "STATUS", + R.string.reference_number to "NUMBER", + R.string.split_transaction to "SPLIT", + R.string.tags to "TAGS" ) private lateinit var fields: List> @@ -80,27 +86,35 @@ class CsvImportDataFragment : Fragment() { @Inject lateinit var prefHandler: PrefHandler + private val dataSet + get() = viewModel.dataFlow.value + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().application as MyApplication).appComponent.inject(this) setHasOptionsMenu(true) cellMinWidth = resources.getDimensionPixelSize(R.dimen.csv_import_cell_min_width) - checkboxColumnWidth = resources.getDimensionPixelSize(R.dimen.csv_import_checkbox_column_width) + checkboxColumnWidth = + resources.getDimensionPixelSize(R.dimen.csv_import_checkbox_column_width) cellMargin = resources.getDimensionPixelSize(R.dimen.csv_import_cell_margin) - spinnerRightPadding = resources.getDimensionPixelSize(R.dimen.csv_import_spinner_right_padding) - val withValueDate = prefHandler.getBoolean(PrefKey.TRANSACTION_WITH_VALUE_DATE, false) - fields = allFields.filter { - when (it.first) { - R.string.date -> !withValueDate - R.string.booking_date, R.string.value_date -> withValueDate - R.string.account -> (requireActivity() as CsvImportActivity).accountId== 0L - else -> true + spinnerRightPadding = + resources.getDimensionPixelSize(R.dimen.csv_import_spinner_right_padding) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.dataFlow.collect { + setData(it) + } + } } - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { val displayMetrics = resources.displayMetrics windowWidth = displayMetrics.widthPixels header2FieldMap = prefHandler.getString(PrefKey.CSV_IMPORT_HEADER_TO_FIELD_MAP, null)?.let { @@ -110,22 +124,7 @@ class CsvImportDataFragment : Fragment() { null } } ?: JSONObject() - mFieldAdapter = object : ArrayAdapter>( - requireActivity(), R.layout.spinner_item_narrow, 0, fields) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val tv = super.getView(position, convertView, parent) as TextView - tv.text = getString(fields[position].first) - return tv - } - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { - val tv = super.getDropDownView(position, convertView, parent) as TextView - tv.text = getString(fields[position].first) - return tv - } - }.also { - it.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item) - } _binding = ImportCsvDataBinding.inflate(inflater, container, false) // use this setting to improve performance if you know that changes @@ -134,12 +133,11 @@ class CsvImportDataFragment : Fragment() { binding.myRecyclerView.setHasFixedSize(true) if (savedInstanceState != null) { - setData( - savedInstanceState.getSerializable(KEY_DATA_SET) as? ArrayList, - savedInstanceState.getIntArray(KEY_MAPPING) - ) - selectedRows = savedInstanceState.getParcelable(KEY_SELECTED_ROWS)!! + savedInstanceState.getParcelable(KEY_SELECTED_ROWS)?.let { + selectedRows = it + } headerLine = savedInstanceState.getInt(KEY_HEADER_LINE_POSITION) + } return binding.root } @@ -149,15 +147,45 @@ class CsvImportDataFragment : Fragment() { _binding = null } - fun setData(data: List?, mapping: IntArray? = null) { - if (data.isNullOrEmpty()) return - dataSet = ArrayList(data) - nrOfColumns = dataSet.map { it.size() }.maxOrNull()!! - selectedRows = SparseBooleanArrayParcelable() - for (i in 0 until dataSet.size) { + private fun setData(data: List, mapping: IntArray? = null) { + if (data.isEmpty()) return + binding.switcher.displayedChild = 1 + val withValueDate = prefHandler.getBoolean(PrefKey.TRANSACTION_WITH_VALUE_DATE, false) + fields = allFields.filter { + when (it.first) { + R.string.date -> !withValueDate + R.string.booking_date, R.string.value_date -> withValueDate + R.string.account -> (requireActivity() as CsvImportActivity).accountId== 0L + else -> true + } + } + val fieldAdapter = object : ArrayAdapter>( + requireActivity(), R.layout.spinner_item_narrow, 0, fields + ) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val tv = super.getView(position, convertView, parent) as TextView + tv.text = getString(fields[position].first) + return tv + } + + override fun getDropDownView( + position: Int, + convertView: View?, + parent: ViewGroup + ): View { + val tv = super.getDropDownView(position, convertView, parent) as TextView + tv.text = getString(fields[position].first) + return tv + } + }.also { + it.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item) + } + nrOfColumns = data.maxOf { it.size() } + for (i in data.indices) { selectedRows.put(i, true) } - val availableCellWidth = ((windowWidth - checkboxColumnWidth - cellMargin * nrOfColumns * 2) / nrOfColumns) + val availableCellWidth = + ((windowWidth - checkboxColumnWidth - cellMargin * nrOfColumns * 2) / nrOfColumns) val cellWidth: Int val tableWidth: Int if (availableCellWidth > cellMinWidth) { @@ -165,7 +193,8 @@ class CsvImportDataFragment : Fragment() { tableWidth = windowWidth } else { cellWidth = cellMinWidth - tableWidth = cellMinWidth * nrOfColumns + checkboxColumnWidth + cellMargin * nrOfColumns * 2 + tableWidth = + cellMinWidth * nrOfColumns + checkboxColumnWidth + cellMargin * nrOfColumns * 2 } cellParams = LinearLayout.LayoutParams(cellWidth, MATCH_PARENT).apply { setMargins(cellMargin, cellMargin, cellMargin, cellMargin) @@ -183,7 +212,7 @@ class CsvImportDataFragment : Fragment() { for (i in 0 until nrOfColumns) { val cell = AppCompatSpinner(requireContext()) cell.id = ViewCompat.generateViewId() - cell.adapter = mFieldAdapter + cell.adapter = fieldAdapter ViewCompat.setPaddingRelative(cell, 0, 0, spinnerRightPadding, 0) addView(cell, cellParams) mapping?.get(i)?.let { cell.setSelection(it) } @@ -206,7 +235,9 @@ class CsvImportDataFragment : Fragment() { val fieldKey = header2FieldMap.getString(storedLabel) val fieldPosition = fields.indexOfFirst { it.second == fieldKey } if (fieldPosition != -1) { - (binding.headerLine.getChildAt(j + 1) as Spinner).setSelection(fieldPosition) + (binding.headerLine.getChildAt(j + 1) as Spinner).setSelection( + fieldPosition + ) continue@outer } } catch (e: JSONException) { @@ -224,10 +255,15 @@ class CsvImportDataFragment : Fragment() { } } - private inner class MyAdapter : RecyclerView.Adapter(), CompoundButton.OnCheckedChangeListener { + private inner class MyAdapter : RecyclerView.Adapter(), + CompoundButton.OnCheckedChangeListener { override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { val position = buttonView.tag as Int - Timber.d("%s item at position %d", if (isChecked) "Selecting" else "Discarding", position) + Timber.d( + "%s item at position %d", + if (isChecked) "Selecting" else "Discarding", + position + ) if (isChecked) { selectedRows.put(position, true) if (position == headerLine) { @@ -236,36 +272,56 @@ class CsvImportDataFragment : Fragment() { } else { if (headerLine == -1 && position == firstSelectedRow()) { ConfirmationDialogFragment.newInstance(Bundle().apply { - putInt(ConfirmationDialogFragment.KEY_TITLE, - R.string.dialog_title_information) + putInt( + ConfirmationDialogFragment.KEY_TITLE, + R.string.dialog_title_information + ) putString( - ConfirmationDialogFragment.KEY_MESSAGE, - getString(R.string.cvs_import_set_first_line_as_header)) - putInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE, - R.id.SET_HEADER_COMMAND) + ConfirmationDialogFragment.KEY_MESSAGE, + getString(R.string.cvs_import_set_first_line_as_header) + ) + putInt( + ConfirmationDialogFragment.KEY_COMMAND_POSITIVE, + R.id.SET_HEADER_COMMAND + ) putInt(KEY_HEADER_LINE_POSITION, position) - putInt(ConfirmationDialogFragment.KEY_POSITIVE_BUTTON_LABEL, R.string.response_yes) - putInt(ConfirmationDialogFragment.KEY_NEGATIVE_BUTTON_LABEL, R.string.response_no) + putInt( + ConfirmationDialogFragment.KEY_POSITIVE_BUTTON_LABEL, + R.string.response_yes + ) + putInt( + ConfirmationDialogFragment.KEY_NEGATIVE_BUTTON_LABEL, + R.string.response_no + ) }).show( - parentFragmentManager, "SET_HEADER_CONFIRMATION") + parentFragmentManager, "SET_HEADER_CONFIRMATION" + ) } selectedRows.delete(position) } notifyItemChanged(position) } - inner class ViewHolder(val itemBinding: ImportCsvDataRowBinding) : RecyclerView.ViewHolder(itemBinding.root) + inner class ViewHolder(val itemBinding: ImportCsvDataRowBinding) : + RecyclerView.ViewHolder(itemBinding.root) - override fun onCreateViewHolder(parent: ViewGroup, - viewType: Int): ViewHolder { - val itemBinding = ImportCsvDataRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val itemBinding = + ImportCsvDataRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) for (i in 0 until nrOfColumns) { val cell = TextView(parent.context) cell.setSingleLine() cell.ellipsize = TextUtils.TruncateAt.END cell.isSelected = true cell.gravity = Gravity.CENTER_HORIZONTAL - cell.setOnClickListener { v1: View -> (requireActivity() as ProtectedFragmentActivity).showSnackBar((v1 as TextView).text) } + cell.setOnClickListener { v1: View -> + (requireActivity() as ProtectedFragmentActivity).showSnackBar( + (v1 as TextView).text + ) + } if (viewType == 0) { cell.setTypeface(null, Typeface.BOLD) } @@ -299,10 +355,13 @@ class CsvImportDataFragment : Fragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putSerializable(KEY_DATA_SET, dataSet) outState.putParcelable(KEY_SELECTED_ROWS, selectedRows) outState.putInt(KEY_HEADER_LINE_POSITION, headerLine) - outState.putIntArray(KEY_MAPPING, (0 until nrOfColumns).map { (binding.headerLine.getChildAt(it + 1) as Spinner).selectedItemPosition }.toIntArray()) + outState.putIntArray( + KEY_MAPPING, + (0 until nrOfColumns).map { (binding.headerLine.getChildAt(it + 1) as Spinner).selectedItemPosition } + .toIntArray() + ) } @Deprecated("Deprecated in Java") @@ -316,13 +375,17 @@ class CsvImportDataFragment : Fragment() { val columnToFieldMap = IntArray(nrOfColumns) val header = headerLine.takeIf { it > -1 }?.let { dataSet[it] } for (i in 0 until nrOfColumns) { - val position = (binding.headerLine.getChildAt(i + 1) as Spinner).selectedItemPosition + val position = + (binding.headerLine.getChildAt(i + 1) as Spinner).selectedItemPosition columnToFieldMap[i] = fields[position].first if (position > 0) { header?.let { if (header.isSet(i)) { try { - header2FieldMap.put(Utils.normalize(header[i]), fields[position].second) + header2FieldMap.put( + Utils.normalize(header[i]), + fields[position].second + ) } catch (e: JSONException) { CrashHandler.report(e) } @@ -331,9 +394,16 @@ class CsvImportDataFragment : Fragment() { } } if (validateMapping(columnToFieldMap)) { - prefHandler.putString(PrefKey.CSV_IMPORT_HEADER_TO_FIELD_MAP, header2FieldMap.toString()) + prefHandler.putString( + PrefKey.CSV_IMPORT_HEADER_TO_FIELD_MAP, + header2FieldMap.toString() + ) val selectedData = dataSet.filterIndexed { index, _ -> selectedRows[index] } - (activity as? CsvImportActivity)?.importData(selectedData, columnToFieldMap, dataSet.size - selectedData.size) + (activity as? CsvImportActivity)?.importData( + selectedData, + columnToFieldMap, + dataSet.size - selectedData.size + ) } } return super.onOptionsItemSelected(item) @@ -354,7 +424,12 @@ class CsvImportDataFragment : Fragment() { for (field in columnToFieldMap) { if (field != R.string.discard) { if (foundFields[field, false]) { - activity.showSnackBar(getString(R.string.csv_import_field_mapped_more_than_once, getString(field))) + activity.showSnackBar( + getString( + R.string.csv_import_field_mapped_more_than_once, + getString(field) + ) + ) return false } foundFields.put(field, true) @@ -365,8 +440,9 @@ class CsvImportDataFragment : Fragment() { return false } if (!(foundFields[R.string.amount, false] || - foundFields[R.string.expense, false] || - foundFields[R.string.income, false])) { + foundFields[R.string.expense, false] || + foundFields[R.string.income, false]) + ) { activity.showSnackBar(R.string.csv_import_no_mapping_found_for_amount) return false } @@ -374,14 +450,13 @@ class CsvImportDataFragment : Fragment() { } private fun firstSelectedRow(): Int { - for (i in 0 until dataSet.size) { + for (i in dataSet.indices) { if (selectedRows.get(i)) return i } return -1 } companion object { - const val KEY_DATA_SET = "DATA_SET" const val KEY_SELECTED_ROWS = "SELECTED_ROWS" const val KEY_HEADER_LINE_POSITION = "HEADER_LINE_POSITION" const val KEY_MAPPING = "MAPPING" diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/viewmodel/CsvImportViewModel.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/viewmodel/CsvImportViewModel.kt index 2d86116035..9b3870a70b 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/viewmodel/CsvImportViewModel.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/viewmodel/CsvImportViewModel.kt @@ -3,7 +3,10 @@ package org.totschnig.myexpenses.viewmodel import android.app.Application import android.net.Uri import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.liveData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVRecord import org.totschnig.myexpenses.R @@ -14,27 +17,37 @@ import org.totschnig.myexpenses.io.CSVParser import org.totschnig.myexpenses.model.AccountType import org.totschnig.myexpenses.model2.Account import org.totschnig.myexpenses.provider.TransactionProvider +import org.totschnig.myexpenses.util.ResultUnit import java.io.InputStreamReader data class AccountConfiguration(val id: Long, val currency: String, val type: AccountType) -class CsvImportViewModel(application: Application) : ImportDataViewModel(application) { +class CsvImportViewModel(application: Application, val savedStateHandle: SavedStateHandle) : + ImportDataViewModel(application) { - override val format= "CSV" + override val format = "CSV" - fun parseFile(uri: Uri, delimiter: Char, encoding: String): LiveData>> = + val dataFlow: StateFlow> = savedStateHandle.getStateFlow("data", emptyList()) + + private var data: List + get() = savedStateHandle["data"] ?: emptyList() + set(value) { + savedStateHandle["data"] = value + } + + + fun parseFile(uri: Uri, delimiter: Char, encoding: String): LiveData> = liveData(context = coroutineContext()) { - try { - contentResolver.openInputStream(uri)?.use { - emit( - Result.success( - CSVFormat.DEFAULT.withDelimiter(delimiter) - .parse(InputStreamReader(it, encoding)).records - ) - ) - } ?: throw java.lang.Exception("OpenInputStream returned null") - } catch (e: Exception) { - emit(Result.failure(e)) + contentResolver.openInputStream(uri)?.use { + try { + contentResolver.openInputStream(uri)?.use { + data = CSVFormat.DEFAULT.withDelimiter(delimiter) + .parse(InputStreamReader(it, encoding)).records + emit(ResultUnit) + } ?: throw java.lang.Exception("OpenInputStream returned null") + } catch (e: Exception) { + emit(Result.failure(e)) + } } } diff --git a/myExpenses/src/main/res/layout/activity_with_tabs.xml b/myExpenses/src/main/res/layout/activity_with_tabs.xml index 7886b1ba68..5722f86b3c 100644 --- a/myExpenses/src/main/res/layout/activity_with_tabs.xml +++ b/myExpenses/src/main/res/layout/activity_with_tabs.xml @@ -38,7 +38,7 @@ - - + android:layout_height="match_parent"> - + + + android:fillViewport="true"> + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="vertical"> + + - + + + + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> - - - - \ No newline at end of file + + \ No newline at end of file