diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml index 2688f0e8..28ba58a6 100644 --- a/android_app/app/src/main/AndroidManifest.xml +++ b/android_app/app/src/main/AndroidManifest.xml @@ -34,6 +34,16 @@ + + + + diff --git a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java index e3b70b9d..21b47b5f 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java @@ -21,8 +21,11 @@ import android.arch.persistence.room.Room; import android.arch.persistence.room.RoomDatabase; import android.content.Context; import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; import android.os.Handler; import android.preference.PreferenceManager; +import android.provider.OpenableColumns; import android.support.v4.app.Fragment; import android.text.format.DateFormat; import android.util.Log; @@ -46,9 +49,10 @@ import com.health.openscale.core.utils.CsvHelper; import com.health.openscale.gui.fragments.FragmentUpdateListener; import java.io.BufferedReader; -import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; @@ -328,10 +332,20 @@ public class OpenScale { updateScaleData(); } - public void importData(String filename) { + private String getFilenameFromUri(Uri uri) { + Cursor cursor = context.getContentResolver().query( + uri, null, null, null, null); + cursor.moveToFirst(); + return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + + public void importData(Uri uri) { try { + final String filename = getFilenameFromUri(uri); + + InputStream input = context.getContentResolver().openInputStream(uri); List csvScaleMeasurementList = - CsvHelper.importFrom(new BufferedReader(new FileReader(filename))); + CsvHelper.importFrom(new BufferedReader(new InputStreamReader(input))); final int userId = getSelectedScaleUser().getId(); for (ScaleMeasurement measurement : csvScaleMeasurementList) { @@ -340,11 +354,11 @@ public class OpenScale { measurementDAO.insertAll(csvScaleMeasurementList); updateScaleData(); - Toast.makeText(context, context.getString(R.string.info_data_imported) + " /sdcard" + filename, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getString(R.string.info_data_imported) + " " + filename, Toast.LENGTH_SHORT).show(); } catch (IOException e) { - Toast.makeText(context, context.getString(R.string.error_importing) + e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getString(R.string.error_importing) + ": " + e.getMessage(), Toast.LENGTH_SHORT).show(); } catch (ParseException e) { - Toast.makeText(context, context.getString(R.string.error_importing) + e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getString(R.string.error_importing) + ": " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/utils/CsvHelper.java b/android_app/app/src/main/java/com/health/openscale/core/utils/CsvHelper.java index 5403be3f..77161fa8 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/utils/CsvHelper.java +++ b/android_app/app/src/main/java/com/health/openscale/core/utils/CsvHelper.java @@ -33,6 +33,10 @@ public class CsvHelper { } private static String[] getOldStyleHeaders(String sampleLine) { + if (sampleLine == null) { + return null; + } + final String[] fields = sampleLine.split(",", -1); // Return an array with header fields so that all the headers that actually are diff --git a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java index 3b985c90..c53dd7ce 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java @@ -279,7 +279,7 @@ public class MainActivity extends AppCompatActivity { case R.id.nav_settings: Intent settingsIntent = new Intent(this, SettingsActivity.class); settingsIntent.putExtra(SettingsActivity.EXTRA_TINT_COLOR, navDrawer.getItemTextColor().getDefaultColor()); - startActivityForResult(settingsIntent, 1); + startActivity(settingsIntent); return; case R.id.nav_help: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/oliexdev/openScale/wiki"))); @@ -336,7 +336,7 @@ public class MainActivity extends AppCompatActivity { return true; case R.id.action_add_measurement: Intent intent = new Intent(getApplicationContext(), DataEntryActivity.class); - startActivityForResult(intent, 1); + startActivity(intent); return true; case R.id.action_bluetooth_status: invokeSearchBluetoothDevice(); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java index 829e54b2..b2cd7aa2 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java @@ -640,7 +640,7 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { Intent intent = new Intent(graphView.getContext(), DataEntryActivity.class); intent.putExtra(DataEntryActivity.EXTRA_ID, id); - startActivityForResult(intent, 1); + startActivity(intent); } @Override diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java index 0fdd1ab1..369f7e15 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java @@ -29,9 +29,9 @@ import android.os.Environment; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; +import android.support.v4.content.FileProvider; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.SpannedString; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MenuItem; @@ -50,6 +50,7 @@ import android.widget.TableRow; import android.widget.TextView; import android.widget.Toast; +import com.health.openscale.BuildConfig; import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; @@ -78,6 +79,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import static android.app.Activity.RESULT_OK; import static android.util.TypedValue.COMPLEX_UNIT_DIP; public class TableFragment extends Fragment implements FragmentUpdateListener { @@ -93,7 +95,9 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { private ArrayList measurementsList; private int selectedSubpageNr; - private static String SELECTED_SUBPAGE_NR_KEY = "selectedSubpageNr"; + private static final String SELECTED_SUBPAGE_NR_KEY = "selectedSubpageNr"; + + private static final int IMPORT_DATA_REQUEST = 100; public TableFragment() { @@ -109,6 +113,9 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { tableDataView = (ListView) tableView.findViewById(R.id.tableDataView); tableHeaderView = (LinearLayout) tableView.findViewById(R.id.tableHeaderView); + tableDataView.setAdapter(new ListViewAdapter()); + tableDataView.setOnItemClickListener(new onClickListenerRow()); + optionMenu = (ImageView) tableView.findViewById(R.id.optionMenu); measurementsList = new ArrayList<>(); @@ -154,12 +161,9 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { case R.id.importData: - if (PermissionHelper.requestReadPermission(getActivity())) { - importTable(); - } + importTable(); return true; case R.id.exportData: if (PermissionHelper.requestWritePermission(getActivity())) { @@ -167,9 +171,7 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { } return true; case R.id.shareData: - if (PermissionHelper.requestWritePermission(getActivity())) { - shareTable(); - } + shareTable(); return true; default: return false; @@ -192,15 +194,12 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { @Override public void updateOnView(List scaleMeasurementList) { - tableDataView.setAdapter(new ListViewAdapter(new ArrayList>())); // delete all data in the table with an empty adapter array list - - if (scaleMeasurementList.isEmpty()) { - return; - } - final int maxSize = 25; - int subpageCount = (int)Math.ceil(scaleMeasurementList.size() / (double)maxSize); + final int subpageCount = (int)Math.ceil(scaleMeasurementList.size() / (double)maxSize); + if (selectedSubpageNr >= subpageCount) { + selectedSubpageNr = Math.max(0, subpageCount - 1); + } subpageView.removeAllViews(); @@ -216,7 +215,7 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { moveSubpageLeft.setEnabled(selectedSubpageNr > 0); subpageView.addView(moveSubpageLeft); - for (int i=0; i 1) { TextView selectedSubpageNrView = (TextView) subpageView.getChildAt(selectedSubpageNr + 1); if (selectedSubpageNrView != null) { @@ -247,6 +247,7 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { tableHeaderView.removeAllViews(); + ArrayList visibleMeasurements = new ArrayList<>(); for (MeasurementView measurement : measurementsList) { measurement.updatePreferences(prefs); @@ -259,50 +260,16 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { headerIcon.getLayoutParams().height = pxImageDp(20); tableHeaderView.addView(headerIcon); + + visibleMeasurements.add(measurement); } } - ArrayList> dataRowList = new ArrayList<>(); + ListViewAdapter adapter = (ListViewAdapter) tableDataView.getAdapter(); - int displayCount = 0; - - for (int i = (maxSize * selectedSubpageNr); i < scaleMeasurementList.size(); i++) { - ScaleMeasurement scaleMeasurement = scaleMeasurementList.get(i); - - ScaleMeasurement prevScaleMeasurement = null; - if (i < scaleMeasurementList.size() - 1) { - prevScaleMeasurement = scaleMeasurementList.get(i + 1); - } - - HashMap dataRow = new HashMap<>(); - - int columnNr = 0; - dataRow.put(columnNr++, new SpannedString(Long.toString(scaleMeasurement.getId()))); - - for (MeasurementView measurement : measurementsList) { - measurement.loadFrom(scaleMeasurement, prevScaleMeasurement); - - if (measurement.isVisible()) { - SpannableStringBuilder text = new SpannableStringBuilder(); - text.append(measurement.getValueAsString()); - text.append("\n"); - measurement.appendDiffValue(text); - - dataRow.put(columnNr++, text); - } - } - - dataRowList.add(dataRow); - - displayCount++; - - if (maxSize <= displayCount) { - break; - } - } - - tableDataView.setAdapter(new ListViewAdapter(dataRowList)); - tableDataView.setOnItemClickListener(new onClickListenerRow()); + final int startOffset = maxSize * selectedSubpageNr; + final int endOffset = Math.min(startOffset + maxSize + 1, scaleMeasurementList.size()); + adapter.setMeasurements(visibleMeasurements, scaleMeasurementList.subList(startOffset, endOffset), maxSize); } private int pxImageDp(float dp) { @@ -311,21 +278,26 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { private class onClickListenerRow implements AdapterView.OnItemClickListener { @Override - public void onItemClick(AdapterView parent, View view, int position, long click_id) { - LinearLayout dataRow = (LinearLayout)view; - TextView idTextView = (TextView) dataRow.getChildAt(0); - int id = Integer.parseInt(idTextView.getText().toString()); - + public void onItemClick(AdapterView parent, View view, int position, long id) { Intent intent = new Intent(tableView.getContext(), DataEntryActivity.class); - intent.putExtra(DataEntryActivity.EXTRA_ID, id); - startActivityForResult(intent, 1); } + intent.putExtra(DataEntryActivity.EXTRA_ID, (int)id); + startActivity(intent); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == IMPORT_DATA_REQUEST && resultCode == RESULT_OK && data != null) { + OpenScale.getInstance(getContext()).importData(data.getData()); + } } private void importTable() { int selectedUserId = OpenScale.getInstance(getContext()).getSelectedScaleUserId(); - if (selectedUserId == -1) - { + if (selectedUserId == -1) { AlertDialog.Builder infoDialog = new AlertDialog.Builder(getContext()); infoDialog.setMessage(getResources().getString(R.string.info_no_selected_user)); @@ -334,36 +306,14 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { infoDialog.show(); } - else - { - AlertDialog.Builder filenameDialog = new AlertDialog.Builder(getActivity()); + else { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/*"); - filenameDialog.setTitle(getResources().getString(R.string.info_set_filename) + " /sdcard ..."); - - String exportFilename = prefs.getString("exportFilename", "/openScale_data_" + OpenScale.getInstance(getContext()).getSelectedScaleUser().getUserName() + ".csv"); - - final EditText txtFilename = new EditText(tableView.getContext()); - txtFilename.setText(exportFilename); - - filenameDialog.setView(txtFilename); - - filenameDialog.setPositiveButton(getResources().getString(R.string.label_ok), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - OpenScale.getInstance(getContext()).importData(Environment.getExternalStorageDirectory().getPath() + txtFilename.getText().toString()); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(tableView.getContext()); - prefs.edit().putString("exportFilename", txtFilename.getText().toString()).commit(); - updateOnView(OpenScale.getInstance(getContext()).getScaleMeasurementList()); - } - }); - - filenameDialog.setNegativeButton(getResources().getString(R.string.label_cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.dismiss(); - } - }); - - - filenameDialog.show(); + startActivityForResult( + Intent.createChooser(intent, getResources().getString(R.string.label_import)), + IMPORT_DATA_REQUEST); } } @@ -401,29 +351,27 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { filenameDialog.show(); } - private void shareTable() { final ScaleUser selectedScaleUser = OpenScale.getInstance(getContext()).getSelectedScaleUser(); - String exportFilename = prefs.getString("exportFilename" + selectedScaleUser.getId(), "openScale_data_" + selectedScaleUser.getUserName() + ".csv"); - String fullPath = Environment.getExternalStorageDirectory().getPath() + "/tmp/" + exportFilename; - - if (!OpenScale.getInstance(getContext()).exportData(fullPath)) { + File shareFile = new File(getContext().getCacheDir(), + String.format("openScale %s.csv", selectedScaleUser.getUserName())); + if (!OpenScale.getInstance(getContext()).exportData(shareFile.getPath())) { return; } - Intent intentShareFile = new Intent(Intent.ACTION_SEND); - File shareFile = new File(fullPath); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType("text/csv"); - if(shareFile.exists()) { - intentShareFile.setType("text/comma-separated-values"); - intentShareFile.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://"+fullPath)); + final Uri uri = FileProvider.getUriForFile( + getContext(), BuildConfig.APPLICATION_ID + ".fileprovider", shareFile); + intent.putExtra(Intent.EXTRA_STREAM, uri); - intentShareFile.putExtra(Intent.EXTRA_SUBJECT, "openScale export csv file"); - intentShareFile.putExtra(Intent.EXTRA_TEXT, exportFilename); + intent.putExtra(Intent.EXTRA_SUBJECT, + getResources().getString(R.string.label_share_subject, selectedScaleUser.getUserName())); - startActivity(Intent.createChooser(intentShareFile, getResources().getString(R.string.label_share))); - } + startActivity(Intent.createChooser(intent, getResources().getString(R.string.label_share))); } @Override @@ -480,69 +428,105 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { private class ListViewAdapter extends BaseAdapter { - private ArrayList> dataList; - private LinearLayout row; + private List visibleMeasurements; + private List scaleMeasurements; + private int measurementsToShow = 0; - public ListViewAdapter(ArrayList> list) { - super(); - this.dataList = list; + private Spanned[][] stringCache; + + private ArrayList> dataList; + + public void setMeasurements(List visibleMeasurements, + List scaleMeasurements, + int maxSize) { + this.visibleMeasurements = visibleMeasurements; + this.scaleMeasurements = scaleMeasurements; + measurementsToShow = Math.min(scaleMeasurements.size(), maxSize); + + stringCache = new Spanned[measurementsToShow][visibleMeasurements.size()]; + + notifyDataSetChanged(); } @Override public int getCount() { - return dataList.size(); + return measurementsToShow; } @Override public Object getItem(int position) { - return dataList.get(position); + return scaleMeasurements.get(position); } @Override public long getItemId(int position) { - return 0; + return scaleMeasurements.get(position).getId(); } @Override public View getView(int position, View convertView, ViewGroup parent) { - if (dataList.isEmpty()) { - return convertView; - } + // Create entries in stringCache if needed + if (stringCache[position][0] == null) { + ScaleMeasurement measurement = scaleMeasurements.get(position); + ScaleMeasurement prevMeasurement = null; + if (position + 1 < scaleMeasurements.size()) { + prevMeasurement = scaleMeasurements.get(position + 1); + } - if (convertView == null) { - row = new LinearLayout(getContext()); - convertView = row; + for (int i = 0; i < visibleMeasurements.size(); ++i) { + visibleMeasurements.get(i).loadFrom(measurement, prevMeasurement); - for (int i = 0; i< dataList.get(0).size(); i++) { - TextView column = new TextView(getContext()); - column.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); - column.getLayoutParams().width = 0; - column.setGravity(Gravity.CENTER); + SpannableStringBuilder string = new SpannableStringBuilder(); + string.append(visibleMeasurements.get(i).getValueAsString()); + visibleMeasurements.get(i).appendDiffValue(string); - if ((getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) != Configuration.SCREENLAYOUT_SIZE_XLARGE && - (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) != Configuration.SCREENLAYOUT_SIZE_LARGE) { - column.setTextSize(COMPLEX_UNIT_DIP, 9); - } - - if (i == 0) { - column.setVisibility(View.GONE); - } - - row.addView(column); + stringCache[position][i] = string; } } - LinearLayout convView = (LinearLayout)convertView; + // Create view if needed + LinearLayout row; + if (convertView == null) { + row = new LinearLayout(getContext()); - HashMap map = dataList.get(position); + final int screenSize = getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; + final boolean isSmallScreen = + screenSize != Configuration.SCREENLAYOUT_SIZE_XLARGE + && screenSize != Configuration.SCREENLAYOUT_SIZE_LARGE; - for (int i = 0; i < map.size(); i++) { - TextView column = (TextView)convView.getChildAt(i); - column.setText(map.get(i)); + for (int i = 0; i < visibleMeasurements.size(); ++i) { + TextView column = new TextView(getContext()); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + 1); + layoutParams.width = 0; + column.setLayoutParams(layoutParams); + column.setMinLines(2); + column.setGravity(Gravity.CENTER_HORIZONTAL); + + if (isSmallScreen) { + column.setTextSize(COMPLEX_UNIT_DIP, 9); + } + row.addView(column); + } + } + else { + row = (LinearLayout) convertView; } - return convertView; + // Fill view with data + for (int i = 0; i < visibleMeasurements.size(); ++i) { + TextView column = (TextView) row.getChildAt(i); + column.setText(stringCache[position][i]); + } + + return row; } + @Override + public boolean hasStableIds() { + return true; + } } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java index b0ec0bfa..166ea1a3 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java @@ -286,6 +286,7 @@ public abstract class FloatMeasurementView extends MeasurementView { color = Color.GRAY; } + text.append('\n'); int start = text.length(); text.append(symbol); text.setSpan(new ForegroundColorSpan(color), start, text.length(), diff --git a/android_app/app/src/main/res/layout/fragment_table.xml b/android_app/app/src/main/res/layout/fragment_table.xml index 49c8d12f..39f9ed3c 100644 --- a/android_app/app/src/main/res/layout/fragment_table.xml +++ b/android_app/app/src/main/res/layout/fragment_table.xml @@ -69,7 +69,8 @@ + android:layout_height="match_parent" + android:layout_marginHorizontal="5dp"> diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 3b26ccb7..4b2e84b1 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Delete Add user Share + openScale CSV data export (%s) Id Weight diff --git a/android_app/app/src/main/res/xml/file_provider_paths.xml b/android_app/app/src/main/res/xml/file_provider_paths.xml new file mode 100644 index 00000000..b7974d83 --- /dev/null +++ b/android_app/app/src/main/res/xml/file_provider_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android_app/app/src/test/java/com/health/openscale/CsvHelperTest.java b/android_app/app/src/test/java/com/health/openscale/CsvHelperTest.java index d801bcb5..d881759b 100644 --- a/android_app/app/src/test/java/com/health/openscale/CsvHelperTest.java +++ b/android_app/app/src/test/java/com/health/openscale/CsvHelperTest.java @@ -24,6 +24,7 @@ import org.junit.Test; import java.io.BufferedReader; import java.io.StringReader; import java.io.StringWriter; +import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -181,4 +182,10 @@ public class CsvHelperTest { assertEquals(1, list.size()); validateEntry(list.get(0), 0); } + + @Test(expected = ParseException.class) + public void empty() throws Exception { + final String data = ""; + CsvHelper.importFrom(new BufferedReader(new StringReader(data))); + } }