mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Refactor CSVExporter
This commit is contained in:
@@ -35,6 +35,7 @@ dependencies {
|
||||
compile 'com.android.support:support-v4:23.1.1'
|
||||
compile 'com.github.paolorotolo:appintro:3.4.0'
|
||||
compile 'org.apmem.tools:layouts:1.10@aar'
|
||||
compile 'com.opencsv:opencsv:3.7'
|
||||
compile project(':libs:drag-sort-listview:library')
|
||||
compile files('libs/ActiveAndroid.jar')
|
||||
|
||||
|
||||
@@ -91,4 +91,9 @@ public class ColorHelper
|
||||
hsv[index] = newValue;
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
public static String toHTML(int color)
|
||||
{
|
||||
return String.format("#%06X", 0xFFFFFF & color);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.text.format.DateFormat;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
@@ -96,6 +97,14 @@ public class DateHelper
|
||||
return df.format(date);
|
||||
}
|
||||
|
||||
public static SimpleDateFormat getCSVDateFormat()
|
||||
{
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
return dateFormat;
|
||||
}
|
||||
|
||||
public static String formatHeaderDate(GregorianCalendar day)
|
||||
{
|
||||
String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH));
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.isoron.uhabits.commands.ChangeHabitColorCommand;
|
||||
import org.isoron.uhabits.commands.DeleteHabitsCommand;
|
||||
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
|
||||
import org.isoron.uhabits.fragments.EditHabitFragment;
|
||||
import org.isoron.uhabits.io.CSVExporter;
|
||||
import org.isoron.uhabits.io.HabitsExporter;
|
||||
import org.isoron.uhabits.loaders.HabitListLoader;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
@@ -226,7 +226,7 @@ public class HabitSelectionCallback implements ActionMode.Callback
|
||||
{
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
String filename;
|
||||
String archiveFilename;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute()
|
||||
@@ -241,15 +241,19 @@ public class HabitSelectionCallback implements ActionMode.Callback
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid)
|
||||
{
|
||||
if(filename != null)
|
||||
if(archiveFilename != null)
|
||||
{
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.setType("application/zip");
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(archiveFilename)));
|
||||
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
else
|
||||
{
|
||||
activity.showToast(R.string.could_not_export);
|
||||
}
|
||||
|
||||
if(progressBar != null)
|
||||
progressBar.setVisibility(View.GONE);
|
||||
@@ -258,8 +262,10 @@ public class HabitSelectionCallback implements ActionMode.Callback
|
||||
@Override
|
||||
protected Void doInBackground(Void... params)
|
||||
{
|
||||
CSVExporter exporter = new CSVExporter(activity, selectedHabits);
|
||||
filename = exporter.writeArchive();
|
||||
String dirName = String.format("%s/export/", activity.getExternalCacheDir());
|
||||
HabitsExporter exporter = new HabitsExporter(selectedHabits, dirName);
|
||||
archiveFilename = exporter.writeArchive();
|
||||
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.io;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.isoron.uhabits.models.Score;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class CSVExporter
|
||||
{
|
||||
private List<Habit> habits;
|
||||
private Context context;
|
||||
private java.text.DateFormat dateFormat;
|
||||
|
||||
private List<String> generateDirs;
|
||||
private List<String> generateFilenames;
|
||||
|
||||
private String basePath;
|
||||
|
||||
public CSVExporter(Context context, List<Habit> habits)
|
||||
{
|
||||
this.habits = habits;
|
||||
this.context = context;
|
||||
generateDirs = new LinkedList<>();
|
||||
generateFilenames = new LinkedList<>();
|
||||
|
||||
basePath = String.format("%s/export/", context.getFilesDir());
|
||||
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public String formatDate(long timestamp)
|
||||
{
|
||||
return dateFormat.format(new Date(timestamp));
|
||||
}
|
||||
|
||||
public String formatScore(int score)
|
||||
{
|
||||
return String.format("%.2f", ((float) score) / Score.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void writeScores(String dirPath, Habit habit) throws IOException
|
||||
{
|
||||
String path = dirPath + "scores.csv";
|
||||
FileWriter out = new FileWriter(basePath + path);
|
||||
generateFilenames.add(path);
|
||||
|
||||
String query = "select timestamp, score from score where habit = ? order by timestamp";
|
||||
String params[] = { habit.getId().toString() };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
if(!cursor.moveToFirst()) return;
|
||||
|
||||
do
|
||||
{
|
||||
String timestamp = formatDate(cursor.getLong(0));
|
||||
String score = formatScore(cursor.getInt(1));
|
||||
out.write(String.format("%s,%s\n", timestamp, score));
|
||||
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
out.close();
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
private void writeCheckmarks(String dirPath, Habit habit) throws IOException
|
||||
{
|
||||
String path = dirPath + "checkmarks.csv";
|
||||
FileWriter out = new FileWriter(basePath + path);
|
||||
generateFilenames.add(path);
|
||||
|
||||
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
|
||||
String params[] = { habit.getId().toString() };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
if(!cursor.moveToFirst()) return;
|
||||
|
||||
do
|
||||
{
|
||||
String timestamp = formatDate(cursor.getLong(0));
|
||||
Integer value = cursor.getInt(1);
|
||||
out.write(String.format("%s,%d\n", timestamp, value));
|
||||
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
out.close();
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
private void writeFiles(Habit habit) throws IOException
|
||||
{
|
||||
String path = String.format("%s/", habit.name);
|
||||
new File(basePath + path).mkdirs();
|
||||
generateDirs.add(path);
|
||||
|
||||
writeScores(path, habit);
|
||||
writeCheckmarks(path, habit);
|
||||
}
|
||||
|
||||
private void writeZipFile(String zipFilename) throws IOException
|
||||
{
|
||||
FileOutputStream fos = new FileOutputStream(zipFilename);
|
||||
ZipOutputStream zos = new ZipOutputStream(fos);
|
||||
|
||||
for(String filename : generateFilenames)
|
||||
addFileToZip(zos, filename);
|
||||
|
||||
zos.close();
|
||||
fos.close();
|
||||
}
|
||||
|
||||
private void addFileToZip(ZipOutputStream zos, String filename) throws IOException
|
||||
{
|
||||
FileInputStream fis = new FileInputStream(new File(basePath + filename));
|
||||
ZipEntry ze = new ZipEntry(filename);
|
||||
zos.putNextEntry(ze);
|
||||
|
||||
int length;
|
||||
byte bytes[] = new byte[1024];
|
||||
while((length = fis.read(bytes)) >= 0)
|
||||
zos.write(bytes, 0, length);
|
||||
|
||||
zos.closeEntry();
|
||||
fis.close();
|
||||
}
|
||||
|
||||
private void cleanup()
|
||||
{
|
||||
for(String filename : generateFilenames)
|
||||
new File(basePath + filename).delete();
|
||||
|
||||
for(String filename : generateDirs)
|
||||
new File(basePath + filename).delete();
|
||||
|
||||
new File(basePath).delete();
|
||||
}
|
||||
|
||||
public String writeArchive()
|
||||
{
|
||||
String date = formatDate(DateHelper.getStartOfToday());
|
||||
|
||||
File dir = context.getExternalCacheDir();
|
||||
|
||||
if(dir == null)
|
||||
{
|
||||
Log.e("CSVExporter", "No suitable directory found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
String zipFilename = String.format("%s/habits-%s.zip", dir, date);
|
||||
|
||||
try
|
||||
{
|
||||
for (Habit h : habits)
|
||||
writeFiles(h);
|
||||
|
||||
writeZipFile(zipFilename);
|
||||
cleanup();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return zipFilename;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
159
app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java
Normal file
159
app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.io;
|
||||
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.CheckmarkList;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.isoron.uhabits.models.ScoreList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class HabitsExporter
|
||||
{
|
||||
private List<Habit> habits;
|
||||
|
||||
private List<String> generateDirs;
|
||||
private List<String> generateFilenames;
|
||||
|
||||
private String exportDirName;
|
||||
|
||||
public HabitsExporter(List<Habit> habits, String exportDirName)
|
||||
{
|
||||
this.habits = habits;
|
||||
this.exportDirName = exportDirName;
|
||||
|
||||
if(!this.exportDirName.endsWith("/"))
|
||||
this.exportDirName += "/";
|
||||
|
||||
generateDirs = new LinkedList<>();
|
||||
generateFilenames = new LinkedList<>();
|
||||
}
|
||||
|
||||
private void writeHabits() throws IOException
|
||||
{
|
||||
String filename = "habits.csv";
|
||||
new File(exportDirName).mkdirs();
|
||||
FileWriter out = new FileWriter(exportDirName + filename);
|
||||
generateFilenames.add(filename);
|
||||
Habit.writeCSV(habits, out);
|
||||
out.close();
|
||||
|
||||
for(Habit h : habits)
|
||||
{
|
||||
String habitDirName = String.format("%s/", h.name);
|
||||
new File(exportDirName + habitDirName).mkdirs();
|
||||
generateDirs.add(habitDirName);
|
||||
|
||||
writeScores(habitDirName, h.scores);
|
||||
writeCheckmarks(habitDirName, h.checkmarks);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeScores(String habitDirName, ScoreList scores) throws IOException
|
||||
{
|
||||
String path = habitDirName + "scores.csv";
|
||||
FileWriter out = new FileWriter(exportDirName + path);
|
||||
generateFilenames.add(path);
|
||||
scores.writeCSV(out);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks) throws IOException
|
||||
{
|
||||
String filename = habitDirName + "checkmarks.csv";
|
||||
FileWriter out = new FileWriter(exportDirName + filename);
|
||||
generateFilenames.add(filename);
|
||||
checkmarks.writeCSV(out);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private String writeZipFile() throws IOException
|
||||
{
|
||||
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
|
||||
String date = dateFormat.format(DateHelper.getStartOfToday());
|
||||
String zipFilename = String.format("%s/habits-%s.zip", exportDirName, date);
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(zipFilename);
|
||||
ZipOutputStream zos = new ZipOutputStream(fos);
|
||||
|
||||
for(String filename : generateFilenames)
|
||||
addFileToZip(zos, filename);
|
||||
|
||||
zos.close();
|
||||
fos.close();
|
||||
|
||||
return zipFilename;
|
||||
}
|
||||
|
||||
private void addFileToZip(ZipOutputStream zos, String filename) throws IOException
|
||||
{
|
||||
FileInputStream fis = new FileInputStream(new File(exportDirName + filename));
|
||||
ZipEntry ze = new ZipEntry(filename);
|
||||
zos.putNextEntry(ze);
|
||||
|
||||
int length;
|
||||
byte bytes[] = new byte[1024];
|
||||
while((length = fis.read(bytes)) >= 0)
|
||||
zos.write(bytes, 0, length);
|
||||
|
||||
zos.closeEntry();
|
||||
fis.close();
|
||||
}
|
||||
|
||||
public String writeArchive()
|
||||
{
|
||||
String zipFilename;
|
||||
|
||||
try
|
||||
{
|
||||
writeHabits();
|
||||
zipFilename = writeZipFile();
|
||||
cleanup();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return zipFilename;
|
||||
}
|
||||
|
||||
private void cleanup()
|
||||
{
|
||||
for(String filename : generateFilenames)
|
||||
new File(exportDirName + filename).delete();
|
||||
|
||||
for(String filename : generateDirs)
|
||||
new File(exportDirName + filename).delete();
|
||||
|
||||
new File(exportDirName).delete();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ import com.activeandroid.query.Select;
|
||||
|
||||
import org.isoron.helpers.DateHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class CheckmarkList
|
||||
@@ -229,4 +233,27 @@ public class CheckmarkList
|
||||
if(today != null) return today.value;
|
||||
else return Checkmark.UNCHECKED;
|
||||
}
|
||||
|
||||
public void writeCSV(Writer out) throws IOException
|
||||
{
|
||||
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
|
||||
|
||||
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
|
||||
String params[] = { habit.getId().toString() };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
if(!cursor.moveToFirst()) return;
|
||||
|
||||
do
|
||||
{
|
||||
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
|
||||
Integer value = cursor.getInt(1);
|
||||
out.write(String.format("%s,%d\n", timestamp, value));
|
||||
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.models;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -33,10 +34,17 @@ import com.activeandroid.query.From;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.activeandroid.query.Update;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
import com.opencsv.CSVReader;
|
||||
import com.opencsv.CSVWriter;
|
||||
|
||||
import org.isoron.helpers.ColorHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -470,4 +478,38 @@ public class Habit extends Model
|
||||
reminderMin = null;
|
||||
reminderDays = DateHelper.ALL_WEEK_DAYS;
|
||||
}
|
||||
|
||||
public static void writeCSV(List<Habit> habits, Writer out) throws IOException
|
||||
{
|
||||
CSVWriter csv = new CSVWriter(out);
|
||||
|
||||
for(Habit habit : habits)
|
||||
{
|
||||
String[] cols = { habit.name, habit.description, Integer.toString(habit.freqNum),
|
||||
Integer.toString(habit.freqDen), ColorHelper.toHTML(habit.color) };
|
||||
csv.writeAll(Collections.singletonList(cols));
|
||||
}
|
||||
|
||||
csv.close();
|
||||
}
|
||||
|
||||
public List<Habit> parseCSV(Reader in)
|
||||
{
|
||||
CSVReader csv = new CSVReader(in);
|
||||
List<Habit> habits = new LinkedList<>();
|
||||
|
||||
for(String cols[] : csv)
|
||||
{
|
||||
Habit habit = new Habit();
|
||||
|
||||
habit.name = cols[0];
|
||||
habit.description = cols[1];
|
||||
habit.freqNum = Integer.parseInt(cols[2]);
|
||||
habit.freqDen = Integer.parseInt(cols[3]);
|
||||
habit.color = Color.parseColor(cols[4]);
|
||||
habits.add(habit);
|
||||
}
|
||||
|
||||
return habits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.ActiveAndroid;
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.From;
|
||||
@@ -33,6 +32,11 @@ import com.activeandroid.query.Select;
|
||||
import org.isoron.helpers.ActiveAndroidHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class ScoreList
|
||||
{
|
||||
@NonNull
|
||||
@@ -278,4 +282,27 @@ public class ScoreList
|
||||
if(score != null) return score.getStarStatus();
|
||||
else return Score.EMPTY_STAR;
|
||||
}
|
||||
|
||||
public void writeCSV(Writer out) throws IOException
|
||||
{
|
||||
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
|
||||
|
||||
String query = "select timestamp, score from score where habit = ? order by timestamp";
|
||||
String params[] = { habit.getId().toString() };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
if(!cursor.moveToFirst()) return;
|
||||
|
||||
do
|
||||
{
|
||||
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
|
||||
String score = String.format("%.2f", ((float) cursor.getInt(1)) / Score.MAX_VALUE);
|
||||
out.write(String.format("%s,%s\n", timestamp, score));
|
||||
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +138,5 @@
|
||||
<string name="five_times_per_week">5 times per week</string>
|
||||
<string name="custom_frequency">Custom …</string>
|
||||
<string name="help">Help & FAQ</string>
|
||||
<string name="could_not_export">Failed to export data.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user